unalloc 0.3.2__tar.gz → 0.3.3__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 (39) hide show
  1. {unalloc-0.3.2 → unalloc-0.3.3}/CHANGELOG.md +8 -0
  2. {unalloc-0.3.2 → unalloc-0.3.3}/PKG-INFO +16 -1
  3. {unalloc-0.3.2 → unalloc-0.3.3}/README.md +15 -0
  4. unalloc-0.3.3/case_studies/environment_lifetime/README.md +184 -0
  5. {unalloc-0.3.2 → unalloc-0.3.3}/pyproject.toml +1 -1
  6. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/__init__.py +1 -1
  7. {unalloc-0.3.2 → unalloc-0.3.3}/.gitignore +0 -0
  8. {unalloc-0.3.2 → unalloc-0.3.3}/LICENSE +0 -0
  9. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/cli.py +0 -0
  10. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/core/__init__.py +0 -0
  11. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/core/attribute.py +0 -0
  12. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/core/models.py +0 -0
  13. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/core/normalize.py +0 -0
  14. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/core/provenance.py +0 -0
  15. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/core/reconcile.py +0 -0
  16. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/fixtures/README.md +0 -0
  17. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/fixtures/anthropic_cost_report.json +0 -0
  18. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/fixtures/invoices.json +0 -0
  19. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/fixtures/litellm_spend.json +0 -0
  20. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/fixtures/openai_costs.json +0 -0
  21. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/fixtures/opencost_allocation.json +0 -0
  22. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/render/__init__.py +0 -0
  23. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/render/table.py +0 -0
  24. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/sources/__init__.py +0 -0
  25. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/sources/anthropic.py +0 -0
  26. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/sources/base.py +0 -0
  27. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/sources/litellm.py +0 -0
  28. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/sources/openai.py +0 -0
  29. {unalloc-0.3.2 → unalloc-0.3.3}/src/unalloc/sources/opencost.py +0 -0
  30. {unalloc-0.3.2 → unalloc-0.3.3}/tests/test_attribute.py +0 -0
  31. {unalloc-0.3.2 → unalloc-0.3.3}/tests/test_cli.py +0 -0
  32. {unalloc-0.3.2 → unalloc-0.3.3}/tests/test_docs_build.py +0 -0
  33. {unalloc-0.3.2 → unalloc-0.3.3}/tests/test_fetch.py +0 -0
  34. {unalloc-0.3.2 → unalloc-0.3.3}/tests/test_normalize.py +0 -0
  35. {unalloc-0.3.2 → unalloc-0.3.3}/tests/test_paper_lint.py +0 -0
  36. {unalloc-0.3.2 → unalloc-0.3.3}/tests/test_paper_numbers.py +0 -0
  37. {unalloc-0.3.2 → unalloc-0.3.3}/tests/test_provenance.py +0 -0
  38. {unalloc-0.3.2 → unalloc-0.3.3}/tests/test_reconcile.py +0 -0
  39. {unalloc-0.3.2 → unalloc-0.3.3}/tests/test_sources.py +0 -0
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.3 — 2026-11-13
4
+
5
+ ### Added
6
+ - Repository-only synthetic environment-lifetime memory allocation case study: per-environment overlap handling, exact equal-share allocation, explicit uncovered residual, and missing-telemetry comparison.
7
+ - 43 focused tests and reproducible output checks in CI. The synthetic fixture reconciles 270 GB-seconds as 120 assigned plus 150 residual; omitting a request still reconciles, illustrating why conservation does not prove correct attribution.
8
+
9
+ No production CLI or source-adapter behavior changes. This is not AWS ingestion, an observed AWS bill, CPU billing, or a savings claim. The case study lives in the GitHub source tree and source archive, not the PyPI wheel/sdist. The package patch includes updated README metadata and version identity.
10
+
3
11
  ## 0.3.2 — 2026-10-05
4
12
 
5
13
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: unalloc
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Find the AI spend nobody owns. Joins OpenCost allocation data with LLM provider bills and reports what's unattributed.
5
5
  Project-URL: Homepage, https://github.com/timurista/unalloc
6
6
  Project-URL: Issues, https://github.com/timurista/unalloc/issues
@@ -120,6 +120,21 @@ The ledger does not tie out. Attribution built on this is not defensible yet.
120
120
 
