From 1639f05e6ec673f86a54019172a54e2c30610dfa Mon Sep 17 00:00:00 2001 From: Seppe De Loore Date: Tue, 12 May 2026 10:47:23 +0200 Subject: [PATCH] Voeg Pandas bestandsformaat-converter demo toe Programma dat CSV, Excel, JSON, vaste-breedte tekst, pickle en Python-snippets onderling converteert via een Pandas DataFrame. Demonstreert dtype-afleiding, datetime-parsing met tijdzonebeheer (UTC-normalisatie) en het omzetten naar een lijst-van-dicts. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 15 ++ README.md | 408 +++++++++++++++++++++++++++++++++ main.py | 598 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 10 + sample.txt | 6 + uv.lock | 132 +++++++++++ 6 files changed, 1169 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 main.py create mode 100644 pyproject.toml create mode 100644 sample.txt create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a5d1faf --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv +.claude/ +.wolf/ +.python-version + +CLAUDE.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..2322448 --- /dev/null +++ b/README.md @@ -0,0 +1,408 @@ +# Joppe — Pandas Bestandsformaat-Converter + +Een klein demo-programma dat een databestand inleest in een Pandas +DataFrame en het terug wegschrijft in een ander formaat. Gemaakt om +een Python-ontwikkelaar te tonen hoe Pandas formaatconversie +omvormt tot een tweetraps-pijplijn: + +```text +bestand op schijf ──[lezer]──▶ DataFrame ──[schrijver]──▶ bestand op schijf +``` + +Ondersteunde formaten (automatisch gekozen op basis van de bestandsextensie): + +| Extensie | Formaat | Pandas-lezer | Pandas-schrijver | +| -------- | ------------------------------- | --------------- | ---------------- | +| `.txt` | Tekst met vaste breedte | `pd.read_fwf` | zelf gemaakt | +| `.csv` | Kommagescheiden waarden | `pd.read_csv` | `df.to_csv` | +| `.xlsx` | Excel-werkmap | `pd.read_excel` | `df.to_excel` | +| `.json` | JSON (records) | `pd.read_json` | `df.to_json` | +| `.pkl` | Python pickle | `pickle.loads` | `pickle.dumps` | +| `.py` | Python-snippet (alleen uitvoer) | — | zelf gemaakt | + +## uv — Pakketbeheer voor Python + +[uv](https://docs.astral.sh/uv/) is een moderne, razendsnel pakketbeheerder +voor Python. Het vervangt de combinatie van `pip`, `venv` en `pip-tools` met +één enkel gereedschap. + +### uv installeren + +**Linux / macOS:** + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +**Windows (PowerShell):** + +```powershell +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +**Via pip (als je al een werkende Python hebt):** + +```bash +pip install uv +``` + +Controleer de installatie: + +```bash +uv --version +``` + +### Nieuw project aanmaken + +```bash +uv init mijn-project +cd mijn-project +``` + +`uv init` maakt automatisch aan: + +| Bestand | Inhoud | +| ------------------ | ---------------------------------------------------- | +| `pyproject.toml` | Projectnaam, Python-versie, dependencies | +| `.python-version` | Gewenste Python-versie (bv. `3.14`) | +| `main.py` | Leeg startbestand | +| `README.md` | Leeg documentatiebestand | + +### Libraries toevoegen + +```bash +uv add pandas openpyxl +``` + +`uv add` doet drie dingen tegelijk: +1. Voegt de library toe aan `dependencies` in `pyproject.toml`. +2. Bepaalt compatibele versies en schrijft ze vast in `uv.lock`. +3. Installeert alles in de virtuele omgeving van het project (`.venv/`). + +Een library verwijderen gaat met: + +```bash +uv remove openpyxl +``` + +Een specifieke versie pinnen: + +```bash +uv add "pandas>=2.0,<4" +``` + +### pyproject.toml van dit project + +```toml +[project] +name = "joppe" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ + "openpyxl>=3.1.5", + "pandas>=3.0.2", +] +``` + +`requires-python` garandeert dat uv een compatibele Python-versie kiest. +`dependencies` bevat de minimale versievereisten; de exacte vergrendelde +versies staan in `uv.lock`. + +### Dit project installeren + +Clone de repository en voer daarna uit: + +```bash +uv sync +``` + +`uv sync` leest `uv.lock`, installeert de exacte versies en maakt `.venv/` +aan als die nog niet bestaat. Alle collega's krijgen zo exact dezelfde +omgeving. + +### Scripts uitvoeren + +```bash +uv run python main.py sample.txt sample.csv +``` + +`uv run` zorgt automatisch dat de virtuele omgeving van het project actief +is. Je hoeft `.venv/bin/activate` nooit handmatig te roepen. + +Wil je een gewone Python-REPL in de projectomgeving? + +```bash +uv run python +``` + +## Gebruik + +```bash +uv run python main.py +``` + +Het programma kijkt naar de **invoer**-extensie om de lezer te kiezen, en +naar de **uitvoer**-extensie om de schrijver te kiezen. Elke combinatie +werkt. + +## Voorbeelden + +Een voorbeeldbestand met vaste breedte, `sample.txt`, is meegeleverd om +alle formaten te testen. Het bevat 5 rijen en 6 kolommen die een mix van +datatypes dekken: integer (`id`, `quantity`), string (`name`), float +(`price`), ISO-datetime met tijdzone (`created_at`) en datum in +dd/mm/yyyy-formaat (`delivery_date`). + +### 1. Vaste breedte → CSV + +```bash +uv run python main.py sample.txt sample.csv +``` + +```csv +id,name,price,quantity,created_at,delivery_date +1,Widget,9.99,10,2026-05-08T12:30:00+0000,2026-05-15T00:00:00+0000 +2,Gadget,19.95,5,2026-04-12T07:00:00+0000,2026-05-20T00:00:00+0000 +3,Sprocket,0.5,250,2026-03-22T07:15:00+0000,2026-06-10T00:00:00+0000 +``` + +Merk op dat `created_at` van `+02:00` (lokale tijd Brussel) naar +`+0000` (UTC) is omgezet: de tijd schuift van 14:30 naar 12:30, maar +het is hetzelfde absolute moment. Dat is de gouden regel — _opslaan +in UTC_. + +### 2. CSV → JSON + +```bash +uv run python main.py sample.csv sample.json +``` + +```json +[ + { + "id": 1, + "name": "Widget", + "price": 9.99, + "quantity": 10, + "created_at": "2026-05-08T12:30:00.000Z", + "delivery_date": "2026-05-15T00:00:00.000Z" + } +] +``` + +`date_format="iso"` is meegegeven aan `to_json` zodat datums leesbaar +blijven in plaats van als Unix-milliseconden te verschijnen. + +### 3. JSON → Excel + +```bash +uv run python main.py sample.json sample.xlsx +``` + +Excel-cellen kunnen geen tijdzone bevatten, dus het programma laat de +tz-info vallen vlak voor het wegschrijven (de UTC-tijd zelf blijft +behouden in de Excel-cel). + +### 4. Excel → vaste breedte + +```bash +uv run python main.py sample.xlsx roundtrip.txt +``` + +In de uitgevoerde `.txt` staat `created_at` weer in ISO 8601 met offset +en `delivery_date` weer in `dd/mm/yyyy` — elk in zijn eigen kolomstijl. + +### 5. Naar pickle (.pkl) + +```bash +uv run python main.py sample.txt sample.pkl +# of vanuit CSV, JSON, Excel — werkt allemaal +uv run python main.py sample.csv sample.pkl +``` + +Het pickle-bestand bevat een lijst van gewone Python-dicts met standaard +types (`int`, `float`, `datetime`). Daarna uitlezen: + +```python +import pickle + +with open("sample.pkl", "rb") as f: + records = pickle.load(f) + +print(records[0]) +# {'id': 1, 'name': 'Widget', 'price': 9.99, 'quantity': 10, +# 'created_at': datetime(2026, 5, 8, 12, 30, 0, tzinfo=timezone.utc), ...} + +print(type(records[0]["id"])) # +print(type(records[0]["created_at"])) # +``` + +Een pickle opnieuw inlezen als DataFrame: + +```bash +uv run python main.py sample.pkl sample.csv +``` + +### 6. Naar Python-snippet (.py) + +```bash +uv run python main.py sample.txt sample.py +# of vanuit CSV, Excel, JSON, pickle — allemaal ondersteund +uv run python main.py sample.csv sample.py +``` + +Dit genereert een kant-en-klaar Python-bestand: + +```python +# Auto-gegenereerd door main.py +from datetime import datetime, timezone + +DATA = [ + { + 'id': 1, + 'name': 'Widget', + 'price': 9.99, + 'quantity': 10, + 'created_at': datetime(2026, 5, 8, 12, 30, 0, tzinfo=timezone.utc), + 'delivery_date': datetime(2026, 5, 15, 0, 0, 0, tzinfo=timezone.utc), + }, + ... +] +``` + +Je kunt dit bestand kopiëren in een project of direct importeren: + +```python +from sample import DATA + +for row in DATA: + print(row["name"], row["created_at"]) +``` + +Datetimes zijn echte `datetime`-objecten (niet strings), zodat je ze +meteen kunt gebruiken voor berekeningen of vergelijkingen. + +### 7. Volledige rondreis + +Je kan de conversies aaneenschakelen om te controleren dat er niets +verloren gaat: + +```bash +uv run python main.py sample.txt stap1.csv +uv run python main.py stap1.csv stap2.json +uv run python main.py stap2.json stap3.xlsx +uv run python main.py stap3.xlsx stap4.pkl +uv run python main.py stap4.pkl eind.py +``` + +## Van DataFrame naar lijst-van-dicts + +De spil van de `.pkl`- en `.py`-uitvoer is `df.to_dict(orient="records")`. +Dit zet elke rij van het DataFrame om naar één dict: + +```python +records = df.to_dict(orient="records") +# [{"id": 1, "name": "Widget", ...}, {"id": 2, ...}, ...] +``` + +En terug de andere kant op: + +```python +df = pd.DataFrame(records) +``` + +### De `orient`-parameter + +| orient | Resultaat | Gebruik | +| ----------- | ------------------------------------------ | ------------------------------ | +| `"records"` | `[{"col": val, ...}, ...]` | REST-API, fixtures | +| `"dict"` | `{"col": {0: val, 1: val, ...}}` | kolom-opzoektabellen | +| `"list"` | `{"col": [val, val, ...]}` | kolom-georiënteerde verwerking | +| `"index"` | `{0: {"col": val}, ...}` | rij-index als sleutel | +| `"split"` | `{"columns": [...], "data": [[...], ...]}` | compacte overdracht | + +Voor de meeste situaties is `"records"` de juiste keuze. + +### Opgelet: NumPy/Pandas-types in dicts + +`to_dict()` levert soms `numpy.int64` of `pd.Timestamp` terug — die zijn +**niet** JSON-serialiseerbaar en gedragen zich subtiel anders dan ingebouwde +Python-types. Converteer ze expliciet: + +```python +import numpy as np + +def to_plain_python(row: dict) -> dict: + result = {} + for k, v in row.items(): + if isinstance(v, pd.Timestamp): + result[k] = v.to_pydatetime() # pd.Timestamp -> datetime + elif isinstance(v, np.integer): + result[k] = int(v) # numpy.int64 -> int + elif isinstance(v, np.floating): + result[k] = float(v) # numpy.float64 -> float + else: + result[k] = v + return result + +records = [to_plain_python(r) for r in df.to_dict(orient="records")] +``` + +Dit programma doet dit automatisch in `df_to_records()` — zodat zowel +pickle als het Python-snippet correcte standaard-types bevatten. + +## Datums en tijdzones + +Het demo-bestand mengt twee veelvoorkomende datumformaten in dezelfde +rijen: + +- `created_at` in ISO 8601 met tijdzone-offset, bv. + `2026-05-08T14:30:00+02:00` (lokale Brusselse tijd). +- `delivery_date` in dag-eerst notatie, bv. `15/05/2026` (geen tijd, + geen tijdzone). + +Bij het inlezen normaliseert `parse_datetimes()` beide naar UTC met +`pd.to_datetime(..., format="mixed", utc=True)`. De truc: + +- `format="mixed"` laat Pandas per rij raden welk formaat van + toepassing is — handig wanneer dezelfde kolom meerdere stijlen bevat. +- `utc=True` zorgt dat alles tijdzone-bewust is in UTC, zodat naïeve + en bewuste timestamps niet door elkaar lopen. + +Voor de dd/mm/yyyy-kolom wordt extra `dayfirst=True` aangezet wanneer +de waarden schuine strepen bevatten — anders zou Pandas `"01/02/2026"` +als `2 januari` lezen in plaats van `1 februari`. + +### De drie nuttige tijdzone-operaties + +```python +# 1. Parse + normaliseer naar UTC in één stap (altijd veilig). +df["created_at"] = pd.to_datetime(df["created_at"], utc=True) + +# 2. Plak een tijdzone op een naïeve timestamp (je weet welke zone bedoeld is). +df["created_at"] = df["created_at"].dt.tz_localize("Europe/Brussels") + +# 3. Reken om naar een andere tijdzone (zelfde moment, andere weergave). +df["created_at"] = df["created_at"].dt.tz_convert("Europe/Brussels") +``` + +De gouden regel: **opslaan in UTC, weergeven in lokale tijd**. Roep +operatie 3 alleen aan vlak voor het tonen of wegschrijven. + +## Wat te bestuderen in `main.py` + +- **`read_any` / `write_any`** — de keuze op basis van de bestandsextensie. + Dat is het volledige idee van "formaatconversie" in 10 regels. +- **Argumenten van `pd.read_fwf`** — `colspecs`, `names`, en vooral + `dtype`. Vaste-breedte bestanden bevatten geen type-informatie, dus + je moet Pandas vertellen wat elke kolom is. +- **`parse_datetimes`** — laat zien hoe je gemengde datumformaten + (ISO én dd/mm/yyyy) in één pass omzet naar UTC-Timestamps. +- **`df_to_records` + `_to_plain_python`** — het omzetten van DataFrame + naar lijst-van-dicts met schone Python-types. +- **`write_fwf`** — een uitgewerkt voorbeeld van uitvoer bouwen vanuit + een DataFrame door over kolommen en rijen te itereren. Handig wanneer + er geen ingebouwde schrijver bestaat voor je doelformaat. + +Bovenaan `main.py` staat een lange docstring die elke gebruikte lezer en +schrijver doorloopt; lees die naast de code. diff --git a/main.py b/main.py new file mode 100644 index 0000000..ed2d6c5 --- /dev/null +++ b/main.py @@ -0,0 +1,598 @@ +""" +Pandas Bestandsformaat-Converter — Demo voor Python developers +===================================================================== + +Dit programma leest een databestand in één van vier formaten (vaste-breedte +tekst, Excel, JSON, CSV) in een Pandas DataFrame en schrijft het terug naar +één van diezelfde vier formaten. Het invoerformaat wordt afgeleid uit de +extensie van het invoerbestand, en het uitvoerformaat uit de extensie van +het uitvoerbestand. + +Gebruik: + uv run main.py + +Voorbeelden: + uv run main.py sample.txt sample.csv + uv run main.py sample.csv sample.xlsx + uv run main.py sample.xlsx sample.json + uv run main.py sample.json sample2.txt # schrijft vaste breedte + uv run main.py sample.csv sample.pkl # pickle van lijst-van-dicts + uv run main.py sample.csv sample.py # Python-snippet met ingebedde data + +Ondersteunde extensies invoer: + .txt -> vaste-breedte tekst + .csv -> kommagescheiden waarden + .xlsx -> Excel-werkmap + .json -> JSON (records-oriëntatie) + .pkl -> pickle van een lijst-van-dicts of een DataFrame + +Ondersteunde extensies uitvoer: + .txt -> vaste-breedte tekst + .csv -> kommagescheiden waarden + .xlsx -> Excel-werkmap + .json -> JSON (records-oriëntatie) + .pkl -> pickle van een lijst-van-dicts + .py -> plakbaar Python-fragment met DATA = [...] + + +Hoe Pandas de conversie afhandelt +--------------------------------- +Een Pandas DataFrame is een 2-dimensionale, in-memory, gelabelde tabel — +zie het als een spreadsheet die in Python leeft. De truc die formaat- +conversie eenvoudig maakt, is dat Pandas I/O ontkoppelt van het datamodel: + + bestand op schijf --[lezer]--> DataFrame --[schrijver]--> bestand op schijf + +Zodra de data in een DataFrame staat, maakt het niet meer uit waar ze +vandaan komt. Elke lezer produceert hetzelfde soort object, en elke +schrijver accepteert datzelfde object. Conversie is dus gewoon: "lees +in een DataFrame, schrijf het DataFrame uit in het nieuwe formaat." + +Hier gebruikte Pandas-lezers: + + * pd.read_fwf(pad, colspecs=..., names=..., dtype=...) + Vaste-breedte bestanden hebben geen scheidingsteken; elke kolom + bezet een specifiek karakterbereik. `colspecs` is een lijst van + (start, eind) tuples (eind-exclusief), `names` levert de + kolomkoppen (omdat het bestand er geen heeft), en `dtype` vertelt + Pandas welk Python/NumPy-type elke kolom moet krijgen. + + * pd.read_csv(pad) + CSV is het eenvoudigste formaat. Pandas leidt de types (int, + float, string) automatisch per kolom af. + + * pd.read_excel(pad) + Leest .xlsx via de `openpyxl`-engine (een extra dependency). + Pandas behandelt types vergelijkbaar met CSV. + + * pd.read_json(pad, orient="records") + JSON kan veel verschillende vormen hebben; `orient="records"` + verwacht een lijst van objecten (`[{"kolom": waarde, ...}, ...]`), + wat de meest natuurlijke vertaling naar een DataFrame is. + +Hier gebruikte Pandas-schrijvers: + + * df.to_csv(pad, index=False, date_format=...) + `index=False` voorkomt dat Pandas de rijnummer-index als + extra kolom wegschrijft. `date_format` regelt hoe Timestamps + als tekst worden geserialiseerd. + + * df.to_excel(pad, index=False) + Gebruikt onder de motorkap eveneens openpyxl voor .xlsx-uitvoer. + Datetimes worden als echte Excel-datumcellen weggeschreven. + + * df.to_json(pad, orient="records", indent=2, date_format="iso") + `indent=2` maakt het bestand leesbaar voor mensen. + `date_format="iso"` voorkomt de standaard Unix-milliseconden. + + * Vaste-breedte UITVOER heeft geen ingebouwde `to_fwf`. We bouwen het + handmatig: per kolom de maximale benodigde breedte bepalen + (kop versus waarden), en daarna elke cel opvullen met + str.ljust / str.rjust tot die breedte. Numerieke kolommen + worden rechts uitgelijnd; tekstkolommen links. + + +Waarom dtypes ertoe doen +------------------------ +Wanneer je een vaste-breedte bestand inleest, is elke "cel" aanvankelijk +gewoon een stuk tekst. Als je Pandas niet vertelt welk type bedoeld is, +kan een kolom met "42" als string eindigen en later rekenkundige bewerkingen +breken. Daarom wordt het `dtype`-argument hieronder expliciet meegegeven +aan `read_fwf`. CSV/Excel/JSON-lezers doen redelijke inferentie zelf, +maar vaste breedte is dom over types — je moet het sturen. + + +Datums en tijden — verschillende formaten +----------------------------------------- +Datums leven in bestanden als gewone tekst, maar in een DataFrame willen we +ze als een echt `datetime64`-type zodat we erop kunnen sorteren, filteren, +optellen ("+1 dag"), enz. De brugfunctie is `pd.to_datetime(...)`. + +Veelvoorkomende formaten in de praktijk: + + * ISO 8601: "2026-05-08T14:30:00+02:00" (machine-vriendelijk) + * Dag eerst (NL/EU): "08/05/2026" + * Maand eerst (US): "05/08/2026" + * Met of zonder tijdzone-offset (`+02:00`, `Z`, of niets) + +Belangrijkste argumenten van `pd.to_datetime`: + + * `format=...` Expliciete strftime-string, bv. "%d/%m/%Y". + Snelste en betrouwbaarste optie. + * `format="mixed"` Pandas herkent het formaat per rij. Handig + wanneer dezelfde kolom meerdere formaten + bevat (bv. ISO én dd/mm/yyyy door elkaar). + * `dayfirst=True` Voor ambigue waarden zoals "01/02/2026" + ("1 februari" i.p.v. "2 januari"). + * `utc=True` Zet alles om naar UTC. Dit is cruciaal als + sommige rijen wél en andere géén tijdzone- + offset hebben — anders krijg je een mengsel + van naïeve en bewuste timestamps en kan je + niet vergelijken. + * `errors="coerce"` Onparseerbare waarden worden `NaT` (Not-a- + Time) i.p.v. een crash. + +Tijdzones in 30 seconden +------------------------ +Pandas onderscheidt **naïeve** en **tijdzone-bewuste** timestamps: + + - Naïef: "2026-05-08 14:30:00" (geen offset, geen tz) + - Bewust: "2026-05-08 14:30:00+02:00" (heeft offset / zone) + +Drie operaties dekken 95% van alle gebruik: + + 1. `pd.to_datetime(serie, utc=True)` + Eén stap: parse en zet om naar UTC. Inputs met offset worden + omgerekend; inputs zonder offset worden als UTC geïnterpreteerd. + Als je niets weet over het bronbestand, is dit altijd veilig. + + 2. `serie.dt.tz_localize("Europe/Brussels")` + Plak een tijdzone op een naïeve timestamp. Gebruik dit als je + weet dat lokale tijd bedoeld was maar de offset niet meegeschreven + is. Werkt enkel op naïeve timestamps. + + 3. `serie.dt.tz_convert("Europe/Brussels")` + Reken een bewuste timestamp om naar een andere tijdzone (de + absolute tijd verandert niet, alleen de weergave). + +De gouden regel: **opslaan in UTC, weergeven in lokale tijd**. Daarom +roept dit programma `pd.to_datetime(..., utc=True)` aan in +`parse_datetimes()`. Wil je het uitvoerbestand in lokale tijd? Pas +`.dt.tz_convert("Europe/Brussels")` toe voordat je schrijft. + +Datums zonder tijd (zoals `delivery_date` in dd/mm/yyyy) hebben in theorie +geen tijdzone nodig, maar Pandas heeft geen apart "date"-type: het wordt +een Timestamp op middernacht. Met `utc=True` is dat middernacht UTC. Voor +pure datums maakt dat verder weinig uit. + + +Van DataFrame naar lijst-van-dicts +----------------------------------- +Een DataFrame is handig voor berekeningen, maar soms wil je de data als +gewone Python-structuren: een lijst van dictionaries, waarbij elke dict +één rij voorstelt. Dit is de standaardvorm die je doorgeeft aan REST-APIs, +Jinja2-templates, unittest-fixtures, enz. + +De sleutel is: + + records = df.to_dict(orient="records") + +Het resultaat is een list[dict], bv.: + [ + {"id": 1, "name": "Widget", "price": 9.99, ...}, + {"id": 2, "name": "Gadget", "price": 19.95, ...}, + ] + +Terug de andere kant op — van lijst naar DataFrame — werkt via: + + df = pd.DataFrame(records) + +Hierbij leidt Pandas de kolomnamen en -types opnieuw af uit de dicts. + +De `orient`-parameter bepaalt de structuur van de output: + + orient="records" -> [{"col": val, ...}, ...] (rij per dict) *meest gebruikt* + orient="dict" -> {"col": {0: val, 1: val}} (kolom per dict, rij-index als sleutel) + orient="list" -> {"col": [val, val, ...]} (kolom per lijst) + orient="index" -> {0: {"col": val}, ...} (rij-index als sleutel) + orient="split" -> {"columns": [...], "data": [[...], ...]} (compacte vorm) + +Voor de meeste dagelijkse situaties is `orient="records"` de juiste keuze: +het sluit het beste aan bij hoe mensen over rijen denken. + +Aandacht voor Pandas-types in dicts +------------------------------------ +`to_dict()` converteert waarden naar Python-types waar het kan, maar +sommige types blijven Pandas/NumPy-specifiek: + + - int-kolommen -> numpy.int64 (werkt als int, maar type is anders) + - float-kolommen -> numpy.float64 + - datetime-kolommen -> pandas.Timestamp (subklasse van datetime.datetime) + +Dit kan problemen geven bij: + * json.dumps(records) — NumPy-types zijn niet JSON-serialiseerbaar. + * pickle — werkt prima, Timestamps zijn picklable. + * vergelijking met isinstance(v, int) — np.int64 is géén int. + +Oplossing: converteer de records expliciet naar standaard Python-types: + + import numpy as np + + def to_plain_python(record: dict) -> dict: + result = {} + for k, v in record.items(): + if isinstance(v, pd.Timestamp): + result[k] = v.to_pydatetime() # -> datetime.datetime + elif isinstance(v, (np.integer,)): + result[k] = int(v) # -> int + elif isinstance(v, (np.floating,)): + result[k] = float(v) # -> float + else: + result[k] = v + return result + + records = [to_plain_python(r) for r in df.to_dict(orient="records")] + +Dit programma past die conversie toe in `df_to_records()` — zodat zowel +pickle als het Python-snippet correcte standaard-types bevatten. + +Pickle — binaire opslag van Python-objecten +-------------------------------------------- +Pickle (.pkl) is Python's ingebouwde binaire serialisatieformaat. Het kan +vrijwel elk Python-object opslaan: lijsten, dicts, datetime-objecten, enz. + + import pickle + + # Wegschrijven: + with open("data.pkl", "wb") as f: # "wb" = write binary + pickle.dump(records, f) + + # Inlezen: + with open("data.pkl", "rb") as f: # "rb" = read binary + records = pickle.load(f) + +Voordelen: + + Razendsnel (binair formaat, geen tekst-parsing) + + Behoudt exacte Python-types inclusief datetime met tijdzone + + Geschikt voor caching van tussenresultaten + +Nadelen: + - Niet leesbaar voor mensen of andere programmeertalen + - Pickle-bestanden van de ene Python-versie werken mogelijk niet in + een andere (gebruik het dus niet als uitwisselingsformaat) + - Nooit een pickle inlezen van een onbekende bron — het kan + arbitraire code uitvoeren bij het laden (veiligheidsrisico) + +Python-snippet (.py) — data direct in code +------------------------------------------- +Soms wil je testdata of fixtures rechtstreeks in een Python-bestand +inbedden, zodat je ze kunt importeren zonder een extern bestand nodig te +hebben. Dit programma genereert een `.py`-bestand met een `DATA`-variabele: + + from datetime import datetime, timezone + + DATA = [ + { + "id": 1, + "name": "Widget", + "price": 9.99, + "quantity": 10, + "created_at": datetime(2026, 5, 8, 12, 30, 0, tzinfo=timezone.utc), + "delivery_date": datetime(2026, 5, 15, 0, 0, 0, tzinfo=timezone.utc), + }, + ... + ] + +Dit bestand kan je kopiëren in een project of importeren: + + from sample import DATA + for row in DATA: + print(row["name"], row["created_at"]) + +Datetimes worden als `datetime(...)` uitgedrukt (niet als strings) zodat +de types meteen correct zijn bij importeren. +""" + +from __future__ import annotations + +import pickle +import sys +from datetime import datetime, timezone +from pathlib import Path + +import numpy as np +import pandas as pd + + +# Schema voor het voorbeeld-bestand met vaste breedte. Elke tuple is +# (start, eind_exclusief). Deze bereiken definiëren de karakterposities +# die elk veld inneemt. +FWF_COLSPECS = [(0, 5), (5, 25), (25, 35), (35, 45), (45, 72), (72, 85)] +FWF_NAMES = ["id", "name", "price", "quantity", "created_at", "delivery_date"] +FWF_DTYPES = { + "id": "int64", + "name": "string", + "price": "float64", + "quantity": "int64", + # Datetime-kolommen lezen we eerst als string en parsen we daarna + # uniform met pd.to_datetime — zo werkt dezelfde code voor alle formaten. + "created_at": "string", + "delivery_date": "string", +} + +# Datetime-kolommen + hoe we ze terug naar tekst formatteren bij het +# wegschrijven naar vaste breedte. Bij het inlezen herkennen we het +# formaat automatisch (ISO of dd/mm/yyyy) via format="mixed". +DATETIME_COLUMNS: dict[str, str] = { + "created_at": "%Y-%m-%dT%H:%M:%S%z", # ISO 8601 met offset + "delivery_date": "%d/%m/%Y", # dag eerst (NL/EU) +} + + +def parse_datetimes(df: pd.DataFrame) -> pd.DataFrame: + """Zet datetime-kolommen om naar tijdzone-bewuste UTC Timestamps. + + Werkt voor alle invoerformaten: + - txt/csv/json: kolommen komen binnen als string en worden geparsed. + - xlsx: Excel parseert datums al zelf; we zorgen alleen dat + ze tijdzone-bewust (UTC) zijn. + + `format="mixed"` laat Pandas per rij het formaat raden, zodat ISO + ("2026-05-08T14:30:00+02:00") én dd/mm/yyyy ("15/05/2026") in dezelfde + kolom kunnen voorkomen — handig wanneer data uit verschillende bronnen + samengevoegd wordt. + + `utc=True` is de sleutel voor tijdzone-veiligheid: alles wordt + genormaliseerd naar UTC, ongeacht of de input een offset had of niet. + """ + for col in DATETIME_COLUMNS: + if col not in df.columns: + continue + series = df[col] + + # Als de kolom al datetime is (bv. uit Excel), alleen de tijdzone fixen. + if pd.api.types.is_datetime64_any_dtype(series): + if series.dt.tz is None: + df[col] = series.dt.tz_localize("UTC") + else: + df[col] = series.dt.tz_convert("UTC") + continue + + # Anders: parse de strings. We "snuiven" of de waarden in + # dd/mm/yyyy-stijl staan (bevatten een schuine streep) of in + # ISO-stijl (yyyy-mm-dd). dayfirst=True is enkel veilig voor de + # eerste vorm — op ISO-strings zou het maand en dag verwisselen. + sample_values = series.dropna().astype(str) + looks_ddmm = len(sample_values) > 0 and sample_values.str.contains("/").any() + df[col] = pd.to_datetime( + series, + format="mixed", + dayfirst=looks_ddmm, + utc=True, + errors="coerce", + ) + return df + + +def format_value(value: object, col: str) -> str: + """Formatteer één celwaarde voor tekstuitvoer (vaste breedte).""" + if pd.isna(value): + return "" + if col in DATETIME_COLUMNS and isinstance(value, pd.Timestamp): + return value.strftime(DATETIME_COLUMNS[col]) + return str(value) + + +def _to_plain_python(value: object) -> object: + """Zet een Pandas/NumPy-waarde om naar een standaard Python-type. + + `df.to_dict()` geeft soms numpy.int64, numpy.float64 of pd.Timestamp + terug. Die zijn niet JSON-serialiseerbaar en gedragen zich subtiel anders + dan de ingebouwde int/float/datetime. Deze functie normaliseert ze. + """ + if isinstance(value, pd.Timestamp): + return value.to_pydatetime() # pd.Timestamp -> datetime.datetime (met tz) + if isinstance(value, np.integer): + return int(value) # numpy.int64 -> int + if isinstance(value, np.floating): + return float(value) # numpy.float64 -> float + if isinstance(value, float) and np.isnan(value): + return None # NaN -> None (JSON-vriendelijk) + return value + + +def df_to_records(df: pd.DataFrame) -> list[dict]: + """Zet een DataFrame om naar een lijst van gewone Python-dicts. + + Stap 1: df.to_dict(orient="records") geeft [{"col": val, ...}, ...] + Stap 2: _to_plain_python() vervangt NumPy/Pandas-types door standaard + Python-types zodat pickle, json.dumps en isinstance() correct werken. + """ + raw = df.to_dict(orient="records") + return [{k: _to_plain_python(v) for k, v in row.items()} for row in raw] + + +def _datetime_repr(dt: datetime) -> str: + """Genereer een leesbare Python-expressie voor een datetime-object.""" + if dt.tzinfo is not None: + return ( + f"datetime({dt.year}, {dt.month}, {dt.day}, " + f"{dt.hour}, {dt.minute}, {dt.second}, tzinfo=timezone.utc)" + ) + return ( + f"datetime({dt.year}, {dt.month}, {dt.day}, " + f"{dt.hour}, {dt.minute}, {dt.second})" + ) + + +def _value_repr(value: object) -> str: + """Genereer een Python-literal voor één waarde in het .py-snippet.""" + if isinstance(value, datetime): + return _datetime_repr(value) + return repr(value) + + +def read_any(path: Path) -> pd.DataFrame: + """Stuur door naar de juiste Pandas-lezer op basis van de bestandsextensie.""" + ext = path.suffix.lower() + if ext == ".txt": + # Vaste breedte: kolomgrenzen, namen en dtypes moeten meegegeven worden. + # `skiprows=1` slaat de leesbare kopregel in het voorbeeldbestand over. + df = pd.read_fwf( + path, + colspecs=FWF_COLSPECS, + names=FWF_NAMES, + dtype=FWF_DTYPES, + skiprows=1, + ) + elif ext == ".csv": + df = pd.read_csv(path) + elif ext == ".xlsx": + df = pd.read_excel(path) + elif ext == ".json": + df = pd.read_json(path, orient="records") + elif ext == ".pkl": + # Een pickle kan een lijst-van-dicts of een DataFrame zijn. + data = pickle.loads(path.read_bytes()) + df = pd.DataFrame(data) if isinstance(data, list) else data + else: + raise ValueError(f"Niet-ondersteunde invoerextensie: {ext}") + return parse_datetimes(df) + + +def write_fwf(df: pd.DataFrame, path: Path) -> None: + """Schrijf een DataFrame weg als een tekstbestand met vaste breedte. + + Pandas heeft geen ingebouwde `to_fwf`, dus we doen het zelf: kies een + breedte per kolom (max van de kopbreedte en de langste waarde) en vul + daarna elke cel op. Numerieke kolommen worden rechts uitgelijnd, + tekstkolommen en datums links — dat is de gangbare opmaak voor data + met vaste breedte. + """ + widths: dict[str, int] = {} + for col in df.columns: + header_w = len(str(col)) + formatted = df[col].apply(lambda v, c=col: format_value(v, c)) + values_w = int(formatted.map(len).max() or 0) + widths[col] = max(header_w, values_w) + 2 # +2 voor wat ademruimte + + def fmt_cell(value: object, col: str) -> str: + s = format_value(value, col) + if pd.api.types.is_numeric_dtype(df[col]): + return s.rjust(widths[col]) + return s.ljust(widths[col]) + + lines: list[str] = [] + header = "".join( + str(col).rjust(widths[col]) if pd.api.types.is_numeric_dtype(df[col]) + else str(col).ljust(widths[col]) + for col in df.columns + ) + lines.append(header) + for _, row in df.iterrows(): + lines.append("".join(fmt_cell(row[col], col) for col in df.columns)) + + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def write_pkl(df: pd.DataFrame, path: Path) -> None: + """Schrijf de DataFrame weg als een pickle van een lijst-van-dicts. + + We gebruiken df_to_records() zodat de pickle standaard Python-types + bevat (int, float, datetime) in plaats van NumPy/Pandas-specifieke types. + """ + records = df_to_records(df) + path.write_bytes(pickle.dumps(records)) + + +def write_py(df: pd.DataFrame, path: Path) -> None: + """Genereer een plakbaar Python-bestand met DATA = [...]. + + Het resultaat is een geldig .py-bestand dat je rechtstreeks kunt + importeren: + + from sample import DATA + for row in DATA: + print(row["name"]) + + Datetimes worden als `datetime(...)` uitgedrukt zodat je ze kunt + gebruiken zonder extra parsing. + """ + records = df_to_records(df) + + needs_datetime = any( + isinstance(v, datetime) + for row in records + for v in row.values() + ) + + lines: list[str] = ["# Auto-gegenereerd door main.py\n"] + if needs_datetime: + lines.append("from datetime import datetime, timezone\n") + lines.append("\nDATA = [\n") + + for row in records: + lines.append(" {\n") + for key, value in row.items(): + lines.append(f" {key!r}: {_value_repr(value)},\n") + lines.append(" },\n") + + lines.append("]\n") + path.write_text("".join(lines), encoding="utf-8") + + +def write_any(df: pd.DataFrame, path: Path) -> None: + """Stuur door naar de juiste Pandas-schrijver op basis van de bestandsextensie.""" + ext = path.suffix.lower() + if ext == ".txt": + write_fwf(df, path) + elif ext == ".csv": + # ISO 8601 met offset is de veiligste "tekstvorm" voor datetimes. + df.to_csv(path, index=False, date_format="%Y-%m-%dT%H:%M:%S%z") + elif ext == ".xlsx": + # Excel-cellen kunnen geen tijdzone bevatten — drop tz vlak voor + # het schrijven, zodat openpyxl niet struikelt. + df_out = df.copy() + for col in DATETIME_COLUMNS: + if col in df_out.columns and pd.api.types.is_datetime64_any_dtype(df_out[col]): + if df_out[col].dt.tz is not None: + df_out[col] = df_out[col].dt.tz_convert("UTC").dt.tz_localize(None) + df_out.to_excel(path, index=False) + elif ext == ".json": + # date_format="iso" voorkomt Unix-milliseconden in de JSON. + df.to_json(path, orient="records", indent=2, date_format="iso") + elif ext == ".pkl": + write_pkl(df, path) + elif ext == ".py": + write_py(df, path) + else: + raise ValueError(f"Niet-ondersteunde uitvoerextensie: {ext}") + + +def main(argv: list[str]) -> int: + if len(argv) != 3: + print(__doc__) + print("FOUT: precies twee argumenten verwacht: ") + return 1 + + in_path = Path(argv[1]) + out_path = Path(argv[2]) + + if not in_path.exists(): + print(f"FOUT: invoerbestand bestaat niet: {in_path}") + return 1 + + print(f"Bezig met lezen van {in_path} ({in_path.suffix}) ...") + df = read_any(in_path) + print(f"{len(df)} rijen geladen, {len(df.columns)} kolommen.") + print("Voorbeeld:") + print(df.head().to_string(index=False)) + + print(f"Bezig met schrijven van {out_path} ({out_path.suffix}) ...") + write_any(df, out_path) + print("Klaar.") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..53cff4f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "joppe" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.14" +dependencies = [ + "openpyxl>=3.1.5", + "pandas>=3.0.2", +] diff --git a/sample.txt b/sample.txt new file mode 100644 index 0000000..244baab --- /dev/null +++ b/sample.txt @@ -0,0 +1,6 @@ + idname price quantitycreated_at delivery_date + 1Widget 9.99 102026-05-08T14:30:00+02:00 15/05/2026 + 2Gadget 19.95 52026-04-12T09:00:00+02:00 20/05/2026 + 3Sprocket 0.50 2502026-03-22T08:15:00+01:00 10/06/2026 + 4Hammer 12.00 422026-02-01T17:45:00+01:00 05/04/2026 + 5Long Item Name Here 100.49 12026-01-15T11:00:00+01:00 28/02/2026 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..9e7d63c --- /dev/null +++ b/uv.lock @@ -0,0 +1,132 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" +resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform == 'emscripten'", + "sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + +[[package]] +name = "joppe" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "openpyxl" }, + { name = "pandas" }, +] + +[package.metadata] +requires-dist = [ + { name = "openpyxl", specifier = ">=3.1.5" }, + { name = "pandas", specifier = ">=3.0.2" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, + { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, + { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +]