Skip to content

Commit 947e63d

Browse files
committed
Add ARCHITECTURE.md design documentation
Closes the last OSPS Baseline Level 2 gap (OSPS-SA-01.01). The document covers two scopes: the runtime library (actors and actions inside the consumer's process) and the build and release pipeline (actors and actions that produce the released artifacts). CI gates, trust boundaries, and the documentation pipeline are summarized in their own sections with pointers to the source of truth for each. The contributing guide gets a new Project Architecture section that points contributors at ARCHITECTURE.md and states the update contract: any change to a public API, the build pipeline, or a trust boundary requires the corresponding architecture update in the same PR.
1 parent cf8a71b commit 947e63d

2 files changed

Lines changed: 339 additions & 0 deletions

File tree

ARCHITECTURE.md

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
# bitmath Architecture
2+
3+
> This document exists to satisfy the OSPS Baseline Level 2 control
4+
> [OSPS-SA-01.01](https://www.bestpractices.dev/en/projects/12749/baseline-2)
5+
> (design documentation showing the actions and actors within the
6+
> system), in addition to being useful for downstream consumers and
7+
> contributors who want to understand how bitmath fits together.
8+
9+
## Contents
10+
11+
- [Why this document exists](#why-this-document-exists)
12+
- [Runtime architecture](#runtime-architecture)
13+
- [Build and release pipeline](#build-and-release-pipeline)
14+
- [CI and quality gates](#ci-and-quality-gates)
15+
- [Trust boundaries](#trust-boundaries)
16+
- [Documentation pipeline](#documentation-pipeline)
17+
- [Maintenance](#maintenance)
18+
19+
## Why this document exists
20+
21+
This is the architecture map for bitmath. It exists so downstream
22+
consumers and future contributors can see, in one place, what the
23+
moving parts are and how they fit together. It covers two things: the
24+
runtime library (what runs in your process when you `import bitmath`),
25+
and the build and release pipeline (how that code gets from this
26+
repository onto your machine).
27+
28+
It does not cover everything in the repository. Other concerns have
29+
their own homes:
30+
31+
* [MAINTAINERS.md](MAINTAINERS.md): who has access to what.
32+
* [SECURITY.md](SECURITY.md): how to report a vulnerability.
33+
* [SECURITY_ASSESSMENT.md](SECURITY_ASSESSMENT.md): the standing
34+
threat model.
35+
* [bitmath on Read The Docs](https://bitmath.readthedocs.io/): API
36+
reference and tutorials.
37+
38+
If you are wondering whether to update this file: yes, if you are
39+
adding a new public function, restructuring the class hierarchy,
40+
changing the build backend, or changing how releases get published.
41+
No, if you are fixing a bug or adding a test.
42+
43+
## Runtime architecture
44+
45+
### Component inventory
46+
47+
The runtime library is a single Python module, `bitmath`, deliberately
48+
kept small. Almost all the logic lives in `bitmath/__init__.py`. The
49+
actors that show up at runtime:
50+
51+
| Actor | Lives at | What it does |
52+
|-------|----------|--------------|
53+
| User code | The consumer's process | Imports bitmath, calls module-level functions, instantiates unit classes, does arithmetic |
54+
| Public module API | `bitmath.*` | Module-level entry points: `parse_string`, `parse_string_unsafe`, `best_prefix`, `getsize`, `listdir` (deprecated in 2.1.0), `query_capacity`, `query_device_capacity` |
55+
| `Bitmath` base class | `bitmath.Bitmath` | The arithmetic, comparison, bitwise, and formatting engine. Every concrete unit class inherits from it |
56+
| `Byte` family | `bitmath.Byte`, `KiB`, `MiB`, `GiB`, `TiB`, `PiB`, `EiB`, `kB`, `MB`, `GB`, `TB`, `PB`, `EB`, `ZB`, `YB` | Byte-based concrete unit classes (NIST binary and SI decimal) |
57+
| `Bit` family | `bitmath.Bit`, `Kib`, `Mib`, `Gib`, `Tib`, `Pib`, `Eib`, `kb`, `Mb`, `Gb`, `Tb`, `Pb`, `Eb`, `Zb`, `Yb` | Bit-based concrete unit classes (NIST binary and SI decimal) |
58+
| Constants | `bitmath.NIST`, `bitmath.SI`, `bitmath.NIST_PREFIXES`, `bitmath.SI_PREFIXES`, `bitmath.ALL_UNIT_TYPES` | Module-level lookup tables and system identifiers |
59+
| OS abstraction layer | Standard library: `shutil`, `os`, `fcntl` (Linux), `ctypes` (Windows) | Platform-specific calls used by the capacity-query functions |
60+
61+
### Layered view
62+
63+
```
64+
+--------------------------------------------------------------+
65+
| User code |
66+
| import bitmath; KiB(1024).MiB |
67+
+--------------------------------------------------------------+
68+
|
69+
v
70+
+--------------------------------------------------------------+
71+
| Public module API (bitmath.*) |
72+
| parse_string, best_prefix, getsize, query_capacity, etc. |
73+
+--------------------------------------------------------------+
74+
|
75+
v
76+
+--------------------------------------------------------------+
77+
| Library core |
78+
| Bitmath base class |
79+
| Byte family (KiB, MiB, kB, MB, ...) |
80+
| Bit family (Kib, Mib, kb, Mb, ...) |
81+
| _norm_value: normalize every value to bits internally |
82+
+--------------------------------------------------------------+
83+
|
84+
v (only for getsize / listdir /
85+
| query_capacity /
86+
| query_device_capacity)
87+
+--------------------------------------------------------------+
88+
| OS abstraction layer |
89+
| shutil.disk_usage -- cross-platform capacity |
90+
| fcntl.ioctl -- Linux block device capacity |
91+
| ctypes / Win32 API -- Windows block device capacity |
92+
| macOS: NotImplementedError (System Integrity Protection) |
93+
+--------------------------------------------------------------+
94+
```
95+
96+
### Actions
97+
98+
The things that happen at runtime, in roughly the order a typical
99+
consumer triggers them:
100+
101+
1. **Construction.** A user creates a bitmath instance (`KiB(1024)`,
102+
`Byte(bits=8192)`, `parse_string("4.5 GiB")`). The constructor
103+
passes the value through `_norm_value`, which converts everything
104+
to an internal bits representation using the class-level
105+
`_base_value` and `_unit_value` constants. After construction, the
106+
instance carries one canonical value.
107+
108+
2. **Conversion.** A user requests a different unit
109+
(`KiB(1024).MiB`). Each unit class exposes properties that compute
110+
their own value from the internal bits representation. No
111+
conversion is cached; every property access does the math fresh.
112+
113+
3. **Arithmetic.** Operators (`+`, `-`, `*`, `/`, `//`, `%`,
114+
`divmod`) operate on the internal bits representation and return
115+
a new instance. Mixed-unit arithmetic (`KiB(1) + MiB(1)`)
116+
resolves through the bits representation, so the result is
117+
unit-correct.
118+
119+
4. **Comparison and bitwise.** Comparisons (`<`, `>`, `==`) and
120+
bitwise operations (`&`, `|`, `^`, `~`, `<<`, `>>`) work on the
121+
internal bits representation.
122+
123+
5. **Formatting.** `str()`, `repr()`, and `format()` (including
124+
f-string usage) produce human-readable output. The `best_prefix`
125+
function picks the most readable unit for a given byte value.
126+
127+
6. **Parsing.** `parse_string` and `parse_string_unsafe` accept a
128+
string like `"4.5 GiB"` and return a bitmath instance. The strict
129+
form raises on ambiguous unit; the unsafe form picks a default
130+
system (SI by default) when the unit is ambiguous.
131+
132+
7. **Filesystem inspection.** `getsize` accepts a path or
133+
`pathlib.Path` and returns a bitmath instance for the file size.
134+
`listdir` (deprecated in 2.1.0) walks a tree and yields sizes.
135+
136+
8. **Capacity querying.** `query_capacity(path)` returns a
137+
`Capacity(total, used, free)` named tuple of `Byte` instances.
138+
This uses `shutil.disk_usage` and works on every supported
139+
platform without elevated privileges. `query_device_capacity(fd)`
140+
returns the raw physical block-device capacity and requires
141+
elevated privileges on Linux (root) and Windows (administrator);
142+
macOS raises `NotImplementedError`.
143+
144+
### Data flow
145+
146+
Untrusted input enters the library at exactly three runtime places:
147+
the string parsers (`parse_string`, `parse_string_unsafe`), the
148+
filesystem helpers (`getsize`, `listdir`), and the capacity functions
149+
(`query_capacity`, `query_device_capacity`). For everything else the
150+
input is a Python number or another bitmath instance, both of which
151+
are inherently trusted. The trust boundaries section below covers
152+
what bitmath does and does not validate at each entry point. See
153+
[SECURITY_ASSESSMENT.md](SECURITY_ASSESSMENT.md) for the full threat
154+
model.
155+
156+
## Build and release pipeline
157+
158+
### Component inventory
159+
160+
| Actor | Lives at | What it does |
161+
|-------|----------|--------------|
162+
| Source repository | github.com/timlnx/bitmath | Single source of truth for all code, docs, and pipeline configuration |
163+
| `VERSION` file | Repository root | Single source of the project version; read by Hatchling and the Makefile |
164+
| `pyproject.toml` | Repository root | PEP 517/518/621 project metadata; declares Hatchling as the build backend |
165+
| Hatchling | Build-time only | PyPA-blessed build backend; produces the wheel and sdist artifacts |
166+
| `Makefile` | Repository root | Local automation; targets like `make ci`, `make build`, `make docs` |
167+
| GitHub Actions runners | `.github/workflows/*.yml` | Hosted CI execution environment |
168+
| `publish.yml` workflow | `.github/workflows/publish.yml` | Two-job workflow: build artifacts, then publish via OIDC |
169+
| PyPI Trusted Publishing | PyPI's OIDC verifier | Validates the OIDC token from the `publish.yml` workflow and authorizes the upload |
170+
| PyPI | pypi.org | The distribution channel; serves wheels and sdists over HTTPS to `pip` |
171+
| Read The Docs | bitmath.readthedocs.io | Hosts the rendered documentation; rebuilds on push to master |
172+
173+
### Pipeline view
174+
175+
```
176+
+------------------------+
177+
| Developer |
178+
| (see MAINTAINERS.md) |
179+
+-----------+------------+
180+
| git push, git tag
181+
v
182+
+------------------------+
183+
| GitHub repository |
184+
| timlnx/bitmath |
185+
| branch protection on |
186+
| master (17 required |
187+
| checks, enforce |
188+
| admins) |
189+
+-----------+------------+
190+
| release: published
191+
v
192+
+------------------------+
193+
| publish.yml workflow |
194+
| |
195+
| Job 1: build |
196+
| pip install build |
197+
| python -m build |
198+
| -> dist/ artifacts |
199+
| |
200+
| Job 2: publish |
201+
| id-token: write |
202+
| PyPI OIDC exchange |
203+
+-----------+------------+
204+
| OIDC-authenticated upload
205+
v
206+
+------------------------+
207+
| PyPI |
208+
| pypi.org/project |
209+
| /bitmath/ |
210+
| wheel + sdist |
211+
| + PEP 740 |
212+
| attestations |
213+
+-----------+------------+
214+
| pip install bitmath
215+
v
216+
+------------------------+
217+
| Consumer's process |
218+
+------------------------+
219+
```
220+
221+
### Actions
222+
223+
1. **Version bump.** I edit `VERSION` to the new SemVer string.
224+
`pyproject.toml` reads this dynamically via `[tool.hatch.version]`;
225+
the Makefile reads it for docs and man pages. There is no other
226+
place to update.
227+
228+
2. **Changelog.** I add the section for the new version to
229+
`NEWS.rst`.
230+
231+
3. **Tag and push.** A signed tag is pushed to GitHub.
232+
233+
4. **Create a GitHub Release.** Releasing from the tag fires the
234+
`release: published` event. The `publish.yml` workflow has no
235+
other trigger; it cannot fire on pull requests or direct pushes.
236+
237+
5. **Build job.** Runs on a clean ubuntu-latest runner with Python
238+
3.12. Installs `build`, runs `python -m build`, uploads the
239+
`dist/` directory as a workflow artifact.
240+
241+
6. **Publish job.** Depends on the build job. Downloads the artifact.
242+
Requests an OIDC token (`id-token: write`) from GitHub Actions and
243+
exchanges it with PyPI for short-lived upload credentials via
244+
Trusted Publishing. Uploads the wheel and sdist. No long-lived
245+
PyPI API token exists anywhere in the project.
246+
247+
7. **Consumer install.** A consumer runs `pip install bitmath`.
248+
`pip` fetches the wheel over HTTPS from PyPI, verifies the package
249+
against the published SHA-256 hash, and installs into their
250+
environment.
251+
252+
## CI and quality gates
253+
254+
CI runs continuously on `master` and on every pull request. These
255+
actors influence what is merged but do not produce released
256+
artifacts. They are summarized here for completeness; the source of
257+
truth for each is the corresponding workflow file in
258+
`.github/workflows/`.
259+
260+
| Actor | Workflow | What it gates |
261+
|-------|----------|---------------|
262+
| pytest matrix | `python.yml` | Full test suite across Python 3.9–3.13 on macOS, Ubuntu, Windows |
263+
| Bandit | `bandit.yml` | Python security smell analysis across `bitmath/` and `tests/` |
264+
| CodeQL | `codeql.yml` | Semantic analysis for Python vulnerabilities |
265+
| OSSF Scorecard | `scorecard.yml` | Weekly repository security posture report |
266+
| Dependabot | `.github/dependabot.yml` | Automated dependency update pull requests |
267+
268+
The first three are required status checks on the `master` branch.
269+
Merges are blocked until they pass. `enforce_admins: true` so I
270+
cannot bypass them.
271+
272+
## Trust boundaries
273+
274+
bitmath validates input only at the places untrusted data enters the
275+
library. The ingress points are:
276+
277+
* **String parsing** (`parse_string`, `parse_string_unsafe`).
278+
Regex-driven, no `eval`, raises `ValueError` on anything
279+
malformed. Hypothesis-based fuzzing tests exercise this path.
280+
* **Filesystem paths** (`getsize`, `listdir`). bitmath does no path
281+
sanitization. The consumer is responsible for validating paths
282+
before passing them in. This is documented behavior; sanitization
283+
rules depend on the consumer's threat model in ways the library
284+
cannot anticipate.
285+
* **File descriptors** (`query_device_capacity`). Operates only on
286+
descriptors the consumer already opened. The ioctl arguments are
287+
fixed constants; no untrusted input flows into the kernel call.
288+
* **The build pipeline.** `publish.yml` cannot be triggered by a
289+
pull request, only by a release event. Untrusted contributor code
290+
never touches PyPI credentials. Branch protection prevents direct
291+
pushes to master from bypassing CI.
292+
293+
See [SECURITY_ASSESSMENT.md](SECURITY_ASSESSMENT.md) for the full
294+
threat model and the specific mitigations at each boundary.
295+
296+
## Documentation pipeline
297+
298+
The documentation lives in `docsite/source/`. Sphinx builds it.
299+
`index.rst.in` is the source template; `index.rst` is generated from
300+
`index.rst.in` plus `README.rst` by `make docs`. Editing `index.rst`
301+
directly is wrong because the next `make docs` will overwrite it.
302+
303+
Read The Docs runs the same build automatically on push to master
304+
and publishes the result at https://bitmath.readthedocs.io/.
305+
306+
## Maintenance
307+
308+
This document is reviewed at every minor release. The contract:
309+
310+
* New public API → update the Runtime architecture component
311+
inventory and the actions list.
312+
* New CI workflow or build pipeline change → update Build and
313+
release pipeline and/or CI and quality gates.
314+
* New trust boundary → update Trust boundaries here and the threat
315+
model in [SECURITY_ASSESSMENT.md](SECURITY_ASSESSMENT.md).
316+
* Build-backend or package-tooling change → update Build and release
317+
pipeline.
318+
319+
If this document drifts out of date, that is a defect worth
320+
[opening an issue](https://github.com/timlnx/bitmath/issues/new) or
321+
a PR.

docsite/source/contributing.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,24 @@ provided template.
3434
* `View open issues <https://github.com/timlnx/bitmath/issues>`_
3535

3636

37+
.. _contributing_architecture:
38+
39+
Project Architecture
40+
********************
41+
42+
If you are about to make a non-trivial change, read the
43+
`ARCHITECTURE.md
44+
<https://github.com/timlnx/bitmath/blob/master/ARCHITECTURE.md>`_
45+
document in the repository root first. It is the map of how bitmath
46+
is put together: the runtime library, the build and release
47+
pipeline, the CI gates, and the trust boundaries.
48+
49+
The contract is that ``ARCHITECTURE.md`` is reviewed at every minor
50+
release and updated alongside any change to a public API, the build
51+
pipeline, or a trust boundary. If you are touching one of those
52+
things, your pull request should include the corresponding update.
53+
54+
3755
.. _contributing_code_style:
3856

3957
Code Style and Formatting

0 commit comments

Comments
 (0)