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 <noreply@anthropic.com>
This commit is contained in:
2026-05-12 10:47:23 +02:00
commit 1639f05e6e
6 changed files with 1169 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
.claude/
.wolf/
.python-version
CLAUDE.md
+408
View File
@@ -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 <invoerbestand> <uitvoerbestand>
```
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"])) # <class 'int'>
print(type(records[0]["created_at"])) # <class 'datetime.datetime'>
```
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.
+598
View File
@@ -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 <invoerbestand> <uitvoerbestand>
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: <invoerbestand> <uitvoerbestand>")
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))
+10
View File
@@ -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",
]
+6
View File
@@ -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
Generated
+132
View File
@@ -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" },
]