jfrog-xray 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.
- jfrog_xray-0.1.0/.gitignore +20 -0
- jfrog_xray-0.1.0/.idea/.gitignore +3 -0
- jfrog_xray-0.1.0/.idea/workspace.xml +4 -0
- jfrog_xray-0.1.0/LICENSE +21 -0
- jfrog_xray-0.1.0/PKG-INFO +162 -0
- jfrog_xray-0.1.0/README.md +140 -0
- jfrog_xray-0.1.0/pyproject.toml +71 -0
- jfrog_xray-0.1.0/src/jfrog_xray/__init__.py +72 -0
- jfrog_xray-0.1.0/src/jfrog_xray/_auth.py +48 -0
- jfrog_xray-0.1.0/src/jfrog_xray/_base_client.py +178 -0
- jfrog_xray-0.1.0/src/jfrog_xray/_client.py +139 -0
- jfrog_xray-0.1.0/src/jfrog_xray/_config.py +42 -0
- jfrog_xray-0.1.0/src/jfrog_xray/_exceptions.py +178 -0
- jfrog_xray-0.1.0/src/jfrog_xray/_pagination.py +117 -0
- jfrog_xray-0.1.0/src/jfrog_xray/_types.py +32 -0
- jfrog_xray-0.1.0/src/jfrog_xray/_version.py +5 -0
- jfrog_xray-0.1.0/src/jfrog_xray/models/__init__.py +52 -0
- jfrog_xray-0.1.0/src/jfrog_xray/models/common.py +86 -0
- jfrog_xray-0.1.0/src/jfrog_xray/models/components.py +36 -0
- jfrog_xray-0.1.0/src/jfrog_xray/models/scans.py +26 -0
- jfrog_xray-0.1.0/src/jfrog_xray/models/summaries.py +77 -0
- jfrog_xray-0.1.0/src/jfrog_xray/models/violations.py +34 -0
- jfrog_xray-0.1.0/src/jfrog_xray/models/vulnerabilities.py +33 -0
- jfrog_xray-0.1.0/src/jfrog_xray/py.typed +0 -0
- jfrog_xray-0.1.0/src/jfrog_xray/resources/__init__.py +21 -0
- jfrog_xray-0.1.0/src/jfrog_xray/resources/_base.py +12 -0
- jfrog_xray-0.1.0/src/jfrog_xray/resources/artifacts.py +123 -0
- jfrog_xray-0.1.0/src/jfrog_xray/resources/components.py +86 -0
- jfrog_xray-0.1.0/src/jfrog_xray/resources/licenses.py +19 -0
- jfrog_xray-0.1.0/src/jfrog_xray/resources/scans.py +52 -0
- jfrog_xray-0.1.0/src/jfrog_xray/resources/summaries.py +50 -0
- jfrog_xray-0.1.0/src/jfrog_xray/resources/system.py +19 -0
- jfrog_xray-0.1.0/src/jfrog_xray/resources/violations.py +93 -0
- jfrog_xray-0.1.0/tests/__init__.py +0 -0
- jfrog_xray-0.1.0/tests/conftest.py +22 -0
- jfrog_xray-0.1.0/tests/test_artifacts.py +117 -0
- jfrog_xray-0.1.0/tests/test_client.py +67 -0
- jfrog_xray-0.1.0/tests/test_errors.py +64 -0
- jfrog_xray-0.1.0/tests/test_integration.py +154 -0
- jfrog_xray-0.1.0/tests/test_pagination.py +73 -0
- jfrog_xray-0.1.0/tests/test_retries.py +58 -0
- jfrog_xray-0.1.0/uv.lock +486 -0
jfrog_xray-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xray-api contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: jfrog-xray
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A modern, typed Python client for the JFrog Xray REST API (read side).
|
|
5
|
+
Project-URL: Homepage, https://github.com/helic0ptr/jfrog-xray
|
|
6
|
+
Project-URL: Repository, https://github.com/helic0ptr/jfrog-xray
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: artifactory,cve,jfrog,sbom,sca,security,xray
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.14
|
|
19
|
+
Requires-Dist: httpx>=0.27
|
|
20
|
+
Requires-Dist: pydantic>=2.7
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# jfrog-xray
|
|
24
|
+
|
|
25
|
+
A modern, typed Python client for the **JFrog Xray REST API** — read side.
|
|
26
|
+
|
|
27
|
+
It focuses on the questions teams actually ask of Xray on Artifactory: *what
|
|
28
|
+
are the CVEs for this artifact?*, *what violations do we have?*, and the
|
|
29
|
+
summary / scan-status / license lookups around them. Responses are parsed into
|
|
30
|
+
[pydantic](https://docs.pydantic.dev) models, list endpoints auto-paginate,
|
|
31
|
+
transient failures are retried, and HTTP errors surface as typed exceptions.
|
|
32
|
+
|
|
33
|
+
> Import name is `jfrog_xray`. Sync client today; the core is structured so an
|
|
34
|
+
> async client can be added without a rewrite.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv add jfrog-xray # or: pip install jfrog-xray
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from jfrog_xray import XrayClient
|
|
46
|
+
|
|
47
|
+
# base_url is the JFrog Platform root; token is a Bearer access token.
|
|
48
|
+
# Both fall back to env vars: XRAY_URL / XRAY_BASE_URL and XRAY_TOKEN.
|
|
49
|
+
with XrayClient(base_url="https://acme.jfrog.io", token="...") as x:
|
|
50
|
+
x.system.ping() # {"status": "pong"}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Headline: CVEs for an artifact
|
|
54
|
+
|
|
55
|
+
Get the vulnerabilities for a single artifact as a typed list (via Summary v2):
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
for cve in x.artifacts.vulnerabilities(repo="docker-local",
|
|
59
|
+
path="nginx/1.25/manifest.json"):
|
|
60
|
+
print(cve.cve, cve.severity, cve.component, cve.fixed_versions)
|
|
61
|
+
|
|
62
|
+
# ...or identify the artifact by checksum:
|
|
63
|
+
x.artifacts.vulnerabilities(sha256="9f6c...")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or download a full report / SBOM (ZIP) for it:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
x.artifacts.export(
|
|
70
|
+
component_name="nginx",
|
|
71
|
+
package_type="docker",
|
|
72
|
+
vulnerabilities=True,
|
|
73
|
+
sbom="cyclonedx", # or "spdx"
|
|
74
|
+
out="nginx-report.zip",
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Other read APIs
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# Violations — the returned page auto-paginates when iterated.
|
|
82
|
+
for v in x.violations.list(watch_name="prod", min_severity="High"):
|
|
83
|
+
print(v.issue_id, v.severity, v.violation_details_url)
|
|
84
|
+
|
|
85
|
+
# Summaries
|
|
86
|
+
summary = x.summaries.artifact(paths=["docker-local/nginx/1.25/manifest.json"])
|
|
87
|
+
build = x.summaries.build(build_name="my-app", build_number="42")
|
|
88
|
+
|
|
89
|
+
# CVE / component lookups
|
|
90
|
+
x.components.search_by_cves(["CVE-2023-0001"])
|
|
91
|
+
x.components.search_cves_by_components(["gav://com.example:app:1.0.0"])
|
|
92
|
+
for r in x.components.impacted_resources(vulnerability="CVE-2023-0001"):
|
|
93
|
+
print(r.repository, r.path)
|
|
94
|
+
|
|
95
|
+
# Scan status & licenses
|
|
96
|
+
x.scans.artifact_status(repo="docker-local", path="nginx/1.25/manifest.json")
|
|
97
|
+
x.licenses.list()
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
XrayClient(
|
|
104
|
+
base_url=..., # or XRAY_URL / XRAY_BASE_URL
|
|
105
|
+
token=..., # or XRAY_TOKEN; alternatively auth=<httpx.Auth>
|
|
106
|
+
timeout=30.0, # float seconds or httpx.Timeout
|
|
107
|
+
max_retries=2, # connection/timeout/5xx/429, honoring Retry-After
|
|
108
|
+
http_client=..., # inject a preconfigured httpx.Client
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Per-call overrides (shares the same underlying HTTP client):
|
|
112
|
+
x.with_options(timeout=5.0, max_retries=0).system.ping()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Errors
|
|
116
|
+
|
|
117
|
+
All errors derive from `XrayError`. HTTP failures raise an `APIStatusError`
|
|
118
|
+
subclass carrying `.status_code`, `.response`, and `.body`:
|
|
119
|
+
|
|
120
|
+
`BadRequestError` (400), `AuthenticationError` (401), `PermissionDeniedError`
|
|
121
|
+
(403), `NotFoundError` (404), `ConflictError` (409), `UnprocessableEntityError`
|
|
122
|
+
(422), `RateLimitError` (429, with `.retry_after`), `InternalServerError` (5xx).
|
|
123
|
+
Network problems raise `APIConnectionError` / `APITimeoutError`.
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
uv sync
|
|
129
|
+
uv run ruff check src tests
|
|
130
|
+
uv run mypy src
|
|
131
|
+
uv run pytest # unit tests (respx-mocked; no network)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Integration tests
|
|
135
|
+
|
|
136
|
+
Live tests in `tests/test_integration.py` run read-only against a real JFrog
|
|
137
|
+
Platform. They **skip** unless `XRAY_URL` (or `XRAY_BASE_URL`) and `XRAY_TOKEN`
|
|
138
|
+
are set; individual tests skip when their resource env var is absent.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
export XRAY_URL="https://acme.jfrog.io"
|
|
142
|
+
export XRAY_TOKEN="..."
|
|
143
|
+
# optional, to exercise resource-specific tests:
|
|
144
|
+
export XRAY_TEST_ARTIFACT_PATH="docker-local/nginx/1.25/manifest.json"
|
|
145
|
+
export XRAY_TEST_CVE="CVE-2021-44228"
|
|
146
|
+
# ...see the module docstring for the full list
|
|
147
|
+
|
|
148
|
+
uv run pytest -m integration
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Scope
|
|
152
|
+
|
|
153
|
+
**In v1 (read side):** system, summaries, violations, CVE/component lookups,
|
|
154
|
+
scan status, licenses, and the `artifacts` convenience resource.
|
|
155
|
+
|
|
156
|
+
**Deferred:** async client, the async Reports API (create → poll → paginated
|
|
157
|
+
content → delete), governance reads (watches / policies / ignore rules), and
|
|
158
|
+
all write-side actions.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# jfrog-xray
|
|
2
|
+
|
|
3
|
+
A modern, typed Python client for the **JFrog Xray REST API** — read side.
|
|
4
|
+
|
|
5
|
+
It focuses on the questions teams actually ask of Xray on Artifactory: *what
|
|
6
|
+
are the CVEs for this artifact?*, *what violations do we have?*, and the
|
|
7
|
+
summary / scan-status / license lookups around them. Responses are parsed into
|
|
8
|
+
[pydantic](https://docs.pydantic.dev) models, list endpoints auto-paginate,
|
|
9
|
+
transient failures are retried, and HTTP errors surface as typed exceptions.
|
|
10
|
+
|
|
11
|
+
> Import name is `jfrog_xray`. Sync client today; the core is structured so an
|
|
12
|
+
> async client can be added without a rewrite.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv add jfrog-xray # or: pip install jfrog-xray
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from jfrog_xray import XrayClient
|
|
24
|
+
|
|
25
|
+
# base_url is the JFrog Platform root; token is a Bearer access token.
|
|
26
|
+
# Both fall back to env vars: XRAY_URL / XRAY_BASE_URL and XRAY_TOKEN.
|
|
27
|
+
with XrayClient(base_url="https://acme.jfrog.io", token="...") as x:
|
|
28
|
+
x.system.ping() # {"status": "pong"}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Headline: CVEs for an artifact
|
|
32
|
+
|
|
33
|
+
Get the vulnerabilities for a single artifact as a typed list (via Summary v2):
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
for cve in x.artifacts.vulnerabilities(repo="docker-local",
|
|
37
|
+
path="nginx/1.25/manifest.json"):
|
|
38
|
+
print(cve.cve, cve.severity, cve.component, cve.fixed_versions)
|
|
39
|
+
|
|
40
|
+
# ...or identify the artifact by checksum:
|
|
41
|
+
x.artifacts.vulnerabilities(sha256="9f6c...")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or download a full report / SBOM (ZIP) for it:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
x.artifacts.export(
|
|
48
|
+
component_name="nginx",
|
|
49
|
+
package_type="docker",
|
|
50
|
+
vulnerabilities=True,
|
|
51
|
+
sbom="cyclonedx", # or "spdx"
|
|
52
|
+
out="nginx-report.zip",
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Other read APIs
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
# Violations — the returned page auto-paginates when iterated.
|
|
60
|
+
for v in x.violations.list(watch_name="prod", min_severity="High"):
|
|
61
|
+
print(v.issue_id, v.severity, v.violation_details_url)
|
|
62
|
+
|
|
63
|
+
# Summaries
|
|
64
|
+
summary = x.summaries.artifact(paths=["docker-local/nginx/1.25/manifest.json"])
|
|
65
|
+
build = x.summaries.build(build_name="my-app", build_number="42")
|
|
66
|
+
|
|
67
|
+
# CVE / component lookups
|
|
68
|
+
x.components.search_by_cves(["CVE-2023-0001"])
|
|
69
|
+
x.components.search_cves_by_components(["gav://com.example:app:1.0.0"])
|
|
70
|
+
for r in x.components.impacted_resources(vulnerability="CVE-2023-0001"):
|
|
71
|
+
print(r.repository, r.path)
|
|
72
|
+
|
|
73
|
+
# Scan status & licenses
|
|
74
|
+
x.scans.artifact_status(repo="docker-local", path="nginx/1.25/manifest.json")
|
|
75
|
+
x.licenses.list()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
XrayClient(
|
|
82
|
+
base_url=..., # or XRAY_URL / XRAY_BASE_URL
|
|
83
|
+
token=..., # or XRAY_TOKEN; alternatively auth=<httpx.Auth>
|
|
84
|
+
timeout=30.0, # float seconds or httpx.Timeout
|
|
85
|
+
max_retries=2, # connection/timeout/5xx/429, honoring Retry-After
|
|
86
|
+
http_client=..., # inject a preconfigured httpx.Client
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Per-call overrides (shares the same underlying HTTP client):
|
|
90
|
+
x.with_options(timeout=5.0, max_retries=0).system.ping()
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Errors
|
|
94
|
+
|
|
95
|
+
All errors derive from `XrayError`. HTTP failures raise an `APIStatusError`
|
|
96
|
+
subclass carrying `.status_code`, `.response`, and `.body`:
|
|
97
|
+
|
|
98
|
+
`BadRequestError` (400), `AuthenticationError` (401), `PermissionDeniedError`
|
|
99
|
+
(403), `NotFoundError` (404), `ConflictError` (409), `UnprocessableEntityError`
|
|
100
|
+
(422), `RateLimitError` (429, with `.retry_after`), `InternalServerError` (5xx).
|
|
101
|
+
Network problems raise `APIConnectionError` / `APITimeoutError`.
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
uv sync
|
|
107
|
+
uv run ruff check src tests
|
|
108
|
+
uv run mypy src
|
|
109
|
+
uv run pytest # unit tests (respx-mocked; no network)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Integration tests
|
|
113
|
+
|
|
114
|
+
Live tests in `tests/test_integration.py` run read-only against a real JFrog
|
|
115
|
+
Platform. They **skip** unless `XRAY_URL` (or `XRAY_BASE_URL`) and `XRAY_TOKEN`
|
|
116
|
+
are set; individual tests skip when their resource env var is absent.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
export XRAY_URL="https://acme.jfrog.io"
|
|
120
|
+
export XRAY_TOKEN="..."
|
|
121
|
+
# optional, to exercise resource-specific tests:
|
|
122
|
+
export XRAY_TEST_ARTIFACT_PATH="docker-local/nginx/1.25/manifest.json"
|
|
123
|
+
export XRAY_TEST_CVE="CVE-2021-44228"
|
|
124
|
+
# ...see the module docstring for the full list
|
|
125
|
+
|
|
126
|
+
uv run pytest -m integration
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Scope
|
|
130
|
+
|
|
131
|
+
**In v1 (read side):** system, summaries, violations, CVE/component lookups,
|
|
132
|
+
scan status, licenses, and the `artifacts` convenience resource.
|
|
133
|
+
|
|
134
|
+
**Deferred:** async client, the async Reports API (create → poll → paginated
|
|
135
|
+
content → delete), governance reads (watches / policies / ignore rules), and
|
|
136
|
+
all write-side actions.
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "jfrog-xray"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A modern, typed Python client for the JFrog Xray REST API (read side)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.14"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["jfrog", "xray", "artifactory", "security", "sca", "cve", "sbom"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.14",
|
|
19
|
+
"Topic :: Security",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"httpx>=0.27",
|
|
25
|
+
"pydantic>=2.7",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/helic0ptr/jfrog-xray"
|
|
30
|
+
Repository = "https://github.com/helic0ptr/jfrog-xray"
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.2",
|
|
35
|
+
"respx>=0.21",
|
|
36
|
+
"anyio>=4.4",
|
|
37
|
+
"ruff>=0.6",
|
|
38
|
+
"mypy>=1.11",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["src/jfrog_xray"]
|
|
43
|
+
|
|
44
|
+
[tool.ruff]
|
|
45
|
+
line-length = 100
|
|
46
|
+
src = ["src", "tests"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint]
|
|
49
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "C4", "RUF"]
|
|
50
|
+
ignore = ["B008"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint.per-file-ignores]
|
|
53
|
+
"tests/**" = ["S101"]
|
|
54
|
+
|
|
55
|
+
[tool.mypy]
|
|
56
|
+
python_version = "3.14"
|
|
57
|
+
strict = true
|
|
58
|
+
plugins = ["pydantic.mypy"]
|
|
59
|
+
files = ["src"]
|
|
60
|
+
|
|
61
|
+
[tool.pydantic-mypy]
|
|
62
|
+
init_forbid_extra = true
|
|
63
|
+
init_typed = true
|
|
64
|
+
warn_required_dynamic_aliases = true
|
|
65
|
+
|
|
66
|
+
[tool.pytest.ini_options]
|
|
67
|
+
testpaths = ["tests"]
|
|
68
|
+
addopts = "-ra"
|
|
69
|
+
markers = [
|
|
70
|
+
"integration: tests that hit a real JFrog Platform (require XRAY_URL/XRAY_TOKEN)",
|
|
71
|
+
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""A modern, typed Python client for the JFrog Xray REST API (read side)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._auth import ApiKeyAuth, BearerAuth
|
|
6
|
+
from ._client import XrayClient
|
|
7
|
+
from ._exceptions import (
|
|
8
|
+
APIConnectionError,
|
|
9
|
+
APIStatusError,
|
|
10
|
+
APITimeoutError,
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
BadRequestError,
|
|
13
|
+
ConflictError,
|
|
14
|
+
InternalServerError,
|
|
15
|
+
NotFoundError,
|
|
16
|
+
PermissionDeniedError,
|
|
17
|
+
RateLimitError,
|
|
18
|
+
UnprocessableEntityError,
|
|
19
|
+
XrayError,
|
|
20
|
+
)
|
|
21
|
+
from ._pagination import SyncPage
|
|
22
|
+
from ._version import __version__
|
|
23
|
+
from .models import (
|
|
24
|
+
ArtifactSummary,
|
|
25
|
+
BuildSummary,
|
|
26
|
+
Component,
|
|
27
|
+
ComponentDetail,
|
|
28
|
+
Cve,
|
|
29
|
+
CveSearchResult,
|
|
30
|
+
IgnoredViolation,
|
|
31
|
+
ImpactedResource,
|
|
32
|
+
License,
|
|
33
|
+
PackageType,
|
|
34
|
+
Severity,
|
|
35
|
+
Violation,
|
|
36
|
+
Vulnerability,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [ # noqa: RUF022 -- grouped by category for readability
|
|
40
|
+
"__version__",
|
|
41
|
+
"XrayClient",
|
|
42
|
+
"BearerAuth",
|
|
43
|
+
"ApiKeyAuth",
|
|
44
|
+
"SyncPage",
|
|
45
|
+
# errors
|
|
46
|
+
"XrayError",
|
|
47
|
+
"APIConnectionError",
|
|
48
|
+
"APITimeoutError",
|
|
49
|
+
"APIStatusError",
|
|
50
|
+
"BadRequestError",
|
|
51
|
+
"AuthenticationError",
|
|
52
|
+
"PermissionDeniedError",
|
|
53
|
+
"NotFoundError",
|
|
54
|
+
"ConflictError",
|
|
55
|
+
"UnprocessableEntityError",
|
|
56
|
+
"RateLimitError",
|
|
57
|
+
"InternalServerError",
|
|
58
|
+
# models
|
|
59
|
+
"Severity",
|
|
60
|
+
"PackageType",
|
|
61
|
+
"Cve",
|
|
62
|
+
"Component",
|
|
63
|
+
"License",
|
|
64
|
+
"ArtifactSummary",
|
|
65
|
+
"BuildSummary",
|
|
66
|
+
"Violation",
|
|
67
|
+
"IgnoredViolation",
|
|
68
|
+
"ComponentDetail",
|
|
69
|
+
"CveSearchResult",
|
|
70
|
+
"ImpactedResource",
|
|
71
|
+
"Vulnerability",
|
|
72
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Authentication flows for the Xray client.
|
|
2
|
+
|
|
3
|
+
Bearer access tokens are first-class (JFrog's recommended method), but auth is
|
|
4
|
+
pluggable: any :class:`httpx.Auth` can be supplied instead. Auth classes are
|
|
5
|
+
I/O-free so a single instance works for both the sync and (future) async client.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Generator
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
__all__ = ["ApiKeyAuth", "BearerAuth", "resolve_auth"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BearerAuth(httpx.Auth):
|
|
18
|
+
"""Adds ``Authorization: Bearer <token>`` to every request."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, token: str) -> None:
|
|
21
|
+
self._token = token
|
|
22
|
+
|
|
23
|
+
def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Response]:
|
|
24
|
+
request.headers["Authorization"] = f"Bearer {self._token}"
|
|
25
|
+
yield request
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ApiKeyAuth(httpx.Auth):
|
|
29
|
+
"""Adds a legacy JFrog API-key header (``X-JFrog-Art-Api`` by default)."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, api_key: str, *, header: str = "X-JFrog-Art-Api") -> None:
|
|
32
|
+
self._api_key = api_key
|
|
33
|
+
self._header = header
|
|
34
|
+
|
|
35
|
+
def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Response]:
|
|
36
|
+
request.headers[self._header] = self._api_key
|
|
37
|
+
yield request
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resolve_auth(*, token: str | None, auth: httpx.Auth | None) -> httpx.Auth | None:
|
|
41
|
+
"""Turn the ``token`` / ``auth`` constructor arguments into an ``httpx.Auth``."""
|
|
42
|
+
if token is not None and auth is not None:
|
|
43
|
+
raise ValueError("Pass either `token=` or `auth=`, not both.")
|
|
44
|
+
if auth is not None:
|
|
45
|
+
return auth
|
|
46
|
+
if token is not None:
|
|
47
|
+
return BearerAuth(token)
|
|
48
|
+
return None
|