focus-data-toolkit 0.11.0__py3-none-any.whl

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 (116) hide show
  1. focus_data_toolkit/__init__.py +69 -0
  2. focus_data_toolkit/__main__.py +6 -0
  3. focus_data_toolkit/_version.py +8 -0
  4. focus_data_toolkit/cli.py +968 -0
  5. focus_data_toolkit/context/__init__.py +88 -0
  6. focus_data_toolkit/context/billing.py +54 -0
  7. focus_data_toolkit/context/provider.py +90 -0
  8. focus_data_toolkit/convert/__init__.py +708 -0
  9. focus_data_toolkit/convert/billing_period.py +65 -0
  10. focus_data_toolkit/convert/contract_applied.py +235 -0
  11. focus_data_toolkit/convert/contract_commitment.py +182 -0
  12. focus_data_toolkit/convert/cost_and_usage.py +179 -0
  13. focus_data_toolkit/convert/detect.py +39 -0
  14. focus_data_toolkit/convert/invoice_detail.py +199 -0
  15. focus_data_toolkit/convert/streaming.py +1030 -0
  16. focus_data_toolkit/errors.py +145 -0
  17. focus_data_toolkit/focus_json.py +68 -0
  18. focus_data_toolkit/generators/__init__.py +61 -0
  19. focus_data_toolkit/generators/_shim.py +43 -0
  20. focus_data_toolkit/generators/engine/__init__.py +14 -0
  21. focus_data_toolkit/generators/engine/context.py +12 -0
  22. focus_data_toolkit/generators/engine/determinism.py +117 -0
  23. focus_data_toolkit/generators/engine/json_focus.py +63 -0
  24. focus_data_toolkit/generators/engine/ladder.py +71 -0
  25. focus_data_toolkit/generators/engine/scenarios_core.py +380 -0
  26. focus_data_toolkit/generators/engine/serialize.py +151 -0
  27. focus_data_toolkit/generators/generate_aws_focus_1_2.py +19 -0
  28. focus_data_toolkit/generators/generate_aws_focus_1_3.py +20 -0
  29. focus_data_toolkit/generators/generate_azure_focus_1_2.py +17 -0
  30. focus_data_toolkit/generators/generate_azure_focus_1_3.py +17 -0
  31. focus_data_toolkit/generators/generate_gcp_focus_1_2.py +17 -0
  32. focus_data_toolkit/generators/generate_gcp_focus_1_3.py +17 -0
  33. focus_data_toolkit/generators/providers/__init__.py +29 -0
  34. focus_data_toolkit/generators/providers/aws.py +186 -0
  35. focus_data_toolkit/generators/providers/azure.py +191 -0
  36. focus_data_toolkit/generators/providers/gcp.py +194 -0
  37. focus_data_toolkit/generators/providers/profile.py +123 -0
  38. focus_data_toolkit/generators/scenarios.py +178 -0
  39. focus_data_toolkit/generators/versions/__init__.py +17 -0
  40. focus_data_toolkit/generators/versions/adapter.py +41 -0
  41. focus_data_toolkit/generators/versions/v1_2.py +111 -0
  42. focus_data_toolkit/generators/versions/v1_3.py +154 -0
  43. focus_data_toolkit/io/__init__.py +1 -0
  44. focus_data_toolkit/io/atomic_writer.py +462 -0
  45. focus_data_toolkit/io/csv_io.py +128 -0
  46. focus_data_toolkit/io/parquet_io.py +528 -0
  47. focus_data_toolkit/io/records.py +92 -0
  48. focus_data_toolkit/io/row_source.py +117 -0
  49. focus_data_toolkit/lifecycle.py +342 -0
  50. focus_data_toolkit/manifest.py +114 -0
  51. focus_data_toolkit/model/__init__.py +43 -0
  52. focus_data_toolkit/model/capabilities.py +66 -0
  53. focus_data_toolkit/model/focus_1_4_decimal_scale.json +10 -0
  54. focus_data_toolkit/model/focus_1_4_model.json +1913 -0
  55. focus_data_toolkit/model/focus_1_4_servicesubcategory.json +84 -0
  56. focus_data_toolkit/model/focus_json_keys.py +112 -0
  57. focus_data_toolkit/model/iso_4217_currencies.json +23 -0
  58. focus_data_toolkit/model/json_schema_check.py +205 -0
  59. focus_data_toolkit/model/json_schemas/allocatedmethoddetailsobjectschema.json +82 -0
  60. focus_data_toolkit/model/json_schemas/commitmentprogrameligibilitydetailsobjectschema.json +41 -0
  61. focus_data_toolkit/model/json_schemas/contractappliedobjectschema.json +104 -0
  62. focus_data_toolkit/model/json_schemas/contractcommitmentapplicabilityobjectschema.json +290 -0
  63. focus_data_toolkit/model/json_schemas/json_schemas_provenance.json +38 -0
  64. focus_data_toolkit/model/model_provenance.json +58 -0
  65. focus_data_toolkit/model/validator.py +498 -0
  66. focus_data_toolkit/modes.py +18 -0
  67. focus_data_toolkit/official_validator.py +61 -0
  68. focus_data_toolkit/progress.py +89 -0
  69. focus_data_toolkit/provenance.py +106 -0
  70. focus_data_toolkit/py.typed +1 -0
  71. focus_data_toolkit/runtime.py +243 -0
  72. focus_data_toolkit/schema/__init__.py +17 -0
  73. focus_data_toolkit/schema/detection.py +274 -0
  74. focus_data_toolkit/schema/registry.py +127 -0
  75. focus_data_toolkit/storage/__init__.py +1 -0
  76. focus_data_toolkit/storage/external_index.py +99 -0
  77. focus_data_toolkit/storage/spill.py +150 -0
  78. focus_data_toolkit/studio/__init__.py +19 -0
  79. focus_data_toolkit/studio/app.py +467 -0
  80. focus_data_toolkit/studio/config.py +42 -0
  81. focus_data_toolkit/studio/frontend/app.js +214 -0
  82. focus_data_toolkit/studio/frontend/index.html +101 -0
  83. focus_data_toolkit/studio/frontend/style.css +60 -0
  84. focus_data_toolkit/studio/jobs.py +142 -0
  85. focus_data_toolkit/studio/preview.py +32 -0
  86. focus_data_toolkit/studio/security.py +125 -0
  87. focus_data_toolkit/studio/server.py +71 -0
  88. focus_data_toolkit/supplement/__init__.py +50 -0
  89. focus_data_toolkit/supplement/adapters/__init__.py +21 -0
  90. focus_data_toolkit/supplement/adapters/adapters_provenance.json +39 -0
  91. focus_data_toolkit/supplement/adapters/aws_invoice_summary.json +24 -0
  92. focus_data_toolkit/supplement/adapters/aws_savings_plans.json +31 -0
  93. focus_data_toolkit/supplement/adapters/azure_invoice.json +25 -0
  94. focus_data_toolkit/supplement/adapters/gcp_compute_commitments.json +28 -0
  95. focus_data_toolkit/supplement/adapters/registry.py +215 -0
  96. focus_data_toolkit/supplement/apply.py +318 -0
  97. focus_data_toolkit/supplement/gaps.py +219 -0
  98. focus_data_toolkit/supplement/kinds.py +118 -0
  99. focus_data_toolkit/supplement/loader.py +409 -0
  100. focus_data_toolkit/supplement/spec.py +74 -0
  101. focus_data_toolkit/supplement/validate.py +215 -0
  102. focus_data_toolkit/validate/__init__.py +15 -0
  103. focus_data_toolkit/validate/allocation.py +333 -0
  104. focus_data_toolkit/validate/bundle.py +254 -0
  105. focus_data_toolkit/validate/codes.py +93 -0
  106. focus_data_toolkit/validate/corrections.py +245 -0
  107. focus_data_toolkit/validate/reconciliation.py +98 -0
  108. focus_data_toolkit/validate/referential.py +289 -0
  109. focus_data_toolkit-0.11.0.dist-info/METADATA +519 -0
  110. focus_data_toolkit-0.11.0.dist-info/RECORD +116 -0
  111. focus_data_toolkit-0.11.0.dist-info/WHEEL +5 -0
  112. focus_data_toolkit-0.11.0.dist-info/entry_points.txt +2 -0
  113. focus_data_toolkit-0.11.0.dist-info/licenses/LICENSE +21 -0
  114. focus_data_toolkit-0.11.0.dist-info/licenses/LICENSES/CC-BY-4.0.txt +156 -0
  115. focus_data_toolkit-0.11.0.dist-info/licenses/NOTICE +60 -0
  116. focus_data_toolkit-0.11.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,519 @@
