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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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