techrecon 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.
- techrecon-0.1.0/PKG-INFO +232 -0
- techrecon-0.1.0/README.md +209 -0
- techrecon-0.1.0/pyproject.toml +42 -0
- techrecon-0.1.0/setup.cfg +4 -0
- techrecon-0.1.0/src/techrecon/__init__.py +41 -0
- techrecon-0.1.0/src/techrecon/_transport.py +51 -0
- techrecon-0.1.0/src/techrecon/client.py +473 -0
- techrecon-0.1.0/src/techrecon/exceptions.py +98 -0
- techrecon-0.1.0/src/techrecon/models.py +482 -0
- techrecon-0.1.0/src/techrecon.egg-info/PKG-INFO +232 -0
- techrecon-0.1.0/src/techrecon.egg-info/SOURCES.txt +13 -0
- techrecon-0.1.0/src/techrecon.egg-info/dependency_links.txt +1 -0
- techrecon-0.1.0/src/techrecon.egg-info/requires.txt +3 -0
- techrecon-0.1.0/src/techrecon.egg-info/top_level.txt +1 -0
- techrecon-0.1.0/tests/test_client.py +241 -0
techrecon-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: techrecon
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the TechRecon JA4 + web-crawl technology intelligence API
|
|
5
|
+
Author: TechRecon
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://api.techrecon.io
|
|
8
|
+
Project-URL: Documentation, https://api.techrecon.io/v1/docs
|
|
9
|
+
Keywords: techrecon,ja4,technology-detection,fingerprinting
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Internet
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
23
|
+
|
|
24
|
+
# techrecon (Python client)
|
|
25
|
+
|
|
26
|
+
Python client for the [TechRecon](https://api.techrecon.io) JA4 fingerprinting +
|
|
27
|
+
web-crawl technology intelligence API. Generated from `docs/openapi.yaml`
|
|
28
|
+
(spec version 1.1.0).
|
|
29
|
+
|
|
30
|
+
- **Zero runtime dependencies** — built on `urllib.request` from the standard
|
|
31
|
+
library only.
|
|
32
|
+
- **Typed** — one method per endpoint, `TypedDict` response shapes, typed
|
|
33
|
+
exceptions per HTTP status code.
|
|
34
|
+
- Requires Python **3.10+**.
|
|
35
|
+
|
|
36
|
+
> **Not yet published to PyPI.** This package is prepared but not released;
|
|
37
|
+
> see the "Publishing" note in the PR description for what's still a human
|
|
38
|
+
> decision (final package name, PyPI upload).
|
|
39
|
+
|
|
40
|
+
## Install (once published)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install techrecon
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Until then, install from a local checkout:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cd sdk/python
|
|
50
|
+
pip install -e .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 5-minute quickstart
|
|
54
|
+
|
|
55
|
+
### 1. Get an API key
|
|
56
|
+
|
|
57
|
+
Every endpoint except health checks and docs requires an API key, sent as
|
|
58
|
+
the `X-API-Key` header. Get one from your TechRecon account
|
|
59
|
+
(`POST /v1/account/keys`), or ask your TechRecon contact for one.
|
|
60
|
+
|
|
61
|
+
### 2. Create a client
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from techrecon import TechRecon
|
|
65
|
+
|
|
66
|
+
client = TechRecon(api_key="<YOUR_TECHRECON_API_KEY>")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Point at a local dev server instead of production with `base_url`:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
client = TechRecon(api_key="<YOUR_TECHRECON_API_KEY>", base_url="http://localhost:8080")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. Your first lookup
|
|
76
|
+
|
|
77
|
+
Look up a domain's detected technology stack:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
domain = client.get_domain("wordpress.org")
|
|
81
|
+
print(domain["tech_count"], "technologies detected")
|
|
82
|
+
for tech in domain["technologies"]:
|
|
83
|
+
print(f" {tech['technology']} ({tech['confidence']:.2f} confidence)")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
4 technologies detected
|
|
88
|
+
WordPress (0.99 confidence)
|
|
89
|
+
PHP (0.95 confidence)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Or look up a JA4 TLS fingerprint directly:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
result = client.lookup("t13d1516h2_8daaf6152771_b0da82dd1658", ip="93.184.216.34")
|
|
96
|
+
if result["ja4_tech_match"]:
|
|
97
|
+
print(result["ja4_tech_match"]["technology"]) # "nginx"
|
|
98
|
+
else:
|
|
99
|
+
print("no mapping for this fingerprint yet")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 4. Handle errors
|
|
103
|
+
|
|
104
|
+
Every non-2xx response raises a typed exception carrying the status code
|
|
105
|
+
and the API's `{"error": "..."}` message:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from techrecon import NotFoundError, RateLimitError, TechRecon
|
|
109
|
+
|
|
110
|
+
client = TechRecon(api_key="trk_live_...")
|
|
111
|
+
try:
|
|
112
|
+
client.get_domain("does-not-exist.example")
|
|
113
|
+
except NotFoundError as exc:
|
|
114
|
+
print(f"not found: {exc.message}")
|
|
115
|
+
except RateLimitError as exc:
|
|
116
|
+
print(f"rate limited (429): {exc.message}")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
See [Exceptions](#exceptions) below for the full hierarchy.
|
|
120
|
+
|
|
121
|
+
### 5. That's it
|
|
122
|
+
|
|
123
|
+
You now have a working client. See [Endpoints covered](#endpoints-covered)
|
|
124
|
+
below for the full method list, or browse the interactive spec at
|
|
125
|
+
`GET /v1/docs` on any TechRecon server.
|
|
126
|
+
|
|
127
|
+
## More examples
|
|
128
|
+
|
|
129
|
+
**Bulk domain lookup** (up to 100 domains per call):
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
result = client.bulk_domain_lookup(["example.com", "wordpress.org"])
|
|
133
|
+
for r in result["results"]:
|
|
134
|
+
print(r["domain"], r["tech_count"])
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Search by technology:**
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
page = client.search(technology="WordPress", min_confidence=0.8, limit=50)
|
|
141
|
+
for hit in page["results"]:
|
|
142
|
+
print(hit["domain"])
|
|
143
|
+
if page["has_more"]:
|
|
144
|
+
next_page = client.search(technology="WordPress", cursor=page["next_cursor"])
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Change feed** (technology adds/removes/updates in the last N days):
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
changes = client.get_changes(since="2026-06-01T00:00:00Z", technology="WordPress")
|
|
151
|
+
for event in changes["changes"]:
|
|
152
|
+
print(event["domain"], event["change_type"], event["technology"])
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Reverse pivot from a JA4 hash to the domains that present it:**
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
page = client.pivot_ja4("t13d1516h2_8daaf6152771_02713d6af862")
|
|
159
|
+
print(page["summary"]["total_domains"], "domains observed with this fingerprint")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Create a Slack alert on any technology change for a domain:**
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
alert = client.create_alert(
|
|
166
|
+
channel="slack",
|
|
167
|
+
endpoint="https://hooks.slack.com/services/XXX/YYY/ZZZ",
|
|
168
|
+
domain="example.com",
|
|
169
|
+
)
|
|
170
|
+
print(alert["id"])
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Endpoints covered
|
|
174
|
+
|
|
175
|
+
This client covers the stable, public surface of `docs/openapi.yaml`.
|
|
176
|
+
Endpoints marked `x-internal: true` in the spec — self-serve
|
|
177
|
+
signup/email-verification (beta, not GA), Stripe billing (beta, being
|
|
178
|
+
replaced by the NAF gateway), and the privacy admin routes — are
|
|
179
|
+
intentionally **not** wrapped here; they're beta or admin-only per the
|
|
180
|
+
spec's own descriptions. Everything else is covered:
|
|
181
|
+
|
|
182
|
+
| Area | Methods |
|
|
183
|
+
| -------------- | -------------------------------------------------------------------------------------- |
|
|
184
|
+
| Health | `health`, `readiness` |
|
|
185
|
+
| JA4 Lookup | `lookup`, `bulk_lookup`, `stats`, `top_unknown` |
|
|
186
|
+
| Domain | `get_domain`, `bulk_domain_lookup`, `get_domain_history`, `get_domain_changes` |
|
|
187
|
+
| Change feed | `get_changes` |
|
|
188
|
+
| Search | `search` |
|
|
189
|
+
| Technologies | `get_tech_stats`, `get_tech_trend` |
|
|
190
|
+
| Exports | `create_export`, `get_export_status`, `download_export` |
|
|
191
|
+
| System | `get_system_stats`, `get_system_health` |
|
|
192
|
+
| Ingestion | `ingest_crawl_results` (system identity only — 403 for tenant keys) |
|
|
193
|
+
| Crawl requests | `create_crawl_request`, `list_pending_crawl_requests`, `get_crawl_request_status` |
|
|
194
|
+
| Priority crawl | `submit_priority_crawl`, `get_priority_crawl_status` |
|
|
195
|
+
| IP lookup | `ip_batch_lookup` |
|
|
196
|
+
| Pivots | `pivot_ja4`, `pivot_jarm`, `pivot_favicon` |
|
|
197
|
+
| Alerts | `create_alert`, `list_alerts`, `delete_alert` |
|
|
198
|
+
| Auth | `login`, `logout` |
|
|
199
|
+
| Account | `get_account`, `list_api_keys`, `issue_api_key`, `revoke_api_key`, `get_account_usage` |
|
|
200
|
+
|
|
201
|
+
## Exceptions
|
|
202
|
+
|
|
203
|
+
All exceptions inherit from `techrecon.TechReconError`. HTTP errors raise
|
|
204
|
+
`techrecon.APIError` subclasses, each carrying `.status_code`, `.message`
|
|
205
|
+
(the API's `error` field), and `.body` (raw response text):
|
|
206
|
+
|
|
207
|
+
| Exception | HTTP status |
|
|
208
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
209
|
+
| `BadRequestError` | 400 |
|
|
210
|
+
| `AuthenticationError` | 401 |
|
|
211
|
+
| `ForbiddenError` | 403 |
|
|
212
|
+
| `NotFoundError` | 404 |
|
|
213
|
+
| `ConflictError` | 409 |
|
|
214
|
+
| `PayloadTooLargeError` | 413 |
|
|
215
|
+
| `RateLimitError` | 429 (rate limiting **and** quota exhaustion — the spec has no 402; 429 is used uniformly, e.g. alert quota, API-key cap) |
|
|
216
|
+
| `ServiceUnavailableError` | 503 (a required storage backend isn't configured) |
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
cd sdk/python
|
|
222
|
+
python3 -m py_compile src/techrecon/*.py tests/*.py # syntax check
|
|
223
|
+
python3 -m pytest tests/ # run tests (network-free)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Tests use a fake in-memory transport (`tests/fake_transport.py`) — no
|
|
227
|
+
network access or live API key is required to run them.
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
Proprietary — see `docs/openapi.yaml`'s `info.license` for the canonical
|
|
232
|
+
statement. Not licensed for redistribution outside TechRecon-authorized use.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# techrecon (Python client)
|
|
2
|
+
|
|
3
|
+
Python client for the [TechRecon](https://api.techrecon.io) JA4 fingerprinting +
|
|
4
|
+
web-crawl technology intelligence API. Generated from `docs/openapi.yaml`
|
|
5
|
+
(spec version 1.1.0).
|
|
6
|
+
|
|
7
|
+
- **Zero runtime dependencies** — built on `urllib.request` from the standard
|
|
8
|
+
library only.
|
|
9
|
+
- **Typed** — one method per endpoint, `TypedDict` response shapes, typed
|
|
10
|
+
exceptions per HTTP status code.
|
|
11
|
+
- Requires Python **3.10+**.
|
|
12
|
+
|
|
13
|
+
> **Not yet published to PyPI.** This package is prepared but not released;
|
|
14
|
+
> see the "Publishing" note in the PR description for what's still a human
|
|
15
|
+
> decision (final package name, PyPI upload).
|
|
16
|
+
|
|
17
|
+
## Install (once published)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install techrecon
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Until then, install from a local checkout:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd sdk/python
|
|
27
|
+
pip install -e .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 5-minute quickstart
|
|
31
|
+
|
|
32
|
+
### 1. Get an API key
|
|
33
|
+
|
|
34
|
+
Every endpoint except health checks and docs requires an API key, sent as
|
|
35
|
+
the `X-API-Key` header. Get one from your TechRecon account
|
|
36
|
+
(`POST /v1/account/keys`), or ask your TechRecon contact for one.
|
|
37
|
+
|
|
38
|
+
### 2. Create a client
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from techrecon import TechRecon
|
|
42
|
+
|
|
43
|
+
client = TechRecon(api_key="<YOUR_TECHRECON_API_KEY>")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Point at a local dev server instead of production with `base_url`:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
client = TechRecon(api_key="<YOUR_TECHRECON_API_KEY>", base_url="http://localhost:8080")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 3. Your first lookup
|
|
53
|
+
|
|
54
|
+
Look up a domain's detected technology stack:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
domain = client.get_domain("wordpress.org")
|
|
58
|
+
print(domain["tech_count"], "technologies detected")
|
|
59
|
+
for tech in domain["technologies"]:
|
|
60
|
+
print(f" {tech['technology']} ({tech['confidence']:.2f} confidence)")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
4 technologies detected
|
|
65
|
+
WordPress (0.99 confidence)
|
|
66
|
+
PHP (0.95 confidence)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or look up a JA4 TLS fingerprint directly:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
result = client.lookup("t13d1516h2_8daaf6152771_b0da82dd1658", ip="93.184.216.34")
|
|
73
|
+
if result["ja4_tech_match"]:
|
|
74
|
+
print(result["ja4_tech_match"]["technology"]) # "nginx"
|
|
75
|
+
else:
|
|
76
|
+
print("no mapping for this fingerprint yet")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 4. Handle errors
|
|
80
|
+
|
|
81
|
+
Every non-2xx response raises a typed exception carrying the status code
|
|
82
|
+
and the API's `{"error": "..."}` message:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from techrecon import NotFoundError, RateLimitError, TechRecon
|
|
86
|
+
|
|
87
|
+
client = TechRecon(api_key="trk_live_...")
|
|
88
|
+
try:
|
|
89
|
+
client.get_domain("does-not-exist.example")
|
|
90
|
+
except NotFoundError as exc:
|
|
91
|
+
print(f"not found: {exc.message}")
|
|
92
|
+
except RateLimitError as exc:
|
|
93
|
+
print(f"rate limited (429): {exc.message}")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
See [Exceptions](#exceptions) below for the full hierarchy.
|
|
97
|
+
|
|
98
|
+
### 5. That's it
|
|
99
|
+
|
|
100
|
+
You now have a working client. See [Endpoints covered](#endpoints-covered)
|
|
101
|
+
below for the full method list, or browse the interactive spec at
|
|
102
|
+
`GET /v1/docs` on any TechRecon server.
|
|
103
|
+
|
|
104
|
+
## More examples
|
|
105
|
+
|
|
106
|
+
**Bulk domain lookup** (up to 100 domains per call):
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
result = client.bulk_domain_lookup(["example.com", "wordpress.org"])
|
|
110
|
+
for r in result["results"]:
|
|
111
|
+
print(r["domain"], r["tech_count"])
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Search by technology:**
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
page = client.search(technology="WordPress", min_confidence=0.8, limit=50)
|
|
118
|
+
for hit in page["results"]:
|
|
119
|
+
print(hit["domain"])
|
|
120
|
+
if page["has_more"]:
|
|
121
|
+
next_page = client.search(technology="WordPress", cursor=page["next_cursor"])
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Change feed** (technology adds/removes/updates in the last N days):
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
changes = client.get_changes(since="2026-06-01T00:00:00Z", technology="WordPress")
|
|
128
|
+
for event in changes["changes"]:
|
|
129
|
+
print(event["domain"], event["change_type"], event["technology"])
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Reverse pivot from a JA4 hash to the domains that present it:**
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
page = client.pivot_ja4("t13d1516h2_8daaf6152771_02713d6af862")
|
|
136
|
+
print(page["summary"]["total_domains"], "domains observed with this fingerprint")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Create a Slack alert on any technology change for a domain:**
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
alert = client.create_alert(
|
|
143
|
+
channel="slack",
|
|
144
|
+
endpoint="https://hooks.slack.com/services/XXX/YYY/ZZZ",
|
|
145
|
+
domain="example.com",
|
|
146
|
+
)
|
|
147
|
+
print(alert["id"])
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Endpoints covered
|
|
151
|
+
|
|
152
|
+
This client covers the stable, public surface of `docs/openapi.yaml`.
|
|
153
|
+
Endpoints marked `x-internal: true` in the spec — self-serve
|
|
154
|
+
signup/email-verification (beta, not GA), Stripe billing (beta, being
|
|
155
|
+
replaced by the NAF gateway), and the privacy admin routes — are
|
|
156
|
+
intentionally **not** wrapped here; they're beta or admin-only per the
|
|
157
|
+
spec's own descriptions. Everything else is covered:
|
|
158
|
+
|
|
159
|
+
| Area | Methods |
|
|
160
|
+
| -------------- | -------------------------------------------------------------------------------------- |
|
|
161
|
+
| Health | `health`, `readiness` |
|
|
162
|
+
| JA4 Lookup | `lookup`, `bulk_lookup`, `stats`, `top_unknown` |
|
|
163
|
+
| Domain | `get_domain`, `bulk_domain_lookup`, `get_domain_history`, `get_domain_changes` |
|
|
164
|
+
| Change feed | `get_changes` |
|
|
165
|
+
| Search | `search` |
|
|
166
|
+
| Technologies | `get_tech_stats`, `get_tech_trend` |
|
|
167
|
+
| Exports | `create_export`, `get_export_status`, `download_export` |
|
|
168
|
+
| System | `get_system_stats`, `get_system_health` |
|
|
169
|
+
| Ingestion | `ingest_crawl_results` (system identity only — 403 for tenant keys) |
|
|
170
|
+
| Crawl requests | `create_crawl_request`, `list_pending_crawl_requests`, `get_crawl_request_status` |
|
|
171
|
+
| Priority crawl | `submit_priority_crawl`, `get_priority_crawl_status` |
|
|
172
|
+
| IP lookup | `ip_batch_lookup` |
|
|
173
|
+
| Pivots | `pivot_ja4`, `pivot_jarm`, `pivot_favicon` |
|
|
174
|
+
| Alerts | `create_alert`, `list_alerts`, `delete_alert` |
|
|
175
|
+
| Auth | `login`, `logout` |
|
|
176
|
+
| Account | `get_account`, `list_api_keys`, `issue_api_key`, `revoke_api_key`, `get_account_usage` |
|
|
177
|
+
|
|
178
|
+
## Exceptions
|
|
179
|
+
|
|
180
|
+
All exceptions inherit from `techrecon.TechReconError`. HTTP errors raise
|
|
181
|
+
`techrecon.APIError` subclasses, each carrying `.status_code`, `.message`
|
|
182
|
+
(the API's `error` field), and `.body` (raw response text):
|
|
183
|
+
|
|
184
|
+
| Exception | HTTP status |
|
|
185
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
186
|
+
| `BadRequestError` | 400 |
|
|
187
|
+
| `AuthenticationError` | 401 |
|
|
188
|
+
| `ForbiddenError` | 403 |
|
|
189
|
+
| `NotFoundError` | 404 |
|
|
190
|
+
| `ConflictError` | 409 |
|
|
191
|
+
| `PayloadTooLargeError` | 413 |
|
|
192
|
+
| `RateLimitError` | 429 (rate limiting **and** quota exhaustion — the spec has no 402; 429 is used uniformly, e.g. alert quota, API-key cap) |
|
|
193
|
+
| `ServiceUnavailableError` | 503 (a required storage backend isn't configured) |
|
|
194
|
+
|
|
195
|
+
## Development
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
cd sdk/python
|
|
199
|
+
python3 -m py_compile src/techrecon/*.py tests/*.py # syntax check
|
|
200
|
+
python3 -m pytest tests/ # run tests (network-free)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Tests use a fake in-memory transport (`tests/fake_transport.py`) — no
|
|
204
|
+
network access or live API key is required to run them.
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
Proprietary — see `docs/openapi.yaml`'s `info.license` for the canonical
|
|
209
|
+
statement. Not licensed for redistribution outside TechRecon-authorized use.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
# NOTE: "techrecon" is a placeholder package/distribution name. Whether this
|
|
7
|
+
# is the final name to publish under PyPI (vs. a scoped/prefixed alternative)
|
|
8
|
+
# is a human decision — see the PR description for details. This has not been
|
|
9
|
+
# published to PyPI.
|
|
10
|
+
name = "techrecon"
|
|
11
|
+
version = "0.1.0"
|
|
12
|
+
description = "Python client for the TechRecon JA4 + web-crawl technology intelligence API"
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
license = { text = "Proprietary" }
|
|
16
|
+
authors = [{ name = "TechRecon" }]
|
|
17
|
+
keywords = ["techrecon", "ja4", "technology-detection", "fingerprinting"]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 3 - Alpha",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Internet",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
]
|
|
29
|
+
# Zero runtime dependencies by design: the client uses only urllib.request
|
|
30
|
+
# and the standard library so installing this package never pulls in a
|
|
31
|
+
# third-party HTTP stack.
|
|
32
|
+
dependencies = []
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
test = ["pytest>=7"]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://api.techrecon.io"
|
|
39
|
+
Documentation = "https://api.techrecon.io/v1/docs"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Python client for the TechRecon JA4 + web-crawl technology intelligence API.
|
|
2
|
+
|
|
3
|
+
from techrecon import TechRecon
|
|
4
|
+
|
|
5
|
+
client = TechRecon(api_key="trk_live_...")
|
|
6
|
+
result = client.lookup("t13d1516h2_8daaf6152771_b0da82dd1658")
|
|
7
|
+
|
|
8
|
+
See README.md for a full quickstart and https://api.techrecon.io/v1/docs for
|
|
9
|
+
the interactive API reference.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .client import TechRecon
|
|
13
|
+
from .exceptions import (
|
|
14
|
+
APIError,
|
|
15
|
+
AuthenticationError,
|
|
16
|
+
BadRequestError,
|
|
17
|
+
ConflictError,
|
|
18
|
+
ForbiddenError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
PayloadTooLargeError,
|
|
21
|
+
RateLimitError,
|
|
22
|
+
ServiceUnavailableError,
|
|
23
|
+
TechReconError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"TechRecon",
|
|
30
|
+
"TechReconError",
|
|
31
|
+
"APIError",
|
|
32
|
+
"AuthenticationError",
|
|
33
|
+
"ForbiddenError",
|
|
34
|
+
"NotFoundError",
|
|
35
|
+
"BadRequestError",
|
|
36
|
+
"RateLimitError",
|
|
37
|
+
"PayloadTooLargeError",
|
|
38
|
+
"ServiceUnavailableError",
|
|
39
|
+
"ConflictError",
|
|
40
|
+
"__version__",
|
|
41
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Stdlib-only HTTP transport used by :class:`techrecon.client.TechRecon`.
|
|
2
|
+
|
|
3
|
+
The client depends on this module through a small protocol (any object with
|
|
4
|
+
a ``request`` method matching :meth:`Transport.request`), so tests can pass
|
|
5
|
+
a fake/mock transport instead of hitting the network. The default
|
|
6
|
+
:class:`Transport` uses only ``urllib.request`` from the standard library —
|
|
7
|
+
no third-party HTTP library is required to install or use this package.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any, Dict, NamedTuple, Optional
|
|
14
|
+
from urllib import error as urllib_error
|
|
15
|
+
from urllib import request as urllib_request
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HTTPResponse(NamedTuple):
|
|
19
|
+
"""The minimal response shape the client needs from a transport."""
|
|
20
|
+
|
|
21
|
+
status_code: int
|
|
22
|
+
body: bytes
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Transport:
|
|
26
|
+
"""Default transport: issues requests with ``urllib.request``."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, timeout: float = 30.0) -> None:
|
|
29
|
+
self.timeout = timeout
|
|
30
|
+
|
|
31
|
+
def request(
|
|
32
|
+
self,
|
|
33
|
+
method: str,
|
|
34
|
+
url: str,
|
|
35
|
+
headers: Dict[str, str],
|
|
36
|
+
json_body: Optional[Dict[str, Any]] = None,
|
|
37
|
+
) -> HTTPResponse:
|
|
38
|
+
data = None
|
|
39
|
+
if json_body is not None:
|
|
40
|
+
data = json.dumps(json_body).encode("utf-8")
|
|
41
|
+
|
|
42
|
+
req = urllib_request.Request(url, data=data, headers=headers, method=method)
|
|
43
|
+
try:
|
|
44
|
+
with urllib_request.urlopen(req, timeout=self.timeout) as resp:
|
|
45
|
+
return HTTPResponse(status_code=resp.status, body=resp.read())
|
|
46
|
+
except urllib_error.HTTPError as exc:
|
|
47
|
+
# HTTPError is also a valid response with a body; urlopen raises
|
|
48
|
+
# it instead of returning it for non-2xx statuses.
|
|
49
|
+
return HTTPResponse(status_code=exc.code, body=exc.read())
|
|
50
|
+
except urllib_error.URLError as exc:
|
|
51
|
+
raise ConnectionError(f"failed to reach {url}: {exc.reason}") from exc
|