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:
+15
@@ -0,0 +1,15 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
.claude/
|
||||
.wolf/
|
||||
.python-version
|
||||
|
||||
CLAUDE.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 <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.
|
||||
@@ -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))
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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" },
|
||||
]
|
||||
Reference in New Issue
Block a user