cerberus-keys 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,250 @@
1
+ Metadata-Version: 2.4
2
+ Name: cerberus-keys
3
+ Version: 0.1.0
4
+ Summary: Client library for Cerberus — per-key API abuse detection
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Provides-Extra: dev
8
+ Requires-Dist: pytest>=8.0; extra == "dev"
9
+ Requires-Dist: build>=1.0; extra == "dev"
10
+ Requires-Dist: twine>=5.0; extra == "dev"
11
+
12
+ # Cerberus
13
+
14
+ Watches API-key usage metadata and tells you when one of your customers' keys
15
+ starts behaving unlike its own history — a shared, stolen, or scraped
16
+ credential — before the bill arrives.
17
+
18
+ Not bot detection. Specifically: *this paying customer's key is doing
19
+ something it has never done, and it's costing you money.*
20
+
21
+ ---
22
+
23
+ ## What Cerberus receives
24
+
25
+ **Twelve fields. Nothing else.** The ingest endpoint rejects any unknown field
26
+ by name, so this list is enforced in code rather than promised in prose.
27
+
28
+ | Field | Meaning |
29
+ |---|---|
30
+ | `ts` | ISO 8601 UTC timestamp |
31
+ | `key_fp` | Fingerprint of your API key ID — never the key |
32
+ | `endpoint` | Route **template** (see below) |
33
+ | `tokens_in` / `tokens_out` | Token counts |
34
+ | `latency_ms` | Request latency |
35
+ | `status` | HTTP status code |
36
+ | `ip_fp` | Fingerprint of the client IP — never the address |
37
+ | `ip_net_fp` / `ip_block_fp` | Fingerprints of the surrounding network |
38
+ | `ip_family` | `v4` or `v6` |
39
+ | `cost` | Optional, in USD |
40
+
41
+ **No content, ever.** No prompts, no responses — not hashed, not truncated,
42
+ not optional.
43
+
44
+ **Thirty-day rolling retention.**
45
+
46
+ ## Why we cannot see your keys or your users' IP addresses
47
+
48
+ Fingerprints are HMAC-SHA256 under a secret salt **you generate and we never
49
+ receive**, truncated to 128 bits.
50
+
51
+ This matters because a plain hash is not anonymisation: the IPv4 space is
52
+ 2^32, so a GPU builds the whole rainbow table in minutes. With a salt we never
53
+ hold, we cannot reverse a fingerprint even in principle — and because the salt
54
+ is per-tenant, the same IP hitting two of our customers produces two unrelated
55
+ fingerprints. We could not build a cross-customer profile if we wanted to.
56
+
57
+ ### Salt handling
58
+
59
+ - Store it wherever your other secrets live, **not in a config file**.
60
+ - Use the **same salt on every instance**, or one key looks like several.
61
+ - **Do not rotate it.** Every historical fingerprint becomes uncorrelatable
62
+ and all baselines reset.
63
+ - If you lose it, baselines reset. Nothing else breaks.
64
+
65
+ ### Looking up a fingerprint at 2am
66
+
67
+ An alert names a key by fingerprint, because that is all we have. You can
68
+ always map it back — you hold both the salt and your key list:
69
+
70
+ ```python
71
+ from cerberus_keys import fingerprint
72
+ {fingerprint(k, salt): k for k in my_api_keys}
73
+ ```
74
+
75
+ We cannot do this lookup. That is the point. Build the index once during
76
+ onboarding and keep it.
77
+
78
+ ## Installing
79
+
80
+ ```bash
81
+ pip install cerberus-keys
82
+ ```
83
+
84
+ Zero runtime dependencies, standard library only.
85
+
86
+ ```python
87
+ from cerberus_keys import Cerberus
88
+
89
+ client = Cerberus(ingest_token, salt, endpoint_url="https://.../v1/events")
90
+ client.record(api_key=key, ip=client_ip, endpoint="/v1/chat",
91
+ tokens_in=n_in, tokens_out=n_out, latency_ms=ms, status=200)
92
+ ```
93
+
94
+ `record()` **never blocks, never raises, and never adds latency** to your
95
+ request path. It enqueues and returns; a full buffer drops rather than
96
+ applying backpressure; every exception is swallowed at the boundary. A
97
+ monitoring library that can stall the thing it monitors is not worth running.
98
+ Dropped events are visible on `client.dropped` and are never an error.
99
+
100
+ ## You will know within the hour that it works
101
+
102
+ Once your first events reach us, Cerberus posts a one-time confirmation to your
103
+ Slack webhook. It arrives on the next hourly pass — so within the hour, not
104
+ instantly — and it tells you:
105
+
106
+ - how many events we received, and how many distinct API keys they came from
107
+ - how many distinct client IPs we can see
108
+ - when detection starts, since we need about a week of history per key first
109
+
110
+ **If it does not arrive, something is wrong, and that is the point.** Cerberus
111
+ can fail in a way that looks exactly like working: this SDK drops failed sends
112
+ silently, on purpose, so it can never add latency or raise into your request
113
+ path. A Cerberus that is receiving nothing looks identical to a Cerberus that
114
+ is watching quietly. The confirmation exists so that silence means something
115
+ specific instead of nothing.
116
+
117
+ If an hour passes and no message arrives, check in this order:
118
+
119
+ 1. **Is a Slack webhook configured on your account?** No webhook, no message —
120
+ and no alerts either.
121
+ 2. **Is the ingest token right, and not revoked?** A bad token gets a 401,
122
+ which the SDK drops.
123
+ 3. **Is `endpoint_url` correct?** Anything non-2xx is dropped the same way.
124
+ 4. **Is your process alive long enough to flush?** The client batches. A script
125
+ that exits immediately should call `.close()`.
126
+
127
+ `Cerberus(...).dropped` counts events the client discarded, and is the fastest
128
+ local check that something is being sent at all.
129
+
130
+ ### `endpoint` must be a route template
131
+
132
+ Send `/v1/orgs/{org_id}/chat`, **not** `/v1/orgs/acme-corp/chat`. Live paths
133
+ routinely carry identifiers, and this field would carry them to us.
134
+
135
+ The ingest endpoint rejects paths containing `@`, UUIDs, or long digit and hex
136
+ runs — but that is defence in depth, not a guarantee. `/v1/orgs/acme-corp/chat`
137
+ defeats every one of those checks. **The template requirement is the
138
+ mechanism; we cannot detect every violation of it.**
139
+
140
+ ## If you run LiteLLM
141
+
142
+ This is the cheapest integration on offer: LiteLLM's callback payload already
143
+ carries eight of the twelve fields, so there is no instrumentation to write.
144
+ It is **two config blocks, and both are required.**
145
+
146
+ First, a file the proxy can import. Put it next to your config:
147
+
148
+ ```python
149
+ # cerberus_callback.py
150
+ import os
151
+ from cerberus_keys.litellm import CerberusLogger
152
+
153
+ cerberus = CerberusLogger(
154
+ ingest_token=os.environ["CERBERUS_INGEST_TOKEN"],
155
+ salt=bytes.fromhex(os.environ["CERBERUS_SALT"]),
156
+ endpoint_url="https://cerberushq.dev/v1/events",
157
+ )
158
+ ```
159
+
160
+ **Block one** registers it:
161
+
162
+ ```yaml
163
+ litellm_settings:
164
+ callbacks: cerberus_callback.cerberus
165
+ ```
166
+
167
+ **Block two** is the one that matters, and it is in a *different* section:
168
+
169
+ ```yaml
170
+ general_settings:
171
+ use_x_forwarded_for: true # REQUIRED
172
+ ```
173
+
174
+ Without it, LiteLLM reports `request.client.host` — which behind any load
175
+ balancer, ingress, or CDN is *the load balancer's address on every request*.
176
+ Distinct-IP counts collapse to one permanently and fan-out detection silently
177
+ cannot fire, while looking exactly like a clean bill of health. It is the only
178
+ way to install Cerberus, have it appear to work, and get nothing from it.
179
+
180
+ Cerberus checks for this shape and tells you if it sees it. Don't rely on
181
+ that; set the flag.
182
+
183
+ ### Two things that are different on LiteLLM
184
+
185
+ **Resolving a fingerprint takes the hash, not the key.** LiteLLM never hands a
186
+ callback your raw key — it passes `user_api_key_hash`, which is
187
+ `sha256(key).hexdigest()`. So the fingerprint in an alert is
188
+ `HMAC(salt, sha256hex(key))`. Calling `resolve()` with raw keys matches
189
+ nothing, silently, and reads exactly like "that key isn't ours" at the worst
190
+ possible moment. Map them first:
191
+
192
+ ```python
193
+ from cerberus_keys import resolve
194
+ from cerberus_keys.litellm import key_fingerprint_input
195
+
196
+ resolve(fp, [key_fingerprint_input(k) for k in your_keys], salt)
197
+ ```
198
+
199
+ **Streaming latency is time-to-first-token.** LiteLLM's `response_time` is
200
+ `completion_start_time - start_time` when `stream=True`, not the full
201
+ duration. Latency is a digest signal rather than a fan-out condition, so this
202
+ does not affect what pages you — but a streaming-heavy proxy will show lower
203
+ latencies than its users experience, and that is a property of the source, not
204
+ of Cerberus.
205
+
206
+ The proxy path only. `requester_ip_address` is populated in LiteLLM's
207
+ proxy-side request handling, so the LiteLLM **SDK used as a library** does not
208
+ carry it — take the normal `Cerberus(...).record(...)` path there.
209
+
210
+ ## What it detects
211
+
212
+ **One real-time signal: key fan-out.** Not "many IPs" — *many IPs each doing
213
+ very little*, which is the shape of a shared credential and what separates it
214
+ from your infrastructure scaling up. Scaling three containers to forty raises
215
+ volume in proportion; a leaked key inverts that. Five conditions must all
216
+ hold, sustained across consecutive hours, before anything pages you.
217
+
218
+ Everything else — cost and volume anomalies — goes in a daily digest. A
219
+ customer who just launched and 10x'd their usage looks identical to abuse, and
220
+ that customer is the best thing that happened to you this quarter.
221
+
222
+ ## Known limits, stated plainly
223
+
224
+ - **Keys used from fewer than 3 distinct IPs are not protected at all.** A
225
+ key that lives on one or two servers never builds a baseline the rule can
226
+ compare against, so a leak of that key -- however dispersed, however
227
+ sustained -- does not fire. This is the single biggest gap, it covers the
228
+ most common key shape, and `GET /v1/status` names the affected keys as
229
+ `below_detection_floor` rather than pretending they are still warming up.
230
+ The floor is a calibration threshold; it will move only on replay evidence,
231
+ not by guess.
232
+ - **Keys already spread across many IPs are under-protected.** The rule is
233
+ relative to each key's own history, so a key that normally lives on 200 IPs
234
+ needs a far larger jump to trip. Conservative by design; you should know it.
235
+ - **A leak that ramps slowly enough is never caught.** The baseline tracks it
236
+ upward and nothing fires. Detecting that needs a long-horizon reference we
237
+ don't yet have.
238
+ - **Concentrated datacenter scrapers don't page.** A scraper rotating inside
239
+ one provider's range looks concentrated, and the rule requires dispersion.
240
+ Deliberate: a missed scraper is cheaper than a false page.
241
+ - **A serverless migration can page you.** If you move a key from a few
242
+ servers to many small workers (a serverless or autoscaling rollout) at
243
+ roughly constant traffic, and those workers are spread across many network
244
+ blocks — which cloud egress usually is — that has the same shape as a
245
+ leaked key being used from many places, and the rule can fire. It is the one
246
+ false alarm the design cannot rule out from the data it holds: it cannot
247
+ tell your cloud's address ranges apart from a stranger's. If it happens,
248
+ acknowledge the alert (it offers a "this key is legitimately distributed"
249
+ link) and it stops. Tell us if you have a rollout planned and we can quiet
250
+ the key ahead of time.
@@ -0,0 +1,239 @@
1
+ # Cerberus
2
+
3
+ Watches API-key usage metadata and tells you when one of your customers' keys
4
+ starts behaving unlike its own history — a shared, stolen, or scraped
5
+ credential — before the bill arrives.
6
+
7
+ Not bot detection. Specifically: *this paying customer's key is doing
8
+ something it has never done, and it's costing you money.*
9
+
10
+ ---
11
+
12
+ ## What Cerberus receives
13
+
14
+ **Twelve fields. Nothing else.** The ingest endpoint rejects any unknown field
15
+ by name, so this list is enforced in code rather than promised in prose.
16
+
17
+ | Field | Meaning |
18
+ |---|---|
19
+ | `ts` | ISO 8601 UTC timestamp |
20
+ | `key_fp` | Fingerprint of your API key ID — never the key |
21
+ | `endpoint` | Route **template** (see below) |
22
+ | `tokens_in` / `tokens_out` | Token counts |
23
+ | `latency_ms` | Request latency |
24
+ | `status` | HTTP status code |
25
+ | `ip_fp` | Fingerprint of the client IP — never the address |
26
+ | `ip_net_fp` / `ip_block_fp` | Fingerprints of the surrounding network |
27
+ | `ip_family` | `v4` or `v6` |
28
+ | `cost` | Optional, in USD |
29
+
30
+ **No content, ever.** No prompts, no responses — not hashed, not truncated,
31
+ not optional.
32
+
33
+ **Thirty-day rolling retention.**
34
+
35
+ ## Why we cannot see your keys or your users' IP addresses
36
+
37
+ Fingerprints are HMAC-SHA256 under a secret salt **you generate and we never
38
+ receive**, truncated to 128 bits.
39
+
40
+ This matters because a plain hash is not anonymisation: the IPv4 space is
41
+ 2^32, so a GPU builds the whole rainbow table in minutes. With a salt we never
42
+ hold, we cannot reverse a fingerprint even in principle — and because the salt
43
+ is per-tenant, the same IP hitting two of our customers produces two unrelated
44
+ fingerprints. We could not build a cross-customer profile if we wanted to.
45
+
46
+ ### Salt handling
47
+
48
+ - Store it wherever your other secrets live, **not in a config file**.
49
+ - Use the **same salt on every instance**, or one key looks like several.
50
+ - **Do not rotate it.** Every historical fingerprint becomes uncorrelatable
51
+ and all baselines reset.
52
+ - If you lose it, baselines reset. Nothing else breaks.
53
+
54
+ ### Looking up a fingerprint at 2am
55
+
56
+ An alert names a key by fingerprint, because that is all we have. You can
57
+ always map it back — you hold both the salt and your key list:
58
+
59
+ ```python
60
+ from cerberus_keys import fingerprint
61
+ {fingerprint(k, salt): k for k in my_api_keys}
62
+ ```
63
+
64
+ We cannot do this lookup. That is the point. Build the index once during
65
+ onboarding and keep it.
66
+
67
+ ## Installing
68
+
69
+ ```bash
70
+ pip install cerberus-keys
71
+ ```
72
+
73
+ Zero runtime dependencies, standard library only.
74
+
75
+ ```python
76
+ from cerberus_keys import Cerberus
77
+
78
+ client = Cerberus(ingest_token, salt, endpoint_url="https://.../v1/events")
79
+ client.record(api_key=key, ip=client_ip, endpoint="/v1/chat",
80
+ tokens_in=n_in, tokens_out=n_out, latency_ms=ms, status=200)
81
+ ```
82
+
83
+ `record()` **never blocks, never raises, and never adds latency** to your
84
+ request path. It enqueues and returns; a full buffer drops rather than
85
+ applying backpressure; every exception is swallowed at the boundary. A
86
+ monitoring library that can stall the thing it monitors is not worth running.
87
+ Dropped events are visible on `client.dropped` and are never an error.
88
+
89
+ ## You will know within the hour that it works
90
+
91
+ Once your first events reach us, Cerberus posts a one-time confirmation to your
92
+ Slack webhook. It arrives on the next hourly pass — so within the hour, not
93
+ instantly — and it tells you:
94
+
95
+ - how many events we received, and how many distinct API keys they came from
96
+ - how many distinct client IPs we can see
97
+ - when detection starts, since we need about a week of history per key first
98
+
99
+ **If it does not arrive, something is wrong, and that is the point.** Cerberus
100
+ can fail in a way that looks exactly like working: this SDK drops failed sends
101
+ silently, on purpose, so it can never add latency or raise into your request
102
+ path. A Cerberus that is receiving nothing looks identical to a Cerberus that
103
+ is watching quietly. The confirmation exists so that silence means something
104
+ specific instead of nothing.
105
+
106
+ If an hour passes and no message arrives, check in this order:
107
+
108
+ 1. **Is a Slack webhook configured on your account?** No webhook, no message —
109
+ and no alerts either.
110
+ 2. **Is the ingest token right, and not revoked?** A bad token gets a 401,
111
+ which the SDK drops.
112
+ 3. **Is `endpoint_url` correct?** Anything non-2xx is dropped the same way.
113
+ 4. **Is your process alive long enough to flush?** The client batches. A script
114
+ that exits immediately should call `.close()`.
115
+
116
+ `Cerberus(...).dropped` counts events the client discarded, and is the fastest
117
+ local check that something is being sent at all.
118
+
119
+ ### `endpoint` must be a route template
120
+
121
+ Send `/v1/orgs/{org_id}/chat`, **not** `/v1/orgs/acme-corp/chat`. Live paths
122
+ routinely carry identifiers, and this field would carry them to us.
123
+
124
+ The ingest endpoint rejects paths containing `@`, UUIDs, or long digit and hex
125
+ runs — but that is defence in depth, not a guarantee. `/v1/orgs/acme-corp/chat`
126
+ defeats every one of those checks. **The template requirement is the
127
+ mechanism; we cannot detect every violation of it.**
128
+
129
+ ## If you run LiteLLM
130
+
131
+ This is the cheapest integration on offer: LiteLLM's callback payload already
132
+ carries eight of the twelve fields, so there is no instrumentation to write.
133
+ It is **two config blocks, and both are required.**
134
+
135
+ First, a file the proxy can import. Put it next to your config:
136
+
137
+ ```python
138
+ # cerberus_callback.py
139
+ import os
140
+ from cerberus_keys.litellm import CerberusLogger
141
+
142
+ cerberus = CerberusLogger(
143
+ ingest_token=os.environ["CERBERUS_INGEST_TOKEN"],
144
+ salt=bytes.fromhex(os.environ["CERBERUS_SALT"]),
145
+ endpoint_url="https://cerberushq.dev/v1/events",
146
+ )
147
+ ```
148
+
149
+ **Block one** registers it:
150
+
151
+ ```yaml
152
+ litellm_settings:
153
+ callbacks: cerberus_callback.cerberus
154
+ ```
155
+
156
+ **Block two** is the one that matters, and it is in a *different* section:
157
+
158
+ ```yaml
159
+ general_settings:
160
+ use_x_forwarded_for: true # REQUIRED
161
+ ```
162
+
163
+ Without it, LiteLLM reports `request.client.host` — which behind any load
164
+ balancer, ingress, or CDN is *the load balancer's address on every request*.
165
+ Distinct-IP counts collapse to one permanently and fan-out detection silently
166
+ cannot fire, while looking exactly like a clean bill of health. It is the only
167
+ way to install Cerberus, have it appear to work, and get nothing from it.
168
+
169
+ Cerberus checks for this shape and tells you if it sees it. Don't rely on
170
+ that; set the flag.
171
+
172
+ ### Two things that are different on LiteLLM
173
+
174
+ **Resolving a fingerprint takes the hash, not the key.** LiteLLM never hands a
175
+ callback your raw key — it passes `user_api_key_hash`, which is
176
+ `sha256(key).hexdigest()`. So the fingerprint in an alert is
177
+ `HMAC(salt, sha256hex(key))`. Calling `resolve()` with raw keys matches
178
+ nothing, silently, and reads exactly like "that key isn't ours" at the worst
179
+ possible moment. Map them first:
180
+
181
+ ```python
182
+ from cerberus_keys import resolve
183
+ from cerberus_keys.litellm import key_fingerprint_input
184
+
185
+ resolve(fp, [key_fingerprint_input(k) for k in your_keys], salt)
186
+ ```
187
+
188
+ **Streaming latency is time-to-first-token.** LiteLLM's `response_time` is
189
+ `completion_start_time - start_time` when `stream=True`, not the full
190
+ duration. Latency is a digest signal rather than a fan-out condition, so this
191
+ does not affect what pages you — but a streaming-heavy proxy will show lower
192
+ latencies than its users experience, and that is a property of the source, not
193
+ of Cerberus.
194
+
195
+ The proxy path only. `requester_ip_address` is populated in LiteLLM's
196
+ proxy-side request handling, so the LiteLLM **SDK used as a library** does not
197
+ carry it — take the normal `Cerberus(...).record(...)` path there.
198
+
199
+ ## What it detects
200
+
201
+ **One real-time signal: key fan-out.** Not "many IPs" — *many IPs each doing
202
+ very little*, which is the shape of a shared credential and what separates it
203
+ from your infrastructure scaling up. Scaling three containers to forty raises
204
+ volume in proportion; a leaked key inverts that. Five conditions must all
205
+ hold, sustained across consecutive hours, before anything pages you.
206
+
207
+ Everything else — cost and volume anomalies — goes in a daily digest. A
208
+ customer who just launched and 10x'd their usage looks identical to abuse, and
209
+ that customer is the best thing that happened to you this quarter.
210
+
211
+ ## Known limits, stated plainly
212
+
213
+ - **Keys used from fewer than 3 distinct IPs are not protected at all.** A
214
+ key that lives on one or two servers never builds a baseline the rule can
215
+ compare against, so a leak of that key -- however dispersed, however
216
+ sustained -- does not fire. This is the single biggest gap, it covers the
217
+ most common key shape, and `GET /v1/status` names the affected keys as
218
+ `below_detection_floor` rather than pretending they are still warming up.
219
+ The floor is a calibration threshold; it will move only on replay evidence,
220
+ not by guess.
221
+ - **Keys already spread across many IPs are under-protected.** The rule is
222
+ relative to each key's own history, so a key that normally lives on 200 IPs
223
+ needs a far larger jump to trip. Conservative by design; you should know it.
224
+ - **A leak that ramps slowly enough is never caught.** The baseline tracks it
225
+ upward and nothing fires. Detecting that needs a long-horizon reference we
226
+ don't yet have.
227
+ - **Concentrated datacenter scrapers don't page.** A scraper rotating inside
228
+ one provider's range looks concentrated, and the rule requires dispersion.
229
+ Deliberate: a missed scraper is cheaper than a false page.
230
+ - **A serverless migration can page you.** If you move a key from a few
231
+ servers to many small workers (a serverless or autoscaling rollout) at
232
+ roughly constant traffic, and those workers are spread across many network
233
+ blocks — which cloud egress usually is — that has the same shape as a
234
+ leaked key being used from many places, and the rule can fire. It is the one
235
+ false alarm the design cannot rule out from the data it holds: it cannot
236
+ tell your cloud's address ranges apart from a stranger's. If it happens,
237
+ acknowledge the alert (it offers a "this key is legitimately distributed"
238
+ link) and it stops. Tell us if you have a rollout planned and we can quiet
239
+ the key ahead of time.
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "cerberus-keys"
3
+ # DERIVED, never restated: see src/cerberus_keys/version.py. A version
4
+ # written in two files is a version that will be edited in one.
5
+ dynamic = ["version"]
6
+ description = "Client library for Cerberus — per-key API abuse detection"
7
+ readme = "README.md"
8
+ requires-python = ">=3.11"
9
+ dependencies = [] # zero runtime dependencies, deliberately
10
+
11
+ [project.optional-dependencies]
12
+ # `build` and `twine` are the release path, declared so it is
13
+ # reproducible rather than remembered. See docs/RUNBOOK-sdk-release.md.
14
+ dev = ["pytest>=8.0", "build>=1.0", "twine>=5.0"]
15
+
16
+ [build-system]
17
+ requires = ["setuptools>=68"]
18
+ build-backend = "setuptools.build_meta"
19
+
20
+ [tool.setuptools.dynamic]
21
+ version = {attr = "cerberus_keys.version.__version__"}
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
25
+
26
+ # py.typed must reach the wheel, or every customer's type checker silently
27
+ # treats this package as untyped -- no error anywhere, just no checking.
28
+ #
29
+ # MEASURED REDUNDANT with the setuptools in use: removing this stanza and
30
+ # building from a clean tree still ships the file. It is kept as an explicit
31
+ # declaration rather than depending on that behaviour continuing, but it is
32
+ # NOT what guarantees the outcome -- tests/test_python_package.py is, by
33
+ # building a wheel and looking inside it. The server's pricing.json had a
34
+ # correct-looking package-data declaration and shipped nothing, which is the
35
+ # reason nothing here is trusted on the strength of reading it.
36
+ [tool.setuptools.package-data]
37
+ cerberus_keys = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ from .client import Cerberus
2
+ from .version import CLIENT_ID, __version__
3
+ from .fingerprint import (
4
+ IpFingerprints, client_ip_from_forwarded_for, fingerprint, ip_fingerprints,
5
+ resolve,
6
+ )
7
+
8
+ __all__ = ["Cerberus", "fingerprint", "ip_fingerprints", "IpFingerprints",
9
+ "client_ip_from_forwarded_for", "resolve", "CLIENT_ID",
10
+ "__version__"]