1
+ Metadata-Version: 2.4
2
+ Name: focus-data-toolkit
3
+ Version: 0.11.0
4
+ Summary: Generate provider-realistic FOCUS 1.2/1.3 sample data (AWS, Azure, GCP) and convert it to the four FOCUS 1.4 datasets — strictly from source facts, completed by client supplements including native AWS/Azure/GCP exports — with schema detection, gap analysis, a cross-dataset validation gate and atomic writes.
5
+ Author: Guy-Hermann Adiko
6
+ License-Expression: MIT AND CC-BY-4.0
7
+ Project-URL: Homepage, https://github.com/guymano/focus-data-toolkit
8
+ Project-URL: Source, https://github.com/guymano/focus-data-toolkit
9
+ Project-URL: Issues, https://github.com/guymano/focus-data-toolkit/issues
10
+ Project-URL: Changelog, https://github.com/guymano/focus-data-toolkit/blob/main/CHANGELOG.md
11
+ Project-URL: Documentation, https://github.com/guymano/focus-data-toolkit#readme
12
+ Keywords: finops,focus,billing,cost,sample-data
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Office/Business :: Financial
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ License-File: NOTICE
25
+ License-File: LICENSES/CC-BY-4.0.txt
26
+ Provides-Extra: parquet
27
+ Requires-Dist: pyarrow<26,>=15; extra == "parquet"
28
+ Requires-Dist: tzdata>=2024.1; sys_platform == "win32" and extra == "parquet"
29
+ Provides-Extra: scale
30
+ Requires-Dist: focus-data-toolkit[parquet]; extra == "scale"
31
+ Provides-Extra: validator
32
+ Requires-Dist: focus-validator<3,>=2.1; python_version >= "3.12" and extra == "validator"
33
+ Provides-Extra: studio
34
+ Requires-Dist: fastapi<1,>=0.110; extra == "studio"
35
+ Requires-Dist: uvicorn[standard]<1,>=0.29; extra == "studio"
36
+ Requires-Dist: python-multipart<1,>=0.0.9; extra == "studio"
37
+ Provides-Extra: studio-all
38
+ Requires-Dist: focus-data-toolkit[parquet,studio]; extra == "studio-all"
39
+ Provides-Extra: all
40
+ Requires-Dist: focus-data-toolkit[parquet,studio,validator]; extra == "all"
41
+ Provides-Extra: release
42
+ Requires-Dist: build<2,>=1; extra == "release"
43
+ Requires-Dist: twine<7,>=5; extra == "release"
44
+ Provides-Extra: dev
45
+ Requires-Dist: focus-data-toolkit[parquet,release,studio]; extra == "dev"
46
+ Requires-Dist: pytest>=8; extra == "dev"
47
+ Requires-Dist: pytest-cov<8,>=5; extra == "dev"
48
+ Requires-Dist: ruff>=0.6; extra == "dev"
49
+ Requires-Dist: mypy<2.4,>=1.11; extra == "dev"
50
+ Requires-Dist: jsonschema<5,>=4.21; extra == "dev"
51
+ Requires-Dist: httpx<1,>=0.27; extra == "dev"
52
+ Dynamic: license-file
53
+
54
+ # focus-data-toolkit
55
+
56
+ Generate provider-realistic **FOCUS 1.2 / 1.3** sample data (AWS, Azure, GCP),
57
+ **migrate** a FOCUS 1.2/1.3 Cost and Usage table to **FOCUS 1.4**, optionally
58
+ **synthesize** the new 1.4 datasets for demos/tests, and **lint** the results —
59
+ in one dependency-free Python toolkit.
60
+
61
+ [FOCUS](https://focus.finops.org) (FinOps Open Cost and Usage Specification) is
62
+ the open standard for cloud cost and usage data. FOCUS 1.4 (June 2026) defines
63
+ four datasets: **Cost and Usage** (65 columns), **Contract Commitment**
64
+ (30 columns), **Billing Period** (6 columns, new) and **Invoice Detail**
65
+ (22 columns, new).
66
+
67
+ ## What this toolkit does — and does not — claim
68
+
69
+ The toolkit is explicit about three very different operations:
70
+
71
+ - **Schema migration** — transforming columns/formats where the mapping is exact
72
+ or explicitly documented. *Cost and Usage* migrates 1.2/1.3 → 1.4 essentially
73
+ losslessly (1.4 only adds nullable columns and drops two deprecated ones).
74
+ - **Enrichment** — adding data from a complementary *authoritative* source
75
+ (e.g. a real invoice). Not yet ingested; the architecture reserves a place for it.
76
+ - **Synthetic projection** — inventing plausible values for demos/tests/learning.
77
+ These results are **never** presented as real financial facts or as fully
78
+ FOCUS-conformant.
79
+
80
+ The three new-in-1.4 datasets (**Billing Period**, **Invoice Detail**, and the
81
+ 1.4-expanded **Contract Commitment**) contain **Mandatory columns that are
82
+ provider billing-system facts** — invoice status, payment terms, issuer-assigned
83
+ ids, provider record timestamps, commitment commercial terms. These are **not
84
+ present in, and not derivable from, a Cost and Usage table**. FOCUS 1.4 itself
85
+ treats them as provider-emitted (it adds an *Invoice Reconciliation* feature and
86
+ a *Rounding Variance Tolerance* appendix precisely because the issued invoice
87
+ legitimately differs from summed usage). So an aggregation of `BilledCost` is a
88
+ useful analytical summary — **not** a real invoice line.
89
+
90
+ A structurally valid file is **not** automatically FOCUS-conformant.
91
+
92
+ ## Modes
93
+
94
+ | Mode | Behaviour |
95
+ |---|---|
96
+ | **`strict`** (default) | Never invents provider facts. A canonical FOCUS 1.4 dataset is produced only when every Mandatory non-nullable column has a factual lineage (observed / renamed / derived / enriched). From a Cost-and-Usage source that means **only Cost and Usage** is produced; the other three are reported `NOT_PRODUCED` in the manifest, with the exact blocking columns. |
97
+ | **`synthetic`** | Generates assumed values so you get all four datasets for demos/tests. Affected datasets are written with a `synthetic_` filename prefix and marked `PRODUCED_SYNTHETIC` in the manifest; the result is never labelled fully conformant. |
98
+
99
+ Every conversion writes a deterministic **manifest** (`focus_1_4_manifest.json`)
100
+ recording, per column, how each value was obtained
101
+ (`OBSERVED` / `RENAMED` / `DERIVED` / `ENRICHED` / `ASSUMED` / `UNAVAILABLE`).
102
+
103
+ The core package is **standard-library only** (Python ≥ 3.11).
104
+
105
+ ## Installation
106
+
107
+ ### Prerequisites
108
+
109
+ - **Python 3.11, 3.12 or 3.13** (the `validator` extra needs **3.12+**). Linux, macOS and
110
+ Windows are supported — CI tests Ubuntu and Windows across all three versions (see
111
+ [docs/compatibility.md](docs/compatibility.md)).
112
+ - `pip` (or [uv](https://docs.astral.sh/uv/)). **No compiler is needed**: the core package is
113
+ pure Python and standard-library only — zero runtime dependencies.
114
+
115
+ ### Standard install
116
+
117
+ In a virtual environment (recommended):
118
+
119
+ ```bash
120
+ python -m venv .venv
121
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
122
+ pip install focus-data-toolkit
123
+ ```
124
+
125
+ > **Until the first PyPI release**, install straight from GitHub instead:
126
+ > `pip install "git+https://github.com/guymano/focus-data-toolkit"`
127
+ > (append `@vX.Y.Z` to pin a tag once releases exist).
128
+
129
+ This installs the `focus-toolkit` command (alias of `python -m focus_data_toolkit`).
130
+
131
+ ### Optional extras
132
+
133
+ The core covers generation, 1.2/1.3 → 1.4 conversion, client supplements + the AWS/Azure/GCP
134
+ provider-export adapters, bounded-memory CSV streaming, and every validation gate — all with
135
+ the standard library alone (the streaming state uses built-in `sqlite3`). Extras add:
136
+
137
+ | Command | Adds | Requires |
138
+ |---|---|---|
139
+ | `pip install "focus-data-toolkit[parquet]"` | Parquet **input and output** (PyArrow; value-exact decimal128, Hive partitioning). On Windows, `tzdata` comes along automatically. | Python ≥ 3.11 |
140
+ | `pip install "focus-data-toolkit[validator]"` | the official FinOps `focus-validator`, used by `focus-toolkit validate --official` (rule models for FOCUS 1.2/1.3) | Python ≥ 3.12 |
141
+ | `pip install "focus-data-toolkit[all]"` | both of the above (on 3.11 the validator part is skipped automatically by its Python marker) | Python ≥ 3.11 |
142
+
143
+ ### With uv
144
+
145
+ ```bash
146
+ uv tool install focus-data-toolkit # standalone CLI on your PATH
147
+ uv add "focus-data-toolkit[parquet]" # as a project dependency
148
+ uvx --from focus-data-toolkit focus-toolkit --help # one-off run, nothing installed
149
+ # (--from is needed because the command name differs from the package name)
150
+ ```
151
+
152
+ ### From source
153
+
154
+ ```bash
155
+ git clone https://github.com/guymano/focus-data-toolkit
156
+ cd focus-data-toolkit
157
+ pip install . # or: pip install ".[all]"
158
+ ```
159
+
160
+ For a development install (editable, with the test/lint toolchain), see
161
+ [Development](#development). Release artifacts (wheel, sdist) attached to GitHub Releases
162
+ ship with `SHA256SUMS`, two SBOM profiles and GitHub Artifact Attestations — verify a
163
+ downloaded wheel and install it directly:
164
+
165
+ ```bash
166
+ # SHA256SUMS covers every release asset; check just the wheel's line:
167
+ grep "focus_data_toolkit-X.Y.Z-py3-none-any.whl" SHA256SUMS | sha256sum -c -
168
+ gh attestation verify focus_data_toolkit-X.Y.Z-py3-none-any.whl --repo guymano/focus-data-toolkit
169
+ pip install ./focus_data_toolkit-X.Y.Z-py3-none-any.whl
170
+ ```
171
+
172
+ (To verify the whole asset set at once, download all files listed in `SHA256SUMS` into one
173
+ directory and run `sha256sum -c SHA256SUMS` there.)
174
+
175
+ ### Verify the installation
176
+
177
+ ```bash
178
+ focus-toolkit --help # CLI is on the PATH
179
+ # venv/project installs only — a `uv tool install` lives in its own isolated
180
+ # environment, so check it via the tool env instead:
181
+ python -c "import focus_data_toolkit as f; print(f.__version__)"
182
+ # uv tool run --from focus-data-toolkit python -c "import focus_data_toolkit as f; print(f.__version__)"
183
+ # end-to-end smoke test (deterministic, ~1 s):
184
+ focus-toolkit generate --provider aws --focus-version 1.3 --rows 10 --out /tmp/fdt-smoke
185
+ focus-toolkit convert --cost-and-usage /tmp/fdt-smoke/focus_1_3_cost_and_usage_aws.csv \
186
+ --out /tmp/fdt-smoke/focus-1.4
187
+ # exit code 3 is expected: the strict result is intentionally incomplete without supplements
188
+ ```
189
+
190
+ ### Upgrade / uninstall
191
+
192
+ ```bash
193
+ pip install -U focus-data-toolkit
194
+ pip uninstall focus-data-toolkit
195
+ ```
196
+
197
+ ## Quickstart
198
+
199
+ Install first (see [Installation](#installation) above), then:
200
+
201
+ ### 1. Generate FOCUS 1.2/1.3 sample data
202
+
203
+ ```bash
204
+ focus-toolkit generate --provider aws --focus-version 1.3 --rows 1000 --seed 1302 --out ./out
205
+ # -> out/focus_1_3_cost_and_usage_aws.csv (65 columns)
206
+ # -> out/focus_1_3_contract_commitment_aws.csv (13 columns)
207
+ ```
208
+
209
+ Providers: `aws`, `azure`, `gcp`. Versions: `1.2`, `1.3`. Same rows+seed →
210
+ byte-identical output.
211
+
212
+ ### 2. Convert towards FOCUS 1.4
213
+
214
+ **Strict (default)** — migrate what is genuinely migratable:
215
+
216
+ ```bash
217
+ focus-toolkit convert \
218
+ --cost-and-usage out/focus_1_3_cost_and_usage_aws.csv \
219
+ --out ./focus-1.4
220
+ # -> focus-1.4/focus_1_4_cost_and_usage.csv (65 columns)
221
+ # -> focus-1.4/focus_1_4_manifest.json (why the other 3 were NOT produced)
222
+ # exit code 3: strict result is intentionally incomplete
223
+ ```
224
+
225
+ **Synthetic** — also generate the provider-emitted datasets for demos/tests:
226
+
227
+ ```bash
228
+ focus-toolkit convert \
229
+ --cost-and-usage out/focus_1_3_cost_and_usage_aws.csv \
230
+ --contract-commitment out/focus_1_3_contract_commitment_aws.csv \
231
+ --out ./focus-1.4 --mode synthetic
232
+ # -> synthetic_focus_1_4_cost_and_usage.csv (migration + assumed InvoiceDetailId back-link)
233
+ # -> synthetic_focus_1_4_contract_commitment.csv (assumed terms)
234
+ # -> synthetic_focus_1_4_billing_period.csv (assumed status/timestamps)
235
+ # -> synthetic_focus_1_4_invoice_detail.csv (assumed invoice facts)
236
+ # -> focus_1_4_manifest.json
237
+ # exit code 4: synthetic result contains ASSUMED values
238
+ ```
239
+
240
+ In synthetic mode Cost and Usage is also `synthetic_`-prefixed, because its
241
+ `InvoiceDetailId` back-links to the (synthetic) Invoice Detail — every other
242
+ column is a faithful migration, as the manifest's per-column lineage records. In
243
+ strict mode Cost and Usage is emitted unprefixed with its `InvoiceDetailId` left null.
244
+
245
+ Works the same on your **own** FOCUS 1.2/1.3 exports — the source version is
246
+ detected from the CSV header. CLI exit codes: `0` success without assumptions ·
247
+ `1` lint violation · `2` invalid arguments · `3` incomplete strict result ·
248
+ `4` synthetic result produced with assumptions.
249
+
250
+ ### 3. Lint
251
+
252
+ ```bash
253
+ # built-in FOCUS 1.4 structural + semantic linter (not a full conformance validator)
254
+ focus-toolkit validate focus-1.4/focus_1_4_cost_and_usage.csv --dataset cost-and-usage
255
+
256
+ # official FinOps validator (optional extra, requires Python >= 3.12; supports 1.2/1.3)
257
+ pip install "focus-data-toolkit[validator]"
258
+ focus-toolkit validate --official --focus-version 1.2.0.1 my_focus_1_2_export.csv
259
+ ```
260
+
261
+ The official FinOps validator ships rule models for FOCUS 1.2/1.3 and does **not**
262
+ yet support 1.4. Until it does, the built-in linter provides a structural +
263
+ semantic check only — it cannot certify full FOCUS 1.4 conformance.
264
+
265
+ ### Working with real client data (0.3.0 / P1)
266
+
267
+ For consolidated, multi-provider, multi-issuer, multi-currency exports:
268
+
269
+ - **Schema detection** identifies the dataset *and* version (1.2/1.3/1.4) with a confidence,
270
+ and reports missing / extension (`x_`) / unknown columns. Strict mode refuses an ambiguous
271
+ source; force it with `--source-version` / `--source-dataset`.
272
+ - **Grouping keys** use the full billing grain (issuer, invoice, account, currency, period,
273
+ charge category), so lines from different issuers/accounts/currencies are never merged.
274
+ Locally generated ids are `x_fdt_…`-prefixed and never presented as issuer-assigned.
275
+ - **Cross-dataset validation** (`validate_dataset_bundle`) checks referential integrity,
276
+ currency/period/issuer coherence, invoice reconciliation (with a rounding tolerance),
277
+ Split Cost Allocation, and correction/lifecycle integrity — separately from the
278
+ per-dataset linter. It runs as a **publication gate** on every conversion (eager and
279
+ streaming): an ERROR refuses to publish, the outcome is recorded in the manifest's
280
+ `bundle_validation` section, and `--no-validate` skips it (the skip is recorded too).
281
+ In the streaming path the checks re-read the staged files in bounded memory, spilling
282
+ per-key lookup state to a scratch SQLite database past a threshold.
283
+ - **Atomic writes**: output appears only after validation succeeds and checksums + manifest
284
+ are written; nothing partial is left on error. `--on-exists refuse|replace|version`.
285
+ A `replace` swap is **journaled**: if the process dies between its two renames, the next
286
+ publish to the same destination (or `focus-toolkit clean --out DIR`) reads the journal and
287
+ rolls the fully staged result forward — or the previous result back — so the destination
288
+ is never left missing. `clean` also removes orphan `.output.tmp-*` / `.trash-*` leftovers.
289
+
290
+ ```python
291
+ from focus_data_toolkit import convert_to_focus_1_4, detect_focus_schema, validate_bundle
292
+
293
+ detection = detect_focus_schema(headers) # dataset, version, confidence, ...
294
+ report = validate_bundle({"Cost and Usage": cu_rows, "Invoice Detail": invd_rows})
295
+ print(report.ok, report.counts()) # cross-dataset findings, JSON-serialisable
296
+ ```
297
+
298
+ ### Scale: streaming conversion and Parquet (0.3.0 / P1 Phase B)
299
+
300
+ For large client files, `convert_files` streams the Cost and Usage file **once** and stages
301
+ Invoice Detail aggregation / Billing Period dedup in a throwaway SQLite database, so peak
302
+ memory stays **flat regardless of row count** — a constant ~64 MB peak process RSS whether
303
+ converting 50k or 300k rows (6× the rows, ×1.05 the memory; `tools/benchmark_streaming.py`).
304
+ Its output is **byte-identical** to the in-memory path — both call the same pure
305
+ per-row/per-group functions and the same manifest assembler.
306
+
307
+ Sources may be **CSV (gzip ok) or Parquet** — the format is sniffed per file (magic bytes,
308
+ extension as fallback), so `--cost-and-usage export.parquet` works everywhere a CSV does
309
+ (`convert`, `gaps`, `supplements validate`), in both the eager and streaming paths.
310
+
311
+ ```bash
312
+ # bounded-memory streaming to CSV (recommended for large exports)
313
+ focus-toolkit convert --cost-and-usage huge_cost_and_usage.csv.gz --out ./focus-1.4 \
314
+ --mode synthetic --stream
315
+
316
+ # Parquet output with exact decimal128 (requires the [parquet] extra)
317
+ focus-toolkit convert --cost-and-usage huge_cost_and_usage.csv --out ./focus-1.4 \
318
+ --mode synthetic --output-format parquet
319
+ ```
320
+
321
+ Gzip input is auto-detected. Exactness contract: **CSV is byte-exact** (the literal), while
322
+ **Parquet is decimal-value-exact** — decimal columns are written as `decimal128(precision,
323
+ scale)` (never binary float; a value needing more scale than the column allows raises with its
324
+ line number instead of rounding silently), dates as UTC timestamps, JSON/strings verbatim.
325
+
326
+ **Partitioning & compression** (Parquet): `--partition-by` writes the Cost and Usage dataset as
327
+ a Hive-partitioned tree on low-cardinality String / Date-Time columns; `--compression`
328
+ (snappy default / zstd / gzip / none) and `--target-file-size` (roll to a new part file per
329
+ partition) tune the layout:
330
+
331
+ ```bash
332
+ focus-toolkit convert --cost-and-usage huge_cost_and_usage.csv --out ./focus-1.4 \
333
+ --mode synthetic --output-format parquet \
334
+ --partition-by BillingCurrency,BillingPeriodStart --compression zstd --target-file-size 128MB
335
+ # -> synthetic_focus_1_4_cost_and_usage/BillingCurrency=USD/BillingPeriodStart=.../part-0.parquet
336
+ ```
337
+
338
+ The partition columns are stored in the paths (standard Hive), so any `pyarrow.dataset` reader
339
+ reconstructs full rows; a too-high-cardinality key is warned about and, past a hard cap,
340
+ refused (nothing partial is published).
341
+
342
+ ```python
343
+ from focus_data_toolkit import convert_files
344
+
345
+ out = convert_files("huge_cost_and_usage.csv.gz", "./focus-1.4",
346
+ mode="synthetic", output_format="parquet",
347
+ partition_by=["BillingCurrency"]) # -> published Path
348
+ ```
349
+
350
+ `pip install "focus-data-toolkit[parquet]"` for Parquet; streaming CSV needs no extra
351
+ (the external state uses the standard-library `sqlite3`).
352
+
353
+ ### Synthetic scenarios (SCA, corrections, billing lifecycle)
354
+
355
+ Deterministic, provider-agnostic scenario builders produce self-consistent data for tests and
356
+ demos, and typed lifecycle checks validate the full snapshot chains — status transitions plus
357
+ chain structure (id/order uniqueness, `previous_instance_id` resolution, cycle detection,
358
+ `last_updated` monotonicity, closed-instance immutability; `FDT-LIFE-001..006`):
359
+
360
+ ```python
361
+ from focus_data_toolkit.generators.scenarios import (
362
+ split_allocation_group, correction_set, billing_lifecycle_instances,
363
+ )
364
+ from focus_data_toolkit import check_dataset_instances, validate_bundle
365
+
366
+ alloc = split_allocation_group("origin-1", "100.00", weights=[3, 2, 1]) # ratios sum to 1,
367
+ validate_bundle({"Cost and Usage": alloc}).ok # costs sum to origin
368
+
369
+ corr = correction_set("chg-1", "100.00", ["-30.00"]) # original + signed Correction line,
370
+ validate_bundle({"Cost and Usage": corr}).ok # running net recorded in x_NetCharge
371
+
372
+ check_dataset_instances(billing_lifecycle_instances()) # [] — chains + transitions all valid
373
+ ```
374
+
375
+ ### Python API
376
+
377
+ ```python
378
+ from focus_data_toolkit import convert_to_focus_1_4, lint_focus_1_4_structure
379
+ from focus_data_toolkit.convert import read_csv_rows
380
+ from focus_data_toolkit.modes import Mode
381
+
382
+ result = convert_to_focus_1_4(read_csv_rows("focus_1_3_cost_and_usage.csv")) # strict
383
+ print(result.coverage) # ('Cost and Usage',)
384
+ print(result.not_produced) # ('Contract Commitment', 'Billing Period', 'Invoice Detail')
385
+ print(result.manifest["datasets"]["Invoice Detail"]["blocking_columns"])
386
+
387
+ result = convert_to_focus_1_4(read_csv_rows("focus_1_3_cost_and_usage.csv"), mode=Mode.SYNTHETIC)
388
+ rows = result.datasets["Invoice Detail"]
389
+ print(lint_focus_1_4_structure("Invoice Detail", rows).levels_passed)
390
+ ```
391
+
392
+ ## Studio (local web UI)
393
+
394
+ For a no-CLI experience, launch the **Studio** — a local web app over the same engine:
395
+
396
+ ```bash
397
+ pip install "focus-data-toolkit[studio]" # or [studio-all] to also read/write Parquet
398
+ focus-toolkit ui # opens http://127.0.0.1:8765/?token=…
399
+ ```
400
+
401
+ - **Local & private:** binds `127.0.0.1` by default, requires a fresh per-start **token** on every
402
+ request, validates `Host`/`Origin` (anti DNS-rebinding / CSRF), and confines file access to an
403
+ allowlisted `--root`. No telemetry, no external upload — data never leaves the machine.
404
+ - **Workflow:** pick a file already under `--root` (best for large files), upload a small/medium
405
+ file (capped), or generate synthetic test data → **Detect** → **Convert** with live per-phase
406
+ progress and **Cancel** → preview a **sampled** page (the full file is never loaded) → download
407
+ datasets, the manifest, diagnostics (JSON/CSV), `SHA256SUMS`, and an HTML summary.
408
+ - **Same Core:** every operation drives the same SDK the CLI and Runner use, so the Studio's
409
+ manifests, diagnostics and checksums are identical to a CLI run.
410
+ - Exposing it on a network needs `--allow-remote` (the token is still required). Generation is
411
+ capped in the UI (use the CLI/Runner for very large synthetic sets). See
412
+ [docs/studio.md](docs/studio.md).
413
+
414
+ ## Runner (Docker / OCI)
415
+
416
+ For production, automation and large volumes, the toolkit ships as a container image whose
417
+ entrypoint **is** the `focus-toolkit` CLI — a container run is exactly a CLI run (same
418
+ manifests, diagnostics, checksums and exit codes; no FOCUS logic is duplicated). It is
419
+ **batch-only** (no HTTP server); status is the exit code, the logs, and the produced files.
420
+
421
+ ```bash
422
+ docker run --rm \
423
+ --read-only --tmpfs /tmp \
424
+ -v "$PWD/input:/input:ro" \
425
+ -v "$PWD/output:/output" \
426
+ -v fdt-work:/work \
427
+ ghcr.io/guymano/focus-data-toolkit:0.10.0 \
428
+ convert --cost-and-usage /input/focus.csv \
429
+ --stream --output-format parquet --out /output/result \
430
+ --exit-policy pipeline
431
+ ```
432
+
433
+ - **Non-root** (uid 65532) and **read-only-rootfs compatible** — only `/work` (scratch) and
434
+ `/output` are written. Because the atomic publish stages next to `--out`, **`/output` must be
435
+ writable** (mount a writable volume/dir; if you run as the image's non-root uid, ensure the
436
+ target is writable by it — or use a named volume, which Docker initialises writable for you).
437
+ - `/input` is meant to be mounted **read-only**. `FOCUS_TOOLKIT_WORK_DIR=/work` and `TMPDIR=/work`
438
+ are preset, so all scratch stays on the `/work` volume — point it at fast local storage for big
439
+ files, and size it (the SQLite aggregation + bundle spill scale with invoice/period/commitment
440
+ cardinalities, not with the Cost-and-Usage row count).
441
+ - **Signals**: `docker stop` sends SIGTERM to PID 1 (the CLI), which cancels cleanly — exit code
442
+ **130**, nothing partial published. Give it a grace period (`--stop-timeout`).
443
+ - **Exit codes** for orchestrators: use `--exit-policy pipeline` so a functional-but-incomplete
444
+ strict run (3) or a synthetic run (4) reports success (0); genuine failures (1/2/5/130) stay
445
+ non-zero. Disk budgets: `FOCUS_TOOLKIT_MAX_WORK_BYTES`, `FOCUS_TOOLKIT_MIN_WORK_FREE_BYTES`,
446
+ `FOCUS_TOOLKIT_MIN_OUTPUT_FREE_BYTES` (a shortfall fails fast with `FDT-IO-005/006`, exit 5).
447
+
448
+ **Single-node engine — pick the right method by scale:**
449
+
450
+ | Volume | Recommended method |
451
+ |---|---|
452
+ | Tests / ordinary files | CLI (or the forthcoming Studio) |
453
+ | Large files on one machine | Runner |
454
+ | Hundreds of GB | Runner with fast local `/work` + sized resources; Parquet + partitioning + zstd |
455
+ | Beyond a single node | Partition upstream / orchestrate multiple batches |
456
+
457
+ The image is not a distributed engine. **Immutable** tags — `<version>` (e.g. `0.10.0`) and
458
+ `sha-<full-commit>` — always identify the same bytes; the `<major>.<minor>` tag (e.g. `0.10`)
459
+ is a **rolling** convenience alias that advances with each patch release. Every release is
460
+ scanned (trivy) **before** its public tags are assigned, carries a CycloneDX SBOM, and is signed
461
+ (cosign) and attested (build provenance). See [docs/runner.md](docs/runner.md) for details.
462
+
463
+ ## What is really migratable
464
+
465
+ | 1.4 dataset | Real migration? | Notes |
466
+ |---|---|---|
467
+ | Cost and Usage | **Yes** | Column mapping (1.2 lifted to 1.3 shape; deprecated provider columns dropped; nullable 1.4 additions null; `PricingCurrency*` backfilled; `ContractApplied` re-cased 1.3→1.4). |
468
+ | Contract Commitment | Partial → **synthetic** | The 13 source columns migrate; the 14 Mandatory 1.4 commercial terms are provider facts → assumed (synthetic only). |
469
+ | Billing Period | **No** (synthetic) | Period keys derive from Cost and Usage; `BillingPeriodStatus` and the timestamps are provider billing-cycle state. |
470
+ | Invoice Detail | **No** (synthetic) | `BilledCost` aggregates usage, but invoice status/terms/issuer-assigned id/timestamps are provider-issued. |
471
+
472
+ Conversion is pure (no clock, no RNG): the same input always produces the same
473
+ output bytes, in both modes.
474
+
475
+ ## Development
476
+
477
+ ```bash
478
+ git clone https://github.com/guymano/focus-data-toolkit && cd focus-data-toolkit
479
+ pip install -e .[dev] # includes pyarrow, so the Parquet suite runs (not skipped)
480
+ pytest -q # generators, detection, migration/lint, modes, manifest, CLI,
481
+ # streaming, Parquet, split-allocation, corrections, lifecycle
482
+ pytest -m slow # large-scale bounded-memory test (excluded by default)
483
+ python tools/benchmark_streaming.py --rows 100000 500000 # throughput + peak RSS
484
+ ruff check src tests
485
+ ```
486
+
487
+ The FOCUS 1.4 model JSON (`src/focus_data_toolkit/model/focus_1_4_model.json`)
488
+ is the artifact of record, extracted from the FinOps Foundation "FOCUS 1.4
489
+ Data Model" workbook with `tools/extract_focus_1_4_model.py` (the workbook
490
+ itself is not redistributed here — download it from
491
+ [focus.finops.org](https://focus.finops.org)). Its machine-readable provenance
492
+ is recorded in `model_provenance.json` and can be checked with
493
+ `python scripts/verify_model_provenance.py`; see
494
+ [docs/model-provenance.md](docs/model-provenance.md).
495
+
496
+ ## Contributing, security & docs
497
+
498
+ - **Contributing:** [CONTRIBUTING.md](CONTRIBUTING.md)
499
+ - **Security policy / reporting:** [SECURITY.md](SECURITY.md) (private reporting;
500
+ a FOCUS conformance bug is a normal issue, not a security report)
501
+ - **Versioning & reproducibility:** [docs/versioning.md](docs/versioning.md)
502
+ - **Compatibility (Python / OS / FOCUS):** [docs/compatibility.md](docs/compatibility.md)
503
+ - **Security model:** [docs/security-model.md](docs/security-model.md)
504
+ - **Releasing:** [docs/releasing.md](docs/releasing.md)
505
+ - **Changelog:** [CHANGELOG.md](CHANGELOG.md)
506
+
507
+ ## License and credits
508
+
509
+ The toolkit code is **MIT** (see [LICENSE](LICENSE)). The embedded FOCUS 1.4 data
510
+ model is a derivative of the FinOps FOCUS specification / data-model workbook,
511
+ which is © the FinOps Foundation and licensed **CC-BY-4.0**; it is redistributed
512
+ here with attribution (see [NOTICE](NOTICE) and
513
+ [docs/model-provenance.md](docs/model-provenance.md)). "FOCUS" and "FinOps" are
514
+ trademarks of the FinOps Foundation; the FOCUS specification, data-model workbook
515
+ and official validator are © the FinOps Foundation — this project is an
516
+ independent community toolkit and is not endorsed by the FinOps Foundation.
517
+ Related sample datasets from the same generators were contributed to
518
+ [FOCUS-Sample-Data](https://github.com/FinOps-Open-Cost-and-Usage-Spec/FOCUS-Sample-Data)
519
+ (PRs #6 and #7).
@@ -0,0 +1,116 @@
1
+ focus_data_toolkit/__init__.py,sha256=tyd4C3tdJJM7mNU4oeGSc7BaxNwADHnfhtneocDhyxc,2538
2
+ focus_data_toolkit/__main__.py,sha256=BswQRTWsAPifuwP39nW8-evmiw2xKb1pB_uefWJp1-A,191
3
+ focus_data_toolkit/_version.py,sha256=LKhp8tqxoUCYHl3H3Mx2I9TfIZCx2mdjc-v61UYEnLk,323
4
+ focus_data_toolkit/cli.py,sha256=3PdZ8WUMuOfl3nqNzuyp6-4M19C_hDt3HLzTo_6MZU0,38677
5
+ focus_data_toolkit/errors.py,sha256=iiKdOu3YnHODNHQHLaDahUuUIwP22CF7XWXypw3aaz4,5161
6
+ focus_data_toolkit/focus_json.py,sha256=FBWGZzsVAZhjV3J7Q5sf0A5Ya7WKDfsDCe3aOMfrrjE,2806
7
+ focus_data_toolkit/lifecycle.py,sha256=jPwSyJM7vsGjDYAXxpsu90_O_8QeR8NV1t2uJokU3A8,14557
8
+ focus_data_toolkit/manifest.py,sha256=MNFrjmV7aZT5yPwDjvjC1Df8OVGMFPcVbG6Ve-YrvHs,4451
9
+ focus_data_toolkit/modes.py,sha256=fqIsS-Hy4eHSr8g3QeuOeV4Ur5Rhfb-7m-4OitLrJrI,686
10
+ focus_data_toolkit/official_validator.py,sha256=2-GqeucVazyzoe4pvLzNDl66uNIh0wbTBPLZJn27kIc,1987
11
+ focus_data_toolkit/progress.py,sha256=5cxjh0bwEEna4GKFP8rZingZ1k1qBtp6NrCtu_eyTQI,3372
12
+ focus_data_toolkit/provenance.py,sha256=C0wdde3uWPPBdyMlihwHU60IHoVI3b0ek_g8nd-oXf4,4122
13
+ focus_data_toolkit/py.typed,sha256=6s6t40Ie33L-SxfLsfkB0w0Y0WUMke9BmGXW2GdQdIQ,68
14
+ focus_data_toolkit/runtime.py,sha256=Dfse7ROzIJVW_jhl999ULx_f-QJtjHw9lBO3r_aSggs,9814
15
+ focus_data_toolkit/context/__init__.py,sha256=7Bo3atX4ABmPVc5oyY5-KywxZ8mbXi7KhIBgrz_1Hs8,3019
16
+ focus_data_toolkit/context/billing.py,sha256=HoCs4XtHbYp1lseO9oOyZ6V9Gib7p6Jhg_-xcz52FnM,1922
17
+ focus_data_toolkit/context/provider.py,sha256=mdH4cByNwNx40Qxlz_MUCVuE-9bJRzbryMaY812oy18,3834
18
+ focus_data_toolkit/convert/__init__.py,sha256=XePzx4VdbiMB_b-Wsb7b9_mxX4vAHLl-eBI7cchYQ88,30408
19
+ focus_data_toolkit/convert/billing_period.py,sha256=BoJfbTY06se6Vm6Gz7uLBYXlK_88DBpY207Oa2l3S0A,2892
20
+ focus_data_toolkit/convert/contract_applied.py,sha256=58vbVTYH4cSlZtWTT147-a4YHCnVMQvtW55XvTVNOuI,9787
21
+ focus_data_toolkit/convert/contract_commitment.py,sha256=SXMgB9flO-xLApeIj_D_WsFEZNtCwqz1ntFt04fQf4c,8601
22
+ focus_data_toolkit/convert/cost_and_usage.py,sha256=f3O1o8HmRLEeftyAOO_QKENKwPi1OAgpRk57UTd2B-E,7903
23
+ focus_data_toolkit/convert/detect.py,sha256=XoePBgzCGI7AaJi1uroovBoLXZ3huL5_k-CH5Z4mQes,1444
24
+ focus_data_toolkit/convert/invoice_detail.py,sha256=kmzcaeXKZmKfGAt956mO5wrAR9AQbuAwI3OOauNTFeg,8940
25
+ focus_data_toolkit/convert/streaming.py,sha256=KLw48NAQlIYvDBfG0P_uWdraPxXm8cjdqFioRdtOCzA,44030
26
+ focus_data_toolkit/generators/__init__.py,sha256=55H7mKoBdfkyRfjL0c3SCsvw46sZd3CsEBcPPJTvDTI,2591
27
+ focus_data_toolkit/generators/_shim.py,sha256=FdgvnvgnwhPDlbURd6WxOsyvIl0kImEXaSPJrgI15EU,2064
28
+ focus_data_toolkit/generators/generate_aws_focus_1_2.py,sha256=LpjkpGnvNGlV4-bhyHB4AGhbg6ac6SamrvJ5_oFHlW4,768
29
+ focus_data_toolkit/generators/generate_aws_focus_1_3.py,sha256=0_6_7oNLkxE7MPZzCsrOFp3cOYirzGkE7wS-WS4v2bA,845
30
+ focus_data_toolkit/generators/generate_azure_focus_1_2.py,sha256=OFYv6pZOI0qMPEWwhXK5Os1CERaDRAKActlXvtnW_yo,585
31
+ focus_data_toolkit/generators/generate_azure_focus_1_3.py,sha256=vCgnDgRm_anv6G5Po07ntH-EwAAfo5uoyQguOlow5ko,585
32
+ focus_data_toolkit/generators/generate_gcp_focus_1_2.py,sha256=EKIzHtiV283euQRF9pii5CnIUrhFhSCtV_nkZ3eUk28,574
33
+ focus_data_toolkit/generators/generate_gcp_focus_1_3.py,sha256=Q0vw8dSxqs_YHL5DvlWHq_Wifsu-FVQCI70-We2O56Y,574
34
+ focus_data_toolkit/generators/scenarios.py,sha256=HV7zGGb_qaXwD3ORtD25GWinqcABhMAt3AL6Xk0zxjA,7221
35
+ focus_data_toolkit/generators/engine/__init__.py,sha256=KMJ_dIXf1IThG8-ell-9A1_qCwh8bcQC-8Ok8Nta6rI,849
36
+ focus_data_toolkit/generators/engine/context.py,sha256=QSWR8AFmevrmzVmAO4xlrDD3TfjPntgBBYjYj4MLI3M,505
37
+ focus_data_toolkit/generators/engine/determinism.py,sha256=xeiqtOxqpipoS8RjphWbg8q5iF7jIPmh_JCltFQG9EA,4413
38
+ focus_data_toolkit/generators/engine/json_focus.py,sha256=o3j-GZr_V0XdR31eVhOxgdowNGRDoh9Xcld1Pj2YWKw,2567
39
+ focus_data_toolkit/generators/engine/ladder.py,sha256=bC2M2Hp-_dqTU80C6Wzlq7HGENK1CtUwyNSVn8An5gE,2578
40
+ focus_data_toolkit/generators/engine/scenarios_core.py,sha256=VXr0cFzZjoAAmzifnzOqtUPREsd2JL-gI4IQ5mV5UHc,16755
41
+ focus_data_toolkit/generators/engine/serialize.py,sha256=cW8x_VjYYsFlFcHFDBF4nRvWOBpdmnW0bhGsBvZ6sqg,5974
42
+ focus_data_toolkit/generators/providers/__init__.py,sha256=0WEjH3NpHK0hFl11weshUPSH9ADQPE2-YE8oM7mSKnI,892
43
+ focus_data_toolkit/generators/providers/aws.py,sha256=hoyObMVUo2L00NUAIIJq_Fz-zepYa6rPlDGqH2TY7Cw,7360
44
+ focus_data_toolkit/generators/providers/azure.py,sha256=v1HHmepy10et7KCrqyY_nVctXLegtG6Wg0U7M6CxicU,7920
45
+ focus_data_toolkit/generators/providers/gcp.py,sha256=PbHbRuU1GMBTmi76Gf6_GzanyQ37cVTed-_46TPVNq4,7761
46
+ focus_data_toolkit/generators/providers/profile.py,sha256=3KUiNzTAM206xXfBiTsk9s3VqSZjANmOiTMya3MZrfU,4967
47
+ focus_data_toolkit/generators/versions/__init__.py,sha256=tciA-qFbVzVbCKGhBK5chbL7ntfQi6m81lHY_-JMr7Y,798
48
+ focus_data_toolkit/generators/versions/adapter.py,sha256=HkfjLVUNA0f5XOXqq0egtIsUvJsAOOptmE8q3NFU3YU,1687
49
+ focus_data_toolkit/generators/versions/v1_2.py,sha256=QxWqN_cTF44JegCSHdergSyEHBh1QKNPmJpgLxxO-WE,3063
50
+ focus_data_toolkit/generators/versions/v1_3.py,sha256=w7Qirn1qQUfT_F83ns3Cipuing1HwI3lFI0oG10RmvI,4644
51
+ focus_data_toolkit/io/__init__.py,sha256=49aHPWaJ0PN9lYyxYX5pjpmc7UdurqaiSl1mzgG-xvk,51
52
+ focus_data_toolkit/io/atomic_writer.py,sha256=e_eKWMe25Ydwuyil93UPIakJiS1bhn-9d29suxPGi80,20471
53
+ focus_data_toolkit/io/csv_io.py,sha256=w9qOok9uf6ZlICOCfO2j3hAuVo8xS2aZSil5-l3bvU4,5002
54
+ focus_data_toolkit/io/parquet_io.py,sha256=E7BzbG4Kv4WKujMwOkq-GpzA5ZAD0zNh4x_Wi8pTZPw,22587
55
+ focus_data_toolkit/io/records.py,sha256=R78VfSqYHr2JPTso2ucUHe36AB5uS9ekbu9xJLdm95Y,2831
56
+ focus_data_toolkit/io/row_source.py,sha256=NTs2J4X945kJ2ezTP25_pWZ0QG2yuz_cAQCKOtJrhxI,4507
57
+ focus_data_toolkit/model/__init__.py,sha256=ezdCoMnn1-Z0ucqHJWuC-G818gBgqrd_zDEn_s6BwD8,1196
58
+ focus_data_toolkit/model/capabilities.py,sha256=zyNrXs5JNkSyf5oM8z8GNjdtVPKkbNVxGDBydLY3n7A,2576
59
+ focus_data_toolkit/model/focus_1_4_decimal_scale.json,sha256=d06EzrRi-nfl_sq5zMOsoWb_R0Y5uHFnU3lQnfAIpL0,893
60
+ focus_data_toolkit/model/focus_1_4_model.json,sha256=iwBuGja8lnj-9rechHZU-RTqer4lyCdHiqRO4dFbIUs,64971
61
+ focus_data_toolkit/model/focus_1_4_servicesubcategory.json,sha256=hTJGcJO-3R4iioLwfHgtiykobeddCnm8y_iW3r2Gf2Q,3244
62
+ focus_data_toolkit/model/focus_json_keys.py,sha256=5wBp_FmlJoyz7NpkSShbtuT87E9UKm48O28ZWNR8OEk,4164
63
+ focus_data_toolkit/model/iso_4217_currencies.json,sha256=ARakJ2kZJZvcKaqbDUnRpD0XXnhx-39TMefa6GR7us4,1446
64
+ focus_data_toolkit/model/json_schema_check.py,sha256=XCN7VBaHEVyPa8Wf0T6R6F8UD4hSKSiEYjMaYwkNr-w,8953
65
+ focus_data_toolkit/model/model_provenance.json,sha256=t4rHSZqcl15BgxOPjgdpZBw2WiJMeGDzEac_MX92hFQ,3848
66
+ focus_data_toolkit/model/validator.py,sha256=ttquxO8jJ5MYxpIGF6Wybn-WILNg5KTXevB5272NZYU,20534
67
+ focus_data_toolkit/model/json_schemas/allocatedmethoddetailsobjectschema.json,sha256=D1UkaZVEM8BKUa0YVh5LheE3h1cM2kpMxaczo5IU2P0,3250
68
+ focus_data_toolkit/model/json_schemas/commitmentprogrameligibilitydetailsobjectschema.json,sha256=DML8S_w6cgdUWRwBeliZ0hT3jWMBZ6FumcKHXLkd5XY,1647
69
+ focus_data_toolkit/model/json_schemas/contractappliedobjectschema.json,sha256=Jq04D4nBK0FPNoIvtPEnhuTqz7DQCsR4GoR1JIOl1z4,4352
70
+ focus_data_toolkit/model/json_schemas/contractcommitmentapplicabilityobjectschema.json,sha256=bDlqpawMz-0b-xxGTWaYaEUgVUrKUNyjJYMH_3eaVls,7143
71
+ focus_data_toolkit/model/json_schemas/json_schemas_provenance.json,sha256=oS7ns_LE2jTYbMsh-7w_6Bxd_EfXdIQ8sWFILi95auY,1778
72
+ focus_data_toolkit/schema/__init__.py,sha256=2zLyGC0WIDk1EJnBpWs3AnULjUbUUl77qumIDfeCCHk,718
73
+ focus_data_toolkit/schema/detection.py,sha256=oEhQsRJvNNOz7KFbtrDFDY9g8p1UvYLrGSjF6vfNxOE,10715
74
+ focus_data_toolkit/schema/registry.py,sha256=W4CHm6HavH_m8uh1Mv9UYDvhbUhh_ymYMehiSO2Ak3w,5223
75
+ focus_data_toolkit/storage/__init__.py,sha256=fq4abXU1niE3iASKD6YX1dv313ogXkaXxtuQd6lEWjU,80
76
+ focus_data_toolkit/storage/external_index.py,sha256=J1gSMJosSdLXuBnA0zb4yREt7v047lLHD-iAPC7ipp4,4090
77
+ focus_data_toolkit/storage/spill.py,sha256=E5ZannDqio3T0yaxwCa7kowXsqwONsV-_u6Bx-wzyQU,5435
78
+ focus_data_toolkit/studio/__init__.py,sha256=RA2hPijhhWjczdTyb-r0e9yFQDFeJMANlG3XoKi460E,958
79
+ focus_data_toolkit/studio/app.py,sha256=lU1m-UT_IOIRkxzOGLgFUWuuWwEquy_XGl9WPSQ0-v4,20472
80
+ focus_data_toolkit/studio/config.py,sha256=Adcslwc6TMWY0SGd2AvtavQMft_8__-s_XZezTgCk7Y,1654
81
+ focus_data_toolkit/studio/jobs.py,sha256=ALf2L_WCMAXnvwplCrpDEtE6F7HjfWg4dHrMUoj-LWw,5527
82
+ focus_data_toolkit/studio/preview.py,sha256=aNar6QMey_7oV7TPgMLDLWHvp7R9EFw9GYGk7_mL6eE,1229
83
+ focus_data_toolkit/studio/security.py,sha256=5LUReMjwdXwyF-eP9p2SZtjxrm9Ieta905yFD-FVXng,5457
84
+ focus_data_toolkit/studio/server.py,sha256=WkAb5FBudFg3Wh0HktDjd_eaFccA0Gm25BhBCIBaP0o,2211
85
+ focus_data_toolkit/studio/frontend/app.js,sha256=zioD0JAY5RE30WNIPUx3sW5Yq0mA2FsLM_9fIToVjL4,9731
86
+ focus_data_toolkit/studio/frontend/index.html,sha256=9YtZuY9RAL67hj6wlkQlx4HD-l2iMgLR3STtgI95sjg,3606
87
+ focus_data_toolkit/studio/frontend/style.css,sha256=KLfzs335aTfs7-xkx_cT9rAr66NjaQ6hhlRIOpDYHE8,3906
88
+ focus_data_toolkit/supplement/__init__.py,sha256=Py15wzVq6jVt2Avd88dcUvoxNpbmfr38B2tzUoZ0bNU,1845
89
+ focus_data_toolkit/supplement/apply.py,sha256=gQPrSeC29ApSvTZrFVQdvJEjURON6kxry3TcP45cVCQ,12981
90
+ focus_data_toolkit/supplement/gaps.py,sha256=Lov9yU5OEKgNKwPVyBvgqYQ4AVviAYZqKKGlMWpmh44,9502
91
+ focus_data_toolkit/supplement/kinds.py,sha256=LePJW3QZmqvPYDP2fhNDFX0MDr1ymVGb_Jk0ooKDsbM,3986
92
+ focus_data_toolkit/supplement/loader.py,sha256=og8UVY_sGL2G1dJ5R5vviFoQYkZVG4V0r1F6ZW-5_1U,16650
93
+ focus_data_toolkit/supplement/spec.py,sha256=hEjVMSKNM_8Z7j07S2z7PsQF89jAOJY3zSZCXT65OmU,2959
94
+ focus_data_toolkit/supplement/validate.py,sha256=XqiZ3CGShcR-KfxpVhM3OSHQELDOilbQ9gVzOKz2CX4,8977
95
+ focus_data_toolkit/supplement/adapters/__init__.py,sha256=VGo1WZSo595WbdyBH9nDycJTf3cX54dXy2hGxe_rK7M,428
96
+ focus_data_toolkit/supplement/adapters/adapters_provenance.json,sha256=Ddu1sYYkSqB55X2G-_DoMfwpdyG3p-mrdFhNz2KVsqk,1965
97
+ focus_data_toolkit/supplement/adapters/aws_invoice_summary.json,sha256=9p0xkUaU6c_X-ew746_yc9I0Ck-DaChdhsMctpY-4K4,1385
98
+ focus_data_toolkit/supplement/adapters/aws_savings_plans.json,sha256=uvCZuhd1NnLFK6UE2d7vl4wnnEoWjIvnKwVQGoeN9vs,2390
99
+ focus_data_toolkit/supplement/adapters/azure_invoice.json,sha256=NTm-up2EyrWuf6j34sNfsy08Mz6FoB_4h3SK1qR4DoU,1852
100
+ focus_data_toolkit/supplement/adapters/gcp_compute_commitments.json,sha256=itwVsiIRTvb5DeygWq1EgBi1aipyFMQBOp42A5uzSsM,2179
101
+ focus_data_toolkit/supplement/adapters/registry.py,sha256=xJfewTrtsutS4ilCxn2hRC0UjzvgXfKpcL4wgscFbIQ,8240
102
+ focus_data_toolkit/validate/__init__.py,sha256=q26nUoQXAp3ikNbyvE9wNPpsif4Mk5i5ZOMHGPec9O8,399
103
+ focus_data_toolkit/validate/allocation.py,sha256=bMAOwo-d_iuJnCaohSV7IYmbxtXJ7_31sYp79Q6YoSM,13211
104
+ focus_data_toolkit/validate/bundle.py,sha256=OI-MaIKUp1EHqJBS9aTcGmkn7YRZTXnBCeJMg4rfGcQ,9906
105
+ focus_data_toolkit/validate/codes.py,sha256=EUukxWGTopAouuQBdq551qY5v8Kwxzp-ZiyUv7UiLF4,5762
106
+ focus_data_toolkit/validate/corrections.py,sha256=klLSgorVh7ST9mSbPkLCIVmmRtRpa4qli1CzOAk-nok,10164
107
+ focus_data_toolkit/validate/reconciliation.py,sha256=qNBEObAtQG_VLCAm0JF9AnYAZlEdI_D5fZdbM6ZvrkQ,4119
108
+ focus_data_toolkit/validate/referential.py,sha256=dO_uXy6mIwLfxPnwsFKSE7fRiBw-K55XK_De-KzoXsc,11472
109
+ focus_data_toolkit-0.11.0.dist-info/licenses/LICENSE,sha256=NtTLMNmquvok1ABHks0U0ap9Kvz9PWA0JLg5PtQUMi4,1088
110
+ focus_data_toolkit-0.11.0.dist-info/licenses/NOTICE,sha256=J51RSKahdwtYmrhwgKyGK_mZjaJc04zTjguO5TJUgSg,2841
111
+ focus_data_toolkit-0.11.0.dist-info/licenses/LICENSES/CC-BY-4.0.txt,sha256=1VdTnfaOdxzB7tzJHRP3D8qTDlCNEe7cr6SxXbSeN0Q,17023
112
+ focus_data_toolkit-0.11.0.dist-info/METADATA,sha256=meeu3PZZ_w98Vo79s8PBBKUdfVL5oQanEFwID35NtqY,27002
113
+ focus_data_toolkit-0.11.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
114
+ focus_data_toolkit-0.11.0.dist-info/entry_points.txt,sha256=MiGCjfdjj1I73tNzREaD1X28p59PJ-nkboal8xXQ6hg,62
115
+ focus_data_toolkit-0.11.0.dist-info/top_level.txt,sha256=tLPrj44kcwFwJfhRB-fXgP-z_V4e6sPBmCwRBtf7rEw,19
116
+ focus_data_toolkit-0.11.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ focus-toolkit = focus_data_toolkit.cli:main