typedframes 0.4.1__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {typedframes-0.4.1 → typedframes-0.5.0}/PKG-INFO +44 -241
  2. {typedframes-0.4.1 → typedframes-0.5.0}/README.md +43 -240
  3. {typedframes-0.4.1 → typedframes-0.5.0}/pyproject.toml +20 -1
  4. {typedframes-0.4.1 → typedframes-0.5.0}/rust/Cargo.lock +1 -1
  5. {typedframes-0.4.1 → typedframes-0.5.0}/rust/Cargo.toml +1 -1
  6. {typedframes-0.4.1 → typedframes-0.5.0}/rust/src/ast_extract.rs +70 -0
  7. {typedframes-0.4.1 → typedframes-0.5.0}/rust/src/constants.rs +11 -0
  8. {typedframes-0.4.1 → typedframes-0.5.0}/rust/src/errors.rs +1 -1
  9. {typedframes-0.4.1 → typedframes-0.5.0}/rust/src/index.rs +60 -10
  10. {typedframes-0.4.1 → typedframes-0.5.0}/rust/src/linter.rs +720 -5
  11. {typedframes-0.4.1 → typedframes-0.5.0}/src/typedframes/__init__.py +1 -1
  12. typedframes-0.5.0/src/typedframes/_rust_checker.pyi +12 -0
  13. {typedframes-0.4.1 → typedframes-0.5.0}/src/typedframes/cli.py +49 -44
  14. {typedframes-0.4.1 → typedframes-0.5.0}/src/typedframes/mypy.py +2 -2
  15. {typedframes-0.4.1 → typedframes-0.5.0}/src/typedframes/pandas.py +7 -7
  16. {typedframes-0.4.1 → typedframes-0.5.0}/src/typedframes/polars.py +4 -4
  17. {typedframes-0.4.1 → typedframes-0.5.0}/rust/.cargo/config.toml +0 -0
  18. {typedframes-0.4.1 → typedframes-0.5.0}/rust/README.md +0 -0
  19. {typedframes-0.4.1 → typedframes-0.5.0}/rust/benches/parser_bench.rs +0 -0
  20. {typedframes-0.4.1 → typedframes-0.5.0}/rust/src/config.rs +0 -0
  21. {typedframes-0.4.1 → typedframes-0.5.0}/rust/src/contract.rs +0 -0
  22. {typedframes-0.4.1 → typedframes-0.5.0}/rust/src/lib.rs +0 -0
  23. {typedframes-0.4.1 → typedframes-0.5.0}/rust/src/main.rs +0 -0
  24. {typedframes-0.4.1 → typedframes-0.5.0}/rust/src/notebook.rs +0 -0
  25. {typedframes-0.4.1 → typedframes-0.5.0}/rust/src/pyapi.rs +0 -0
  26. {typedframes-0.4.1 → typedframes-0.5.0}/rust/src/sql.rs +0 -0
  27. {typedframes-0.4.1 → typedframes-0.5.0}/rust/src/typo.rs +0 -0
  28. {typedframes-0.4.1 → typedframes-0.5.0}/rust/tests/integration_test.rs +0 -0
  29. {typedframes-0.4.1 → typedframes-0.5.0}/src/typedframes/base_schema.py +0 -0
  30. {typedframes-0.4.1 → typedframes-0.5.0}/src/typedframes/column.py +0 -0
  31. {typedframes-0.4.1 → typedframes-0.5.0}/src/typedframes/column_group.py +0 -0
  32. {typedframes-0.4.1 → typedframes-0.5.0}/src/typedframes/column_group_error.py +0 -0
  33. {typedframes-0.4.1 → typedframes-0.5.0}/src/typedframes/column_set.py +0 -0
  34. {typedframes-0.4.1 → typedframes-0.5.0}/src/typedframes/missing_dependency_error.py +0 -0
  35. {typedframes-0.4.1 → typedframes-0.5.0}/src/typedframes/pandera.py +0 -0
  36. {typedframes-0.4.1 → typedframes-0.5.0}/src/typedframes/py.typed +0 -0
  37. {typedframes-0.4.1 → typedframes-0.5.0}/src/typedframes/schema_algebra.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: typedframes
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -42,7 +42,7 @@ Project-URL: Repository, https://github.com/w-martin/typedframes
42
42
 
43
43
  > ⚠️ **Project Status: Proof of Concept**
44
44
  >
