airom 0.1.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.
airom-0.1.0/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ _bin/
2
+ *.egg-info/
3
+ dist/
4
+ .pytest_cache/
5
+ __pycache__/
airom-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,231 @@
1
+ Metadata-Version: 2.4
2
+ Name: airom
3
+ Version: 0.1.0
4
+ Summary: Python SDK for AIROM — the open-source AI Bill of Materials (AIBOM) scanner
5
+ Project-URL: Homepage, https://github.com/airomhq/airom
6
+ Project-URL: Documentation, https://github.com/airomhq/airom/tree/main/docs-site
7
+ Project-URL: Source, https://github.com/airomhq/airom
8
+ Project-URL: Issues, https://github.com/airomhq/airom/issues
9
+ Author: AIROM Authors
10
+ License-Expression: Apache-2.0
11
+ Keywords: ai,aibom,cyclonedx,sarif,sbom,security,supply-chain
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Security
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy>=1.8; extra == 'dev'
26
+ Requires-Dist: pytest>=7; extra == 'dev'
27
+ Requires-Dist: ruff>=0.5; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # airom — Python SDK
31
+
32
+ Python SDK for [AIROM](https://github.com/airomhq/airom), the open-source **AI Bill of
33
+ Materials (AIBOM) scanner**. Discover AI assets — models, prompts, datasets, embeddings,
34
+ vector databases, frameworks, serving infrastructure — across code, containers, and
35
+ Kubernetes, and get them back as typed Python objects.
36
+
37
+ ```bash
38
+ pip install airom
39
+ ```
40
+
41
+ ## Quick start
42
+
43
+ ```python
44
+ import airom
45
+
46
+ inv = airom.fs("./my-app", min_confidence=0.8)
47
+
48
+ for c in inv.by_kind(airom.ComponentKind.HOSTED_LLM):
49
+ print(c.name, c.provider.or_default("-"), c.confidence)
50
+ for occ in c.evidence.occurrences:
51
+ print(f" {occ.location.path}:{occ.location.line} [{occ.detector_id}]")
52
+ ```
53
+
54
+ ```
55
+ gpt-4.1 openai 0.87
56
+ src/rag.py:88 [rules/openai/model-literal]
57
+ src/agent.py:12 [rules/openai/sdk-import]
58
+ ```
59
+
60
+ Every component carries the evidence that justifies it — that is the point of AIROM, and
61
+ the SDK hands you all of it.
62
+
63
+ ## Scanning
64
+
65
+ ```python
66
+ airom.scan("./app") # auto-detect: path, git URL, or image ref
67
+ airom.fs("./app") # a directory tree
68
+ airom.repo("https://github.com/o/r") # remote (shallow clone) or a local worktree
69
+ airom.image(input="img.tar") # docker save -o img.tar <ref>
70
+ airom.k8s(manifests="./deploy") # offline: enumerate workload images
71
+ airom.version() # the underlying binary's ToolInfo
72
+ ```
73
+
74
+ Common keyword args mirror the CLI flags: `select`, `rules`, `ignore`, `min_confidence`,
75
+ `max_file_size`, `io_budget`, `parallel`, `no_cache`, `cache_dir`, `offline`, `stats`,
76
+ plus `binary`, `timeout`, and `cwd`. `None` leaves the tool's own default in place — the
77
+ SDK never invents defaults.
78
+
79
+ `min_confidence=0.8` is the practical high-signal filter: on general-purpose directories,
80
+ extension-only dataset detection and keyword-only generation-param detection emit
81
+ low-confidence (0.5–0.6) noise. Note the application root always survives the filter — it
82
+ is the scan target, not a finding.
83
+
84
+ `select` tokens are **detector IDs or tags**, not languages — `"-dataset/file"`, not
85
+ `"python"`. Run `airom detectors list` (or `airom.raw(["detectors", "list"])`) to see them.
86
+
87
+ > **Not wired yet:** pulling an image from a live registry/daemon, and live-cluster
88
+ > Kubernetes scanning. Both fail with a clear error. Use `image(input=...)` / an OCI
89
+ > layout and `k8s(manifests=...)` today.
90
+
91
+ ## Tri-state fields
92
+
93
+ `version`, `provider`, `download_location` and `release_time` are **tri-state**, and the
94
+ SDK preserves the distinction rather than collapsing it into `None`:
95
+
96
+ | JSON | Meaning | `Opt` |
97
+ |---|---|---|
98
+ | key omitted | does not apply | `Presence.ABSENT` |
99
+ | `null` | applies, but undetermined (SPDX NOASSERTION) | `Presence.UNKNOWN` |
100
+ | a value | known | `Presence.KNOWN` |
101
+
102
+ ```python
103
+ c.version.known # bool — only True when a real value is present
104
+ c.version.or_none() # value, or None (collapses absent and unknown)
105
+ c.version.or_default("-") # value, or your fallback
106
+ c.version.presence # the full distinction, when you need it
107
+ ```
108
+
109
+ ## Navigating the graph
110
+
111
+ ```python
112
+ inv.components # sorted, deterministic
113
+ inv.by_kind("vector-db", "framework")
114
+ inv.get("airom:1f3a9b2c4d5e6f70")
115
+ inv.application # the scan-root component
116
+ inv.edges_from(c.id) # typed, evidenced relationships
117
+ inv.unknowns # "looked relevant, could not process" — honesty channel
118
+ inv.stats.files_walked # requires stats=True
119
+ len(inv); [c for c in inv] # Inventory is sized and iterable
120
+ ```
121
+
122
+ ## CI gating
123
+
124
+ A `fail_on` match is a **verdict, not an error** — the scan succeeded and the AIBOM is
125
+ complete, so it is reported rather than raised:
126
+
127
+ ```python
128
+ res = airom.execute(
129
+ ["fs", "./app"],
130
+ options=airom.ScanOptions(fail_on="hosted-llm&confidence>=0.9", exit_code=7),
131
+ )
132
+ if res.policy_matched:
133
+ raise SystemExit(res.exit_code)
134
+ ```
135
+
136
+ Often you don't need `fail_on` at all — you have the whole graph, so gate in Python:
137
+
138
+ ```python
139
+ risky = [c for c in inv if c.model and c.model.pickle_risk]
140
+ if risky:
141
+ raise SystemExit(f"unsafe pickle globals in: {[c.name for c in risky]}")
142
+ ```
143
+
144
+ ## Errors
145
+
146
+ | Exception | Raised when |
147
+ |---|---|
148
+ | `BinaryNotFoundError` | the `airom` executable could not be located |
149
+ | `ScanError` | a fatal scan failure (exit 2): unreadable target, clone failure, bad flags |
150
+ | `OutputError` | no parseable AIBOM, or an unsupported `schemaVersion` |
151
+
152
+ Detector errors are **not** exceptions: they degrade to `inv.unknowns` records and the
153
+ scan still succeeds. That is AIROM's degrade-by-default contract, and the SDK preserves it.
154
+
155
+ ## The binary
156
+
157
+ The SDK shells out to the `airom` binary and decodes its native JSON — the lossless
158
+ superset every other format (CycloneDX, SARIF, YAML, table) projects from. Resolution
159
+ order:
160
+
161
+ 1. the `binary=` argument
162
+ 2. a copy bundled in the wheel (`airom/_bin/airom`)
163
+ 3. `$AIROM_BINARY`
164
+ 4. `airom` on `PATH`
165
+
166
+ Platform wheels bundle the binary, so `pip install airom` is self-contained. Installing
167
+ from an sdist does not — put `airom` on your `PATH`
168
+ (`go install github.com/airomhq/airom/cmd/airom@latest`) or set `$AIROM_BINARY`.
169
+
170
+ ## Development
171
+
172
+ ```bash
173
+ cd sdk/python
174
+ pip install -e ".[dev]"
175
+ pytest # builds the binary from the checkout and tests against it
176
+ mypy && ruff check .
177
+ ```
178
+
179
+ The suite runs against the **real binary**, not mocks: a wrapper tested only against mocks
180
+ proves nothing about the contract it wraps.
181
+
182
+ Building a wheel needs the Go toolchain (the build hook compiles the binary with
183
+ `CGO_ENABLED=0`). Set `AIROM_SKIP_BUNDLE=1` for a pure-Python wheel.
184
+
185
+ ## Publishing
186
+
187
+ Releases are published by [`.github/workflows/release-pypi.yml`](../../.github/workflows/release-pypi.yml)
188
+ using **PyPI Trusted Publishing** (OIDC): GitHub Actions authenticates to PyPI directly,
189
+ so there is no API token stored as a secret, and none to leak or rotate.
190
+
191
+ ### One-time setup
192
+
193
+ On [pypi.org/manage/account/publishing](https://pypi.org/manage/account/publishing/), add a
194
+ **pending publisher**:
195
+
196
+ | Field | Value |
197
+ |---|---|
198
+ | PyPI project name | `airom` |
199
+ | Owner | `airomhq` |
200
+ | Repository name | `airom` |
201
+ | Workflow name | `release-pypi.yml` |
202
+ | Environment | *(leave blank)* |
203
+
204
+ That is all — no secret is added to GitHub.
205
+
206
+ ### Cutting a release
207
+
208
+ The workflow runs on every push to `main` that touches the SDK or the scanner, but
209
+ **publishes only when the version is new**: PyPI permanently refuses to re-upload a
210
+ version (even a deleted one), so the workflow compares `__version__` against the index and
211
+ skips cleanly if it is already there.
212
+
213
+ So a release is exactly one deliberate act:
214
+
215
+ ```bash
216
+ # sdk/python/src/airom/__init__.py
217
+ __version__ = "0.1.0" # bump, commit, merge to main -> published
218
+ ```
219
+
220
+ <!-- Version note: a PEP 440 pre-release suffix (.dev0, rc1, …) is NOT installed by
221
+ `pip install airom` unless the user passes --pre. Use a plain version for a
222
+ release people are meant to get by default. -->
223
+
224
+ Publishing is **irreversible**: a version number is burned forever once used, and yanking
225
+ does not free it. Test the whole path first with the manual `workflow_dispatch` run against
226
+ **TestPyPI** (which needs its own pending publisher at
227
+ [test.pypi.org](https://test.pypi.org/manage/account/publishing/)).
228
+
229
+ ## License
230
+
231
+ Apache-2.0, same as AIROM.
airom-0.1.0/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # airom — Python SDK
2
+
3
+ Python SDK for [AIROM](https://github.com/airomhq/airom), the open-source **AI Bill of
4
+ Materials (AIBOM) scanner**. Discover AI assets — models, prompts, datasets, embeddings,
5
+ vector databases, frameworks, serving infrastructure — across code, containers, and
6
+ Kubernetes, and get them back as typed Python objects.
7
+
8
+ ```bash
9
+ pip install airom
10
+ ```
11
+
12
+ ## Quick start
13
+
14
+ ```python
15
+ import airom
16
+
17
+ inv = airom.fs("./my-app", min_confidence=0.8)
18
+
19
+ for c in inv.by_kind(airom.ComponentKind.HOSTED_LLM):
20
+ print(c.name, c.provider.or_default("-"), c.confidence)
21
+ for occ in c.evidence.occurrences:
22
+ print(f" {occ.location.path}:{occ.location.line} [{occ.detector_id}]")
23
+ ```
24
+
25
+ ```
26
+ gpt-4.1 openai 0.87
27
+ src/rag.py:88 [rules/openai/model-literal]
28
+ src/agent.py:12 [rules/openai/sdk-import]
29
+ ```
30
+
31
+ Every component carries the evidence that justifies it — that is the point of AIROM, and
32
+ the SDK hands you all of it.
33
+
34
+ ## Scanning
35
+
36
+ ```python
37
+ airom.scan("./app") # auto-detect: path, git URL, or image ref
38
+ airom.fs("./app") # a directory tree
39
+ airom.repo("https://github.com/o/r") # remote (shallow clone) or a local worktree
40
+ airom.image(input="img.tar") # docker save -o img.tar <ref>
41
+ airom.k8s(manifests="./deploy") # offline: enumerate workload images
42
+ airom.version() # the underlying binary's ToolInfo
43
+ ```
44
+
45
+ Common keyword args mirror the CLI flags: `select`, `rules`, `ignore`, `min_confidence`,
46
+ `max_file_size`, `io_budget`, `parallel`, `no_cache`, `cache_dir`, `offline`, `stats`,
47
+ plus `binary`, `timeout`, and `cwd`. `None` leaves the tool's own default in place — the
48
+ SDK never invents defaults.
49
+
50
+ `min_confidence=0.8` is the practical high-signal filter: on general-purpose directories,
51
+ extension-only dataset detection and keyword-only generation-param detection emit
52
+ low-confidence (0.5–0.6) noise. Note the application root always survives the filter — it
53
+ is the scan target, not a finding.
54
+
55
+ `select` tokens are **detector IDs or tags**, not languages — `"-dataset/file"`, not
56
+ `"python"`. Run `airom detectors list` (or `airom.raw(["detectors", "list"])`) to see them.
57
+
58
+ > **Not wired yet:** pulling an image from a live registry/daemon, and live-cluster
59
+ > Kubernetes scanning. Both fail with a clear error. Use `image(input=...)` / an OCI
60
+ > layout and `k8s(manifests=...)` today.
61
+
62
+ ## Tri-state fields
63
+
64
+ `version`, `provider`, `download_location` and `release_time` are **tri-state**, and the
65
+ SDK preserves the distinction rather than collapsing it into `None`:
66
+
67
+ | JSON | Meaning | `Opt` |
68
+ |---|---|---|
69
+ | key omitted | does not apply | `Presence.ABSENT` |
70
+ | `null` | applies, but undetermined (SPDX NOASSERTION) | `Presence.UNKNOWN` |
71
+ | a value | known | `Presence.KNOWN` |
72
+
73
+ ```python
74
+ c.version.known # bool — only True when a real value is present
75
+ c.version.or_none() # value, or None (collapses absent and unknown)
76
+ c.version.or_default("-") # value, or your fallback
77
+ c.version.presence # the full distinction, when you need it
78
+ ```
79
+
80
+ ## Navigating the graph
81
+
82
+ ```python
83
+ inv.components # sorted, deterministic
84
+ inv.by_kind("vector-db", "framework")
85
+ inv.get("airom:1f3a9b2c4d5e6f70")
86
+ inv.application # the scan-root component
87
+ inv.edges_from(c.id) # typed, evidenced relationships
88
+ inv.unknowns # "looked relevant, could not process" — honesty channel
89
+ inv.stats.files_walked # requires stats=True
90
+ len(inv); [c for c in inv] # Inventory is sized and iterable
91
+ ```
92
+
93
+ ## CI gating
94
+
95
+ A `fail_on` match is a **verdict, not an error** — the scan succeeded and the AIBOM is
96
+ complete, so it is reported rather than raised:
97
+
98
+ ```python
99
+ res = airom.execute(
100
+ ["fs", "./app"],
101
+ options=airom.ScanOptions(fail_on="hosted-llm&confidence>=0.9", exit_code=7),
102
+ )
103
+ if res.policy_matched:
104
+ raise SystemExit(res.exit_code)
105
+ ```
106
+
107
+ Often you don't need `fail_on` at all — you have the whole graph, so gate in Python:
108
+
109
+ ```python
110
+ risky = [c for c in inv if c.model and c.model.pickle_risk]
111
+ if risky:
112
+ raise SystemExit(f"unsafe pickle globals in: {[c.name for c in risky]}")
113
+ ```
114
+
115
+ ## Errors
116
+
117
+ | Exception | Raised when |
118
+ |---|---|
119
+ | `BinaryNotFoundError` | the `airom` executable could not be located |
120
+ | `ScanError` | a fatal scan failure (exit 2): unreadable target, clone failure, bad flags |
121
+ | `OutputError` | no parseable AIBOM, or an unsupported `schemaVersion` |
122
+
123
+ Detector errors are **not** exceptions: they degrade to `inv.unknowns` records and the
124
+ scan still succeeds. That is AIROM's degrade-by-default contract, and the SDK preserves it.
125
+
126
+ ## The binary
127
+
128
+ The SDK shells out to the `airom` binary and decodes its native JSON — the lossless
129
+ superset every other format (CycloneDX, SARIF, YAML, table) projects from. Resolution
130
+ order:
131
+
132
+ 1. the `binary=` argument
133
+ 2. a copy bundled in the wheel (`airom/_bin/airom`)
134
+ 3. `$AIROM_BINARY`
135
+ 4. `airom` on `PATH`
136
+
137
+ Platform wheels bundle the binary, so `pip install airom` is self-contained. Installing
138
+ from an sdist does not — put `airom` on your `PATH`
139
+ (`go install github.com/airomhq/airom/cmd/airom@latest`) or set `$AIROM_BINARY`.
140
+
141
+ ## Development
142
+
143
+ ```bash
144
+ cd sdk/python
145
+ pip install -e ".[dev]"
146
+ pytest # builds the binary from the checkout and tests against it
147
+ mypy && ruff check .
148
+ ```
149
+
150
+ The suite runs against the **real binary**, not mocks: a wrapper tested only against mocks
151
+ proves nothing about the contract it wraps.
152
+
153
+ Building a wheel needs the Go toolchain (the build hook compiles the binary with
154
+ `CGO_ENABLED=0`). Set `AIROM_SKIP_BUNDLE=1` for a pure-Python wheel.
155
+
156
+ ## Publishing
157
+
158
+ Releases are published by [`.github/workflows/release-pypi.yml`](../../.github/workflows/release-pypi.yml)
159
+ using **PyPI Trusted Publishing** (OIDC): GitHub Actions authenticates to PyPI directly,
160
+ so there is no API token stored as a secret, and none to leak or rotate.
161
+
162
+ ### One-time setup
163
+
164
+ On [pypi.org/manage/account/publishing](https://pypi.org/manage/account/publishing/), add a
165
+ **pending publisher**:
166
+
167
+ | Field | Value |
168
+ |---|---|
169
+ | PyPI project name | `airom` |
170
+ | Owner | `airomhq` |
171
+ | Repository name | `airom` |
172
+ | Workflow name | `release-pypi.yml` |
173
+ | Environment | *(leave blank)* |
174
+
175
+ That is all — no secret is added to GitHub.
176
+
177
+ ### Cutting a release
178
+
179
+ The workflow runs on every push to `main` that touches the SDK or the scanner, but
180
+ **publishes only when the version is new**: PyPI permanently refuses to re-upload a
181
+ version (even a deleted one), so the workflow compares `__version__` against the index and
182
+ skips cleanly if it is already there.
183
+
184
+ So a release is exactly one deliberate act:
185
+
186
+ ```bash
187
+ # sdk/python/src/airom/__init__.py
188
+ __version__ = "0.1.0" # bump, commit, merge to main -> published
189
+ ```
190
+
191
+ <!-- Version note: a PEP 440 pre-release suffix (.dev0, rc1, …) is NOT installed by
192
+ `pip install airom` unless the user passes --pre. Use a plain version for a
193
+ release people are meant to get by default. -->
194
+
195
+ Publishing is **irreversible**: a version number is burned forever once used, and yanking
196
+ does not free it. Test the whole path first with the manual `workflow_dispatch` run against
197
+ **TestPyPI** (which needs its own pending publisher at
198
+ [test.pypi.org](https://test.pypi.org/manage/account/publishing/)).
199
+
200
+ ## License
201
+
202
+ Apache-2.0, same as AIROM.
@@ -0,0 +1,86 @@
1
+ """Build hook: compile the ``airom`` binary into the wheel.
2
+
3
+ Wheels are platform-specific because they carry a compiled Go binary, so this
4
+ hook also stamps the wheel tag. It needs the Go toolchain and the repository
5
+ checkout (the module root is three levels up from this file).
6
+
7
+ Opt out with ``AIROM_SKIP_BUNDLE=1`` — the resulting wheel is pure-Python and
8
+ falls back to ``$AIROM_BINARY`` or ``airom`` on ``PATH`` at runtime.
9
+
10
+ Cross-compile by setting ``GOOS``/``GOARCH`` (both are forwarded to ``go
11
+ build``); set ``AIROM_WHEEL_TAG`` to override the platform tag when doing so.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import shutil
18
+ import subprocess
19
+ import sys
20
+ import sysconfig
21
+ from pathlib import Path
22
+
23
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
24
+
25
+ HERE = Path(__file__).parent
26
+ # sdk/python/hatch_build.py -> sdk/python -> sdk -> <repo root>
27
+ REPO_ROOT = HERE.parent.parent
28
+ BIN_DIR = HERE / "src" / "airom" / "_bin"
29
+
30
+
31
+ def _exe_name() -> str:
32
+ goos = os.environ.get("GOOS") or sys.platform
33
+ return "airom.exe" if goos in ("win32", "windows") else "airom"
34
+
35
+
36
+ def _wheel_tag() -> str:
37
+ if tag := os.environ.get("AIROM_WHEEL_TAG"):
38
+ return tag
39
+ # Not pure-Python, but ABI-independent: the payload is a standalone binary,
40
+ # so the wheel works on any CPython for this platform.
41
+ plat = sysconfig.get_platform().replace("-", "_").replace(".", "_")
42
+ return f"py3-none-{plat}"
43
+
44
+
45
+ class AiromBuildHook(BuildHookInterface):
46
+ PLUGIN_NAME = "custom"
47
+
48
+ def initialize(self, version: str, build_data: dict) -> None:
49
+ if self.target_name != "wheel":
50
+ return
51
+
52
+ if os.environ.get("AIROM_SKIP_BUNDLE"):
53
+ self.app.display_waiting("AIROM_SKIP_BUNDLE set — building a pure-Python wheel")
54
+ return
55
+
56
+ if not (REPO_ROOT / "go.mod").is_file():
57
+ self.app.display_warning(
58
+ f"no go.mod under {REPO_ROOT} — building without a bundled binary "
59
+ "(the SDK will fall back to $AIROM_BINARY or PATH)"
60
+ )
61
+ return
62
+
63
+ if shutil.which("go") is None:
64
+ self.app.display_warning(
65
+ "the Go toolchain was not found — building without a bundled binary "
66
+ "(set AIROM_SKIP_BUNDLE=1 to silence this)"
67
+ )
68
+ return
69
+
70
+ BIN_DIR.mkdir(parents=True, exist_ok=True)
71
+ out = BIN_DIR / _exe_name()
72
+
73
+ env = dict(os.environ)
74
+ env["CGO_ENABLED"] = "0" # invariant P8: the release binary is always static
75
+
76
+ cmd = ["go", "build", "-trimpath", "-ldflags", "-s -w", "-o", str(out), "./cmd/airom"]
77
+ self.app.display_info(f"bundling airom: {' '.join(cmd)} (in {REPO_ROOT})")
78
+ try:
79
+ subprocess.run(cmd, cwd=REPO_ROOT, env=env, check=True)
80
+ except subprocess.CalledProcessError as e:
81
+ raise RuntimeError(f"failed to build the airom binary: {e}") from e
82
+
83
+ out.chmod(0o755)
84
+ build_data["pure_python"] = False
85
+ build_data["tag"] = _wheel_tag()
86
+ build_data["artifacts"].append("src/airom/_bin/*")
@@ -0,0 +1,71 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "airom"
7
+ dynamic = ["version"]
8
+ description = "Python SDK for AIROM — the open-source AI Bill of Materials (AIBOM) scanner"
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "AIROM Authors" }]
13
+ keywords = ["aibom", "sbom", "ai", "security", "cyclonedx", "sarif", "supply-chain"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Security",
24
+ "Topic :: Software Development :: Quality Assurance",
25
+ "Typing :: Typed",
26
+ ]
27
+ # Deliberately dependency-free: the SDK is a typed wrapper over a static binary.
28
+ dependencies = []
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/airomhq/airom"
32
+ Documentation = "https://github.com/airomhq/airom/tree/main/docs-site"
33
+ Source = "https://github.com/airomhq/airom"
34
+ Issues = "https://github.com/airomhq/airom/issues"
35
+
36
+ [project.optional-dependencies]
37
+ dev = ["pytest>=7", "mypy>=1.8", "ruff>=0.5"]
38
+
39
+ [tool.hatch.version]
40
+ path = "src/airom/__init__.py"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/airom"]
44
+ # The bundled binary is produced by hatch_build.py; ship it if it is there.
45
+ artifacts = ["src/airom/_bin/*"]
46
+
47
+ [tool.hatch.build.targets.sdist]
48
+ # The sdist carries no binary — installing from it falls back to $AIROM_BINARY
49
+ # or `airom` on PATH.
50
+ exclude = ["src/airom/_bin"]
51
+
52
+ [tool.hatch.build.hooks.custom]
53
+ path = "hatch_build.py"
54
+
55
+ [tool.pytest.ini_options]
56
+ testpaths = ["tests"]
57
+ addopts = "-q"
58
+
59
+ [tool.ruff]
60
+ line-length = 100
61
+ src = ["src"]
62
+
63
+ [tool.ruff.lint]
64
+ select = ["E", "F", "I", "UP", "B", "SIM"]
65
+
66
+ [tool.mypy]
67
+ python_version = "3.10"
68
+ packages = ["airom"]
69
+ mypy_path = "src"
70
+ strict = true
71
+ warn_unused_ignores = false