121
121
  Anthropic agrees within the 1% tolerance. OpenAI billed $381 more than its costs endpoint reported, so any attribution built on that ledger understates OpenAI spend until the gap is explained.
122
122
 
123
+ ## Environment-lifetime accounting example
124
+
125
+ A new [repository-only case study](case_studies/environment_lifetime/README.md) separates
126
+ summed request duration from environment-lifetime memory in a synthetic, offline fixture.
127
+ It splits overlapping requests within each environment, preserves uncovered capacity as a
128
+ residual, and shows that missing request telemetry can change attribution while totals still
129
+ reconcile exactly. Equal sharing and constant memory are explicit modeling choices, not
130
+ measured AWS ownership. No AWS ingestion, pricing, savings, or production CLI integration
131
+ is claimed. Clone the repository to run it; case studies are not installed by `pip install unalloc`.
132
+
133
+ ```bash
134
+ python3 -m case_studies.environment_lifetime
135
+ python3 -m unittest discover -s case_studies/environment_lifetime -t . -v
136
+ ```
137
+
123
138
  ## Install
124
139
 
125
140
  ```bash
@@ -78,6 +78,21 @@ The ledger does not tie out. Attribution built on this is not defensible yet.
78
78
 
79
79
  Anthropic agrees within the 1% tolerance. OpenAI billed $381 more than its costs endpoint reported, so any attribution built on that ledger understates OpenAI spend until the gap is explained.
80
80
 
81
+ ## Environment-lifetime accounting example
82
+
83
+ A new [repository-only case study](case_studies/environment_lifetime/README.md) separates
84
+ summed request duration from environment-lifetime memory in a synthetic, offline fixture.
85
+ It splits overlapping requests within each environment, preserves uncovered capacity as a
86
+ residual, and shows that missing request telemetry can change attribution while totals still
87
+ reconcile exactly. Equal sharing and constant memory are explicit modeling choices, not
88
+ measured AWS ownership. No AWS ingestion, pricing, savings, or production CLI integration
89
+ is claimed. Clone the repository to run it; case studies are not installed by `pip install unalloc`.
90
+
91
+ ```bash
92
+ python3 -m case_studies.environment_lifetime
93
+ python3 -m unittest discover -s case_studies/environment_lifetime -t . -v
94
+ ```
95
+
81
96
  ## Install
82
97
 
83
98
  ```bash