45
- > `typedframes` (v0.4.1) is currently an experimental proof-of-concept. The core static analysis and mypy/Rust
45
+ > `typedframes` (v0.5.0) is currently an experimental proof-of-concept. The core static analysis and mypy/Rust
46
46
  > integrations work, but expect rough edges. The codebase prioritizes demonstrating the viability of static DataFrame
47
47
  > column checking over production-grade stability.
48
48
  >
@@ -59,13 +59,13 @@ print(orders["revenue"]) # ✗ unknown-column — 'revenue' not in inferred col
59
59
  ```
60
60
 
61
61
  ```shell
62
- typedframes check src/ --fail-under=90
62
+ typedframes check src/ --coverage-fail-under=90
63
63
  # src/pipeline.py:7:8: error[unknown-column] Column 'revenue' does not exist in inferred column set (defined at line 6)
64
64
  # ✗ Found 1 error in 12 files (0.0s)
65
65
  # ✗ DataFrame schema coverage 82.0% is below the required 90.0% (9/11 DataFrames had column info)
66
66
  ```
67
67
 
68
- `--fail-under=N` is the same idea as a test-coverage or mypy type-coverage gate, applied to how many DataFrames
68
+ `--coverage-fail-under=N` is the same idea as a test-coverage or mypy type-coverage gate, applied to how many DataFrames
69
69
  the checker can resolve columns for — see [DataFrame Schema Coverage Thresholds](#dataframe-schema-coverage-thresholds-opt-in).
70
70
  Add `BaseSchema` classes later for cross-file awareness and IDE autocomplete — see [Quick Start](#quick-start).
71
71
 
@@ -104,7 +104,7 @@ tracking and IDE autocomplete; they're a progressive enhancement, not a prerequi
104
104
  **What you get:**
105
105
 
106
106
  - ✅ **Works without schema annotations** - Column inference from `usecols=`, `dtype=`, and method chains catches errors on unannotated code
107
- - ✅ **CI gate via coverage threshold** - `--fail-under=N` fails the build when too much of your codebase is invisible to the checker, the same way you'd gate on test coverage or a type checker's type-coverage number
107
+ - ✅ **CI gate via coverage threshold** - `--coverage-fail-under=N` fails the build when too much of your codebase is invisible to the checker, the same way you'd gate on test coverage or a type checker's type-coverage number
108
108
  - ✅ **Rust-fast** - Milliseconds, not seconds, even on hundreds of files; fast enough for pre-commit hooks and CI (see [benchmarks](#static-analysis-performance))
109
109
  - ✅ **Cross-file awareness** - Add `BaseSchema` and typed return annotations to follow schemas across module boundaries
110
110
  - ✅ **Refactor-safe access** - `df[Schema.column_group.s].mean()` (pandas) or `df.select(Schema.col.col)` (polars) instead of scattered string literals
@@ -125,7 +125,13 @@ or
125
125
  uv add typedframes
126
126
  ```
127
127
 
128
- The Rust-based checker is included — no separate install needed.
128
+ The Rust-based checker is included — no separate install needed. `typedframes` itself has
129
+ no required dependencies; add the extra for whichever backend(s) your code uses:
130
+
131
+ ```shell
132
+ pip install typedframes[pandas] # includes pandas
133
+ pip install typedframes[polars] # includes polars
134
+ ```
129
135
 
130
136
  ---
131
137
 
@@ -429,10 +435,10 @@ typedframes check src/ --no-index
429
435
  typedframes check src/ --no-warnings
430
436
 
431
437
  # Enforce minimum DataFrame schema coverage (see below)
432
- typedframes check src/ --fail-under=90
438
+ typedframes check src/ --coverage-fail-under=90
433
439
 
434
440
  # Show which DataFrames lack column info, per file
435
- typedframes check src/ --coverage-report=term-missing
441
+ typedframes check src/ --coverage-detail=term-missing
436
442
  ```
437
443
 
438
444
  To suppress warnings project-wide, add to `pyproject.toml`:
@@ -488,103 +494,27 @@ code reference.
488
494
  resolve column information for — the analogue of the "type coverage" reported by mypy,
489
495
  pyright, and pyre, and unrelated to test coverage. That number is informational by
490
496
  default. If you want it enforced — failing the run when too much of your code is
491
- invisible to the checker — enable a threshold.
492
-
493
- This is **entirely opt-in**. With no `[tool.typedframes.coverage]` table and no
494
- `--fail-under`, nothing changes: no threshold, no exit-code difference.
495
-
496
- Every supported key, at its default value:
497
+ invisible to the checker — enable a threshold:
497
498
 
498
499
  ```toml
499
500
  [tool.typedframes.coverage]
500
- # Master switch. Coverage enforcement is off unless this is true, so adding this
501
- # table without setting it changes nothing.
502
- enabled = false
503
-
504
- # Minimum percentage of DataFrames that must have recognized column/schema info
505
- # before `typedframes check` exits 1. Only consulted when `enabled = true`.
506
- # Applies to every file not captured by a glob in [overrides] below.
507
- fail_under = 100.0
508
-
509
- # How much coverage detail to print after each check. One of:
510
- # "summary" one line (the default, unchanged from before this feature)
511
- # "term-missing" per-file table plus the DataFrame sites lacking column info
512
- # "json" machine-readable document, for CI tooling
513
- # Independent of `enabled` — a detailed report is useful without a gate, and
514
- # vice versa. Overridden by `--coverage-report`.
515
- report = "summary"
516
-
517
- [tool.typedframes.coverage.overrides]
518
- # Per-path glob overrides of `fail_under`, for holding legacy code to a lower bar
519
- # than new code. Each glob is graded on its own files as a separate group, so a
520
- # lenient legacy bucket can't drag down (or rescue) the rest of the project.
521
- # Paths are matched project-relative: `**` spans any number of directories,
522
- # `*` and `?` stay within one path segment.
523
- # When several globs match one file the most specific wins — longest literal
524
- # prefix before the first `*` or `?`. Files matching no glob use `fail_under`.
525
- # "legacy/**" = 50.0
526
- # "src/new_module/**" = 100.0
527
- ```
528
-
529
- Prefer to keep `pyproject.toml` clean? The same settings work in a standalone
530
- `typedframes.toml` at the project root, with the `[tool.typedframes]` prefix dropped
531
- (the way `ruff.toml` drops `[tool.ruff]`):
532
-
533
- ```toml
534
- # typedframes.toml
535
- [coverage]
536
- enabled = false
537
- fail_under = 100.0
538
- report = "summary"
539
-
540
- [coverage.overrides]
541
- # "legacy/**" = 50.0
542
- ```
543
-
544
- If both files exist, `typedframes.toml` wins **entirely** — the two are never merged,
545
- so exactly one file explains the whole configuration.
546
-
547
- ### Seeing What's Missing
548
-
549
- The default one-line DataFrame schema coverage summary tells you the ratio but not what
550
- to fix. `--coverage-report=term-missing` names the DataFrames that cost you coverage:
551
-
552
- ```shell
553
- typedframes check src/ --coverage-report=term-missing
554
- ```
555
-
556
- ```
557
- Name Typed Total Cover Missing
558
- ---------------------------------------------
559
- legacy/old.py 0 2 0% old_one:2, old_two:3
560
- src/new.py 1 2 50% bad:3
561
- ---------------------------------------------
562
- TOTAL 1 4 25%
501
+ enabled = true
502
+ fail_under = 90.0
563
503
  ```
564
504
 
565
- Each `Missing` entry is `variable:line` — the assignment where the checker recognized a
566
- DataFrame but couldn't resolve its columns. Fix those (add `usecols=`, name the columns
567
- in the `SELECT`, or annotate the variable) and coverage rises.
568
-
569
- For CI tooling, `--coverage-report=json` emits the same data as a document. Combine it
570
- with `--output-format=json` and the coverage report is nested under a `coverage` key so
571
- stdout stays a single valid JSON document:
505
+ or as a one-off, without touching config:
572
506
 
573
507
  ```shell
574
- typedframes check src/ --output-format=json --coverage-report=json
508
+ typedframes check src/ --coverage-fail-under=90
575
509
  ```
576
510
 
577
- Notes:
578
-
579
- - Coverage is a **separate gate from `--strict`**. `--strict` fails on errors
580
- (correctness); a threshold fails on missing column information (completeness).
581
- Enabling one never implies the other.
582
- - `--fail-under=N` is a **total override**: it applies one threshold everywhere and
583
- ignores the config table, per-path overrides included. Handy for a one-off CI run.
584
- - A group with no recognized DataFrames passes: 0/0 means there was nothing to
585
- measure, not that something failed.
586
- - A failed threshold exits **1** and is reported even under `--no-info` — that flag
587
- silences the informational summary line, not a gate result.
511
+ This is **entirely opt-in** — with no `[tool.typedframes.coverage]` table and no
512
+ `--coverage-fail-under`, nothing changes: no threshold, no exit-code difference. Per-path
513
+ overrides (e.g. a lower bar for `legacy/**`), a `--coverage-detail=term-missing`
514
+ breakdown of exactly which DataFrames cost you coverage (as text or, combined with
515
+ `--output-format=json`, as structured JSON), and the full config reference all live in
516
+ the
517
+ [DataFrame schema coverage thresholds guide](docs/usage.md#dataframe-schema-coverage-thresholds).
588
518
 
589
519
  ## Static Analysis Performance
590
520
 
@@ -824,6 +754,7 @@ Comprehensive comparison of pandas/DataFrame typing and validation tools. **type
824
754
  | Column name checking | ✅ Yes | ⚠️ Limited | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No |
825
755
  | Column type checking | ✅ Yes | ⚠️ Limited | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No |
826
756
  | Typo suggestions | ✅ Yes | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No |
757
+ | Coverage gate (`--coverage-fail-under`) | ✅ Yes | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No |
827
758
  | **Runtime Validation** |
828
759
  | Data validation | ❌ No | ✅ Excellent | ✅ Excellent | ✅ typeguard | ❌ No | ✅ Yes | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes | ✅ Yes |
829
760
  | Value constraints | ❌ No | ✅ Yes | ✅ Excellent | ❌ No | ❌ No | ❌ No | ❌ No | ✅ Yes | ❌ No | ✅ Yes | ✅ Yes |
@@ -967,122 +898,23 @@ The conversion maps:
967
898
 
968
899
  ## Examples
969
900
 
970
- ### Basic CSV Processing
971
-
972
- ```python
973
- from typing import Annotated
974
- import pandas as pd
975
- from typedframes import BaseSchema, Column
976
-
977
-
978
- class Orders(BaseSchema):
979
- order_id = Column(type=int)
980
- customer_id = Column(type=int)
981
- total = Column(type=float)
982
- date = Column(type=str)
983
-
984
-
985
- def calculate_revenue(orders: Annotated[pd.DataFrame, Orders]) -> float:
986
- return orders["total"].sum()
987
-
988
-
989
- df: Annotated[pd.DataFrame, Orders] = pd.read_csv("orders.csv")
990
- revenue = calculate_revenue(df)
991
- ```
992
-
993
- ### Time Series Analysis
994
-
995
- ```python
996
- from typing import Annotated
997
- import pandas as pd
998
- from typedframes import BaseSchema, Column, ColumnSet, ColumnGroup
999
-
1000
-
1001
- class SensorData(BaseSchema):
1002
- timestamp = Column(type=str)
1003
- temperature = ColumnSet(type=float, members=["temp_1", "temp_2", "temp_3"])
1004
- humidity = ColumnSet(type=float, members=["humidity_1", "humidity_2"])
1005
-
1006
- all_sensors = ColumnGroup(members=[temperature, humidity])
1007
-
1008
-
1009
- df: Annotated[pd.DataFrame, SensorData] = pd.read_csv("sensors.csv")
1010
-
1011
- # Clean, type-safe operations using .s for column name lists
1012
- avg_temp_per_row = df[SensorData.temperature.s].mean(axis=1)
1013
- all_readings_stats = df[SensorData.all_sensors.s].describe()
1014
- ```
1015
-
1016
- ### Multi-Step Pipeline
1017
-
1018
- ```python
1019
- from typing import Annotated
1020
- import pandas as pd
1021
- from typedframes import BaseSchema, Column
1022
-
1023
-
1024
- class RawSales(BaseSchema):
1025
- date = Column(type=str)
1026
- product_id = Column(type=int)
1027
- quantity = Column(type=int)
1028
- price = Column(type=float)
1029
-
1030
-
1031
- class AggregatedSales(BaseSchema):
1032
- date = Column(type=str)
1033
- total_revenue = Column(type=float)
1034
- total_quantity = Column(type=int)
1035
-
1036
-
1037
- def aggregate_daily(df: Annotated[pd.DataFrame, RawSales]) -> Annotated[pd.DataFrame, AggregatedSales]:
1038
- result = (
1039
- df.groupby(RawSales.date.s)
1040
- .agg(
1041
- {
1042
- RawSales.price.s: "sum",
1043
- RawSales.quantity.s: "sum",
1044
- }
1045
- )
1046
- .reset_index()
1047
- )
1048
- result.columns = pd.Index(["date", "total_revenue", "total_quantity"])
1049
- return result # type: ignore[return-value]
1050
-
1051
-
1052
- # Type-safe pipeline
1053
- raw: Annotated[pd.DataFrame, RawSales] = pd.read_csv("sales.csv")
1054
- aggregated = aggregate_daily(raw)
1055
-
1056
-
1057
- # Type checker validates schema transformations
1058
- def analyze(df: Annotated[pd.DataFrame, AggregatedSales]) -> float:
1059
- df["total_revenue"] # ✓ OK
1060
- df["price"] # ✗ Error: 'price' not in AggregatedSales
1061
- return df[AggregatedSales.total_revenue.s].mean()
1062
- ```
1063
-
1064
- ### Polars Performance Pipeline
1065
-
1066
- ```python
1067
- from typing import Annotated
1068
- import polars as pl
1069
- from typedframes import BaseSchema, Column
1070
-
1071
-
1072
- class LargeDataset(BaseSchema):
1073
- id = Column(type=int)
1074
- value = Column(type=float)
1075
- category = Column(type=str)
1076
-
1077
-
1078
- def efficient_aggregation(df: Annotated[pl.DataFrame, LargeDataset]) -> pl.DataFrame:
1079
- return df.filter(pl.col("value") > 100).group_by("category").agg(pl.col("value").mean())
1080
-
1081
-
1082
- # Polars handles large files efficiently
1083
- df: Annotated[pl.DataFrame, LargeDataset] = pl.read_csv("huge_file.csv")
1084
- result = efficient_aggregation(df)
1085
- ```
901
+ Runnable versions of everything shown in [Quick Start](#quick-start) and
902
+ [Advanced Usage](#advanced-usage) live under [`examples/features/`](examples/features/):
903
+
904
+ - [`annotated_pandas_example.py`](examples/features/annotated_pandas_example.py) —
905
+ `Annotated[pd.DataFrame, Schema]` with string and `.s` descriptor column access
906
+ - [`annotated_polars_example.py`](examples/features/annotated_polars_example.py) —
907
+ the same, with `pl.col()` and `.col` descriptor expressions
908
+ - [`typedframes_example.py`](examples/features/typedframes_example.py) — pandas and
909
+ polars side by side against one shared schema
910
+ - [`schema_algebra_example.py`](examples/features/schema_algebra_example.py) —
911
+ composing schemas via inheritance and the `+` operator
912
+ - [`inference_example.py`](examples/features/inference_example.py) — all four
913
+ column-inference modes with annotated ✓/✗ comments, no `BaseSchema` at all
914
+ - [`multi_file_inference/`](examples/features/multi_file_inference/) and
915
+ [`multi_file_with_schema/`](examples/features/multi_file_with_schema/) — the same
916
+ cross-file pipeline checked with and without schemas, see
917
+ [`examples/features/README.md`](examples/features/README.md) for what each one catches
1086
918
 
1087
919
  ---
1088
920
 
@@ -1170,35 +1002,6 @@ MIT License - see [LICENSE](LICENSE)
1170
1002
 
1171
1003
  ---
1172
1004
 
1173
- ## Roadmap
1174
-
1175
- **Shipped:**
1176
- - [x] Schema definition API
1177
- - [x] Pandas support
1178
- - [x] Polars support
1179
- - [x] Mypy plugin
1180
- - [x] Standalone checker (Rust)
1181
- - [x] Explicit backend types
1182
- - [x] Merge/join schema preservation
1183
- - [x] Schema Composition (multiple inheritance, `SchemaA + SchemaB`)
1184
- - [x] Column name collision warnings
1185
- - [x] Pandera integration (`to_pandera_schema()`)
1186
- - [x] Cross-file schema inference (project-level index, `--no-index` flag)
1187
- - [x] Aggressive column inference (untracked-dataframe/dropped-unknown-column warnings, method chain propagation)
1188
- - [x] Function parameter contracts (`missing-column`), resolved transitively across chains of helper
1189
- functions and cross-file calls; schema-annotated parameters take priority over body-scanning
1190
- - [x] SQL / data-warehouse column inference (`SELECT` list parsing across Snowflake, BigQuery, Athena, Redshift,
1191
- Databricks, PySpark, DuckDB, SQLAlchemy Core/ORM, Feast, and T-SQL/Synapse/Fabric dialects)
1192
- - [x] Jupyter notebook (`.ipynb`) checking — code cells are checked directly, with errors reported as
1193
- `notebook.ipynb:cell N:line:col`
1194
-
1195
- **Planned:**
1196
-
1197
- - [ ] **Opt-in data loading constraints** - `Field` class with constraints (`gt`, `ge`, `lt`, `le`), strictly isolated
1198
- to `from_schema()` ingestion boundaries
1199
-
1200
- ---
1201
-
1202
1005
  ## FAQ
1203
1006
 
1204
1007
  **Q: Do I need to choose between pandas and polars?**