@@ -0,0 +1,184 @@
1
+ # Environment-lifetime memory allocation (synthetic)
2
+
3
+ This is a small example that runs offline and uses only the standard library. It splits each
4
+ environment's **lifetime memory GB-seconds** among the requests that ran in that
5
+ environment. Any time not covered by a request becomes a residual.
6
+
7
+ > **Scope.** The example does synthetic arithmetic on a hand-written fixture. It is
8
+ > **not** validated AWS ingestion, **not** an AWS bill, and **not** a CPU billing model.
9
+ > It contains no prices and no dollar or savings figures. The case study is not wired into the `unalloc` CLI or its sources,
10
+ > and it changes no existing product behavior.
11
+
12
+ ## Why environment lifetime and not request duration
13
+
14
+ The AWS Lambda docs (checked 2026-11-13) describe two facts this example models:
15
+
16
+ - [Lambda web functions](https://docs.aws.amazon.com/lambda/latest/dg/web-functions.html):
17
+ memory is billed over the life of the environment.
18
+ - [Environment lifecycle](https://docs.aws.amazon.com/lambda/latest/dg/web-functions-env-lifecycle.html):
19
+ an environment has no guaranteed lifetime.
20
+
21
+ So the sum of request durations is not the quantity that memory accrues over. Concurrent
22
+ requests share one environment's memory, and memory also accrues while no request is
23
+ running. This example only shows the arithmetic for those two effects. It reads no AWS data.
24
+
25
+ ## Model
26
+
27
+ For each environment, the fixture gives:
28
+
29
+ - An observed lifetime `[lifetime_start_s, lifetime_end_s]`, which must have positive length.
30
+ - A synthetic constant memory capacity `memory_gb`, which must be greater than 0.
31
+ - `lifetime_gb_seconds = memory_gb × (lifetime_end_s − lifetime_start_s)`.
32
+
33
+ For each request, the fixture gives:
34
+
35
+ - An `environment` that must exist.
36
+ - A span `[start_s, end_s]` that must lie inside that environment's lifetime.
37
+
38
+ The example integrates each environment independently:
39
+
40
+ 1. Cut the lifetime into non-overlapping slices at every lifetime boundary and every request
41
+ boundary.
42
+ 2. Check how many requests cover each slice:
43
+ - If `n ≥ 1` requests cover the slice, the slice counts as busy. Each covering request
44
+ receives `memory_gb × width / n`.
45
+ - If no request covers the slice, it counts as uncovered. Its GB-seconds go to the
46
+ **residual**.
47
+ 3. Never share a slice across environments. Two requests on different environments get
48
+ separate allocations, even when their wall-clock spans overlap.
49
+
50
+ ### Reported quantities
51
+
52
+ The example reports each quantity per environment and as a total (except `overlap_seconds`, which is per-environment only):
53
+
54
+ | Field | Meaning |
55
+ |---|---|
56
+ | `raw_request_seconds` | Sum of request durations. Counts overlapping time more than once. |
57
+ | `union_busy_seconds` | Per-environment union of request spans. The total sums these environment-seconds, not a global wall-clock union. |
58
+ | `overlap_seconds` | `raw_request_seconds − union_busy_seconds`. |
59
+ | `uncovered_seconds` | Lifetime seconds that no request covers. |
60
+ | `lifetime_gb_seconds` | `memory_gb × lifetime seconds`. |
61
+ | `allocated_gb_seconds` | Sum of per-request allocations, equal to `memory_gb × union_busy_seconds`. |
62
+ | `residual_gb_seconds` | `memory_gb × uncovered_seconds`. |
63
+ | `reconciliation` | Checks that `allocated + residual == lifetime` exactly. The `difference` field must be `"0"`. |
64
+
65
+ The output also lists `allocated_gb_seconds` for each request.
66
+
67
+ ### Residual: unobserved unless the fixture claims completeness
68
+
69
+ By default, the example labels the residual `unobserved`. A gap in request telemetry might
70
+ mean the environment sat idle, or it might mean a request went unrecorded.
71
+
72
+ The residual is labelled `idle_by_complete_synthetic_fixture` only when the fixture sets both
73
+ `"synthetic": true` and `"telemetry_complete": true`. The bundled fixture sets both, because
74
+ it is hand-written and lists every request by construction. When you run with
75
+ `--drop-request`, the example always sets `telemetry_complete` to false. In that case, the
76
+ residual is labelled `unobserved`.
77
+
78
+ ### Zero-length requests
79
+
80
+ A request with `start_s == end_s` is accepted if it lies inside its environment's
81
+ lifetime, including the lifetime's endpoints. A zero-length request has the following
82
+ properties:
83
+
84
+ - It covers no slice.
85
+ - It receives exactly `0` GB-seconds.
86
+ - It adds `0` to `raw_request_seconds`.
87
+ - It is flagged `"zero_duration": true`.
88
+
89
+ ### Exactness and validation
90
+
91
+ All arithmetic uses `fractions.Fraction`. The example accepts numbers in these forms:
92
+
93
+ - Decimal strings, such as `"1.5"`.
94
+ - Integers.
95
+ - JSON numbers, which the example parses as `Decimal`, so `0.1` stays exactly 1/10.
96
+ - In-memory `Decimal` or `Fraction` values.
97
+
98
+ The output writes each value as an exact string. A value that terminates in base 10 is written
99
+ as a decimal, such as `"28.75"`. Any other value is written as `"numerator/denominator"`, such
100
+ as `"155/6"`.
101
+
102
+ The example rejects the following inputs with a `FixtureError` that names the offending field:
103
+
104
+ - Binary floats, booleans, `NaN`, `sNaN`, `Infinity`, and the JSON `NaN` or `Infinity`
105
+ literals.
106
+ - Malformed number strings.
107
+ - Negative times, and a memory capacity of 0 or less.
108
+ - Lifetimes where `end <= start`, and requests where `end < start`.
109
+ - Requests that fall outside their environment's lifetime.
110
+ - Unknown environments.
111
+ - Duplicate environment IDs, duplicate request IDs (checked across all environments), and
112
+ duplicate JSON keys.
113
+ - IDs that are empty, not a string, or padded with whitespace.
114
+ - Missing or unknown fields, and a wrong `schema` value.
115
+
116
+ ## Bundled fixture
117
+
118
+ The fixture is [`fixture.json`](fixture.json). All values are synthetic.
119
+
120
+ | Environment | Memory (GB) | Lifetime (s) | Requests | What it shows |
121
+ |---|---|---|---|---|
122
+ | `env-a` | 1.5 | [0, 100] | `a-1` [10,40], `a-2` [20,50], `a-3` [30,35], `a-4` [60,60], `a-5` [70,90] | Three-way overlap, a zero-length request, and gaps |
123
+ | `env-b` | 0.5 | [5, 65] | `b-1` [5,65], `b-2` [15,25], `b-3` [20,30] | Up to 3 concurrent requests. The thirds don't terminate as decimals. |
124
+ | `env-c` | 3 | [0, 30] | none | The whole lifetime is residual |
125
+
126
+ The bundled fixture produces these results, from [`fixture_output.json`](fixture_output.json):
127
+
128
+ | Environment | Raw request s | Union busy s | Lifetime GB-s | Allocated GB-s | Residual GB-s |
129
+ |---|---|---|---|---|---|
130
+ | `env-a` | 85 | 60 | 150 | 90 | 60 |
131
+ | `env-b` | 80 | 60 | 30 | 30 | 0 |
132
+ | `env-c` | 0 | 0 | 90 | 0 | 90 |
133
+ | **total** | **165** | **120** | **270** | **120** | **150** |
134
+
135
+ The per-request allocations are:
136
+
137
+ - `a-1` 28.75, `a-2` 28.75, `a-3` 2.5, `a-4` 0, `a-5` 30.
138
+ - `b-1` 155/6, `b-2` 25/12, `b-3` 25/12.
139
+
140
+ ### Missing telemetry
141
+
142
+ The file [`fixture_output_missing_a-2.json`](fixture_output_missing_a-2.json) shows the result
143
+ when request `a-2` is removed:
144
+
145
+ - The residual for `env-a` grows from 60 to **75** GB-s. The extra 15 GB-s comes from
146
+ [40, 50], the 10 seconds when only `a-2` was running, at 1.5 GB.
147
+ - The slices that `a-2` shared now go to `a-1` (41.25) and `a-3` (3.75).
148
+ - The lifetime GB-seconds don't change, and the reconciliation is still exact.
149
+ - The residual is labelled `unobserved`.
150
+
151
+ ## Reproduce
152
+
153
+ Run these commands from the repository root. They need Python 3.9 or later, no third-party
154
+ packages, and no network access.
155
+
156
+ ```sh
157
+ # Print the result JSON for the bundled fixture.
158
+ python3 -m case_studies.environment_lifetime
159
+
160
+ # Check that the stored outputs match the current code byte for byte. The command exits 1 on a mismatch.
161
+ python3 -m case_studies.environment_lifetime --check case_studies/environment_lifetime/fixture_output.json
162
+ python3 -m case_studies.environment_lifetime --drop-request a-2 \
163
+ --check case_studies/environment_lifetime/fixture_output_missing_a-2.json
164
+
165
+ # Regenerate the stored outputs.
166
+ python3 -m case_studies.environment_lifetime --out case_studies/environment_lifetime/fixture_output.json
167
+ python3 -m case_studies.environment_lifetime --drop-request a-2 \
168
+ --out case_studies/environment_lifetime/fixture_output_missing_a-2.json
169
+
170
+ # Run the tests with unittest from the standard library. pytest is not required.
171
+ python3 -m unittest discover -s case_studies/environment_lifetime -t . -v
172
+ ```
173
+
174
+ The repository's `pytest` configuration only collects tests from `tests/`, so this suite
175
+ runs only when you invoke the `unittest` command above.
176
+
177
+ ## Files
178
+
179
+ - `lifetime.py`: parsing, validation, exact integration, and reconciliation.
180
+ - `__main__.py`: the command-line interface (`--fixture`, `--drop-request`, `--out`, and
181
+ `--check`).
182
+ - `fixture.json`: the synthetic input.
183
+ - `fixture_output.json` and `fixture_output_missing_a-2.json`: the stored outputs.
184
+ - `test_lifetime.py`: the `unittest` suite.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "unalloc"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "Find the AI spend nobody owns. Joins OpenCost allocation data with LLM provider bills and reports what's unattributed."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -2,5 +2,5 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.3.2"
5
+ __version__ = "0.3.3"
6
6
  __all__ = ["__version__"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes