brussle 1.0.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,7 @@
1
+ node_modules/
2
+ dist/
3
+ .venv/
4
+ __pycache__/
5
+ .mypy_cache/
6
+ .pytest_cache/
7
+ *.egg-info/
brussle-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.5
2
+ Name: brussle
3
+ Version: 1.0.0
4
+ Summary: Python SDK for the Brussle API
5
+ License-Expression: LicenseRef-Proprietary
6
+ Requires-Python: >=3.9
7
+ Requires-Dist: httpx<1,>=0.27
8
+ Requires-Dist: typing-extensions>=4.7
9
+ Description-Content-Type: text/markdown
10
+
11
+ # brussle (Python)
12
+
13
+ ```python
14
+ from brussle import And, Client
15
+
16
+ db = Client(api_key="...")
17
+ ns = db.namespace("acme/prod/tenant_123")
18
+ ns.write(upsert=[{"id": "t_123", "attributes": {"plan": "pro"}, "state": {"subject": "Refund"}}])
19
+ page = ns.query(filters=And(("attributes.plan", "Eq", "pro"), ("answers.needs_escalation.p", "Gte", 0.85)), top_k=10)
20
+ ```
21
+
22
+ To load many documents, `import_documents` takes any iterable and writes it in batches, 4 at a time on a thread pool, with retries:
23
+
24
+ ```python
25
+ summary = ns.import_documents(records(), concurrency=4)
26
+ print(summary.documents, summary.failures)
27
+ ```
28
+
29
+ Methods mirror the API's routes. Every route that changes something takes `idempotency_key=`. Reads, writes and calls with a key are retried after network errors, timeouts and 5xx, 2 times by default (`max_retries`). Requests and responses are plain JSON, typed in `brussle.types`. Filters and `rank_by` are tuples. Errors raise `ApiError` with the API's `code` and `details`, or `unknown` for an error response that is not the API's JSON (such as a proxy's error page), or a success whose body is not JSON. A call your organization's plan doesn't include raises `plan_required` (402), and `details["required_plan"]` names the plan that does. See [the SDK overview](../README.md).
@@ -0,0 +1,19 @@
1
+ # brussle (Python)
2
+
3
+ ```python
4
+ from brussle import And, Client
5
+
6
+ db = Client(api_key="...")
7
+ ns = db.namespace("acme/prod/tenant_123")
8
+ ns.write(upsert=[{"id": "t_123", "attributes": {"plan": "pro"}, "state": {"subject": "Refund"}}])
9
+ page = ns.query(filters=And(("attributes.plan", "Eq", "pro"), ("answers.needs_escalation.p", "Gte", 0.85)), top_k=10)
10
+ ```
11
+
12
+ To load many documents, `import_documents` takes any iterable and writes it in batches, 4 at a time on a thread pool, with retries:
13
+
14
+ ```python
15
+ summary = ns.import_documents(records(), concurrency=4)
16
+ print(summary.documents, summary.failures)
17
+ ```
18
+
19
+ Methods mirror the API's routes. Every route that changes something takes `idempotency_key=`. Reads, writes and calls with a key are retried after network errors, timeouts and 5xx, 2 times by default (`max_retries`). Requests and responses are plain JSON, typed in `brussle.types`. Filters and `rank_by` are tuples. Errors raise `ApiError` with the API's `code` and `details`, or `unknown` for an error response that is not the API's JSON (such as a proxy's error page), or a success whose body is not JSON. A call your organization's plan doesn't include raises `plan_required` (402), and `details["required_plan"]` names the plan that does. See [the SDK overview](../README.md).
File without changes
@@ -0,0 +1,95 @@
1
+ """The main spec §6.13 example, made concrete: every call in the order the spec lists them.
2
+
3
+ Run it against staging or a local node with BRUSSLE_API_KEY and BRUSSLE_BASE_URL set:
4
+
5
+ .venv/bin/python examples/spec_6_13.py
6
+
7
+ The tests run it against the mock server.
8
+ """
9
+
10
+ import json
11
+ import os
12
+ from typing import Any, Dict
13
+
14
+ from brussle import DEFAULT_BASE_URL, And, Client
15
+
16
+
17
+ def example(db: Client, *, serves_v15: bool) -> Dict[str, Any]:
18
+ """`serves_v15`: outcomes and job confirmation are v1.5 flows (§6.9, §6.10); v1 has no job awaiting confirmation."""
19
+ ns = db.namespace("acme/prod/tenant_123")
20
+
21
+ created = ns.judgments.create(
22
+ name="needs_escalation",
23
+ type="bool",
24
+ question="Does this ticket require a human to take over from the automated flow?",
25
+ criteria="Escalate when the customer is at risk of leaving, mentions legal action, or the automation has failed twice.",
26
+ context={"fields": ["state.subject", "state.body", "attributes.plan"], "last_n": {"state.messages": 3}, "max_tokens": 4000},
27
+ engine={"name": "jev", "version": "current"},
28
+ thresholds={"escalate": 0.85},
29
+ freshness={"policy": "on_change"},
30
+ activate=True,
31
+ )
32
+
33
+ ns.write(
34
+ upsert=[
35
+ {"id": "t_123", "attributes": {"plan": "pro"}, "state": {"subject": "Refund", "body": "Charged twice.", "messages": []}},
36
+ {"id": "t_124", "attributes": {"plan": "free"}, "state": {"subject": "Login", "body": "Password reset loops."}},
37
+ {"id": "t_125", "attributes": {"plan": "pro"}, "state": {"subject": "Spam", "body": "Buy now."}},
38
+ ],
39
+ patch=[{"id": "t_124", "attributes": {"plan": "enterprise"}, "state": {"status": "open"}}],
40
+ append=[{"id": "t_123", "path": "state.messages", "values": [{"role": "customer", "text": "Third time asking."}]}],
41
+ delete=["t_125"],
42
+ wait_for=["needs_escalation"],
43
+ )
44
+
45
+ page = ns.query(
46
+ filters=And(
47
+ ("attributes.plan", "In", ["pro", "enterprise"]),
48
+ ("answers.needs_escalation.p", "Gte", 0),
49
+ ),
50
+ rank_by=("answers.needs_escalation.p", "desc"),
51
+ top_k=10,
52
+ include={"attributes": ["plan"], "answers": ["needs_escalation"]},
53
+ answers="fresh_only",
54
+ )
55
+
56
+ document = ns.get("t_123", include_history=True)
57
+
58
+ ns.judgments.update("needs_escalation", freshness={"policy": "on_change", "debounce_ms": 2000}, confirm=True)
59
+
60
+ next_version = ns.judgments.create(
61
+ name="needs_escalation",
62
+ type="bool",
63
+ question="Does this ticket require a human to take over from the automated flow?",
64
+ criteria="Escalate on churn risk, legal threats, two automation failures, or a repeated complaint.",
65
+ context={"fields": ["state.subject", "state.body", "attributes.plan"], "last_n": {"state.messages": 3}},
66
+ engine={"name": "jev", "version": "current"},
67
+ thresholds={"escalate": 0.85},
68
+ )
69
+ if "replay" in next_version:
70
+ raise RuntimeError("only an entity judgment's create returns a replay estimate")
71
+ ns.judgments.activate("needs_escalation", version=next_version["version"], force=True)
72
+
73
+ estimate = ns.judgments.backfill("needs_escalation", confirm=False)
74
+ started = ns.judgments.backfill("needs_escalation", confirm=True)
75
+ if "job_id" not in started:
76
+ raise RuntimeError("a confirmed backfill starts a job")
77
+ job_id = started["job_id"]
78
+
79
+ if serves_v15:
80
+ ns.outcomes.append([
81
+ {"document_id": "t_123", "judgment": "needs_escalation", "value": True, "observed_at": "2026-09-23T12:00:00Z"},
82
+ ])
83
+
84
+ db.jobs.get(job_id); db.jobs.pause(job_id); db.jobs.resume(job_id); db.jobs.cancel(job_id) # noqa: E702
85
+ if serves_v15:
86
+ db.jobs.confirm(job_id)
87
+
88
+ return {"created": created, "rows": page["rows"], "document": document, "estimate": estimate}
89
+
90
+
91
+ if __name__ == "__main__":
92
+ base_url = os.environ.get("BRUSSLE_BASE_URL", DEFAULT_BASE_URL)
93
+ with Client(api_key=os.environ["BRUSSLE_API_KEY"], base_url=base_url) as db:
94
+ result = example(db, serves_v15=os.environ.get("JDB_SERVES_V15") == "1")
95
+ print(json.dumps(result, indent=2))
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "brussle"
3
+ version = "1.0.0"
4
+ description = "Python SDK for the Brussle API"
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = "LicenseRef-Proprietary"
8
+ dependencies = ["httpx>=0.27,<1", "typing_extensions>=4.7"]
9
+
10
+ [dependency-groups]
11
+ dev = ["mypy==1.19.1", "pytest>=8", "pyyaml>=6", "types-PyYAML"]
12
+
13
+ [build-system]
14
+ requires = ["hatchling>=1.26"]
15
+ build-backend = "hatchling.build"
16
+
17
+ [tool.hatch.build.targets.wheel]
18
+ packages = ["src/brussle"]
19
+
20
+ [tool.mypy]
21
+ python_version = "3.9"
22
+ strict = true
23
+ files = ["src", "tests", "examples", "scripts"]
24
+
25
+ # Each method returns the parsed JSON (`Any`) under the type the contract gives
26
+ # it. Nothing is validated at runtime (D-surface-2), so there is nothing to cast.
27
+ [[tool.mypy.overrides]]
28
+ module = "brussle._client"
29
+ warn_return_any = false
30
+
31
+ [tool.pytest.ini_options]
32
+ testpaths = ["tests"]
33
+ pythonpath = ["src", "."]
@@ -0,0 +1,306 @@
1
+ """Generates `src/brussle/types.py` from `api/openapi.yaml`, and `_brand.py` from `docs/brand.json`.
2
+
3
+ `docs/brand.json` is the one place the product name and domain are set (D-surface-41).
4
+
5
+ Every component schema becomes a `TypedDict` or a type alias. They are static
6
+ types over the JSON the API sends and receives: nothing is converted or
7
+ validated at runtime, so fields and enum values the API adds later never break
8
+ a client (decision D-surface-2).
9
+
10
+ The generator covers the JSON Schema subset the contract uses. It fails loudly
11
+ on anything else, so a contract change that needs new handling cannot slip
12
+ through as a wrong type.
13
+
14
+ .venv/bin/python scripts/generate_types.py # write the file
15
+ .venv/bin/python scripts/generate_types.py --check # exit 1 if it is stale
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import keyword
22
+ import re
23
+ import sys
24
+ from pathlib import Path
25
+ from typing import Any, Dict, List, Optional
26
+
27
+ import yaml
28
+
29
+ ROOT = Path(__file__).resolve().parents[1]
30
+ OPENAPI = ROOT.parents[1] / "api" / "openapi.yaml"
31
+ OUTPUT = ROOT / "src" / "brussle" / "types.py"
32
+ BRAND = ROOT.parents[1] / "docs" / "brand.json"
33
+ BRAND_OUTPUT = ROOT / "src" / "brussle" / "_brand.py"
34
+
35
+ Schema = Dict[str, Any]
36
+
37
+ # Keywords that only constrain values or help other generators. Types ignore them.
38
+ IGNORED = {
39
+ "description", "examples", "default", "format", "pattern", "minLength", "maxLength",
40
+ "minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "minItems", "maxItems", "uniqueItems", "minProperties",
41
+ "maxProperties", "propertyNames", "discriminator", "if", "then", "not",
42
+ "unevaluatedProperties", "anyOf", "deprecated", "dependentRequired",
43
+ }
44
+ HANDLED = {
45
+ "$ref", "type", "enum", "const", "properties", "required", "additionalProperties",
46
+ "items", "prefixItems", "oneOf", "allOf",
47
+ }
48
+
49
+
50
+ def ref_name(schema: Schema) -> str:
51
+ return str(schema["$ref"]).rsplit("/", 1)[1]
52
+
53
+
54
+ def typed_all_of(schema: Schema) -> List[Schema]:
55
+ """`allOf` members that add a type. Members that only constrain (`if`/`then`/`else`) are ignored like `if` is."""
56
+ return [
57
+ member for member in schema.get("allOf", [])
58
+ if not set(member) <= {"description", "if", "then", "else"}
59
+ ]
60
+
61
+
62
+ def pascal(name: str) -> str:
63
+ return "".join(part[:1].upper() + part[1:] for part in re.split(r"[_\W]+", name) if part)
64
+
65
+
66
+ class Generator:
67
+ def __init__(self, schemas: Dict[str, Schema]) -> None:
68
+ self.schemas = schemas
69
+ # name -> emitted source; TypedDict classes and aliases.
70
+ self.blocks: Dict[str, str] = {}
71
+ # TypedDict base classes must exist before their subclasses at runtime.
72
+ self.bases: Dict[str, List[str]] = {}
73
+ # Schemas other schemas extend with allOf (a union base's variants included); every other TypedDict is final.
74
+ self.base_names: set[str] = set()
75
+ for schema in schemas.values():
76
+ for ref in typed_all_of(schema):
77
+ base = ref_name(ref)
78
+ self.base_names.add(base)
79
+ self.base_names.update(ref_name(variant) for variant in schemas[base].get("oneOf", []))
80
+
81
+ def check_keywords(self, schema: Schema, where: str) -> None:
82
+ unknown = set(schema) - IGNORED - HANDLED
83
+ if unknown:
84
+ raise SystemExit(f"{where}: unsupported schema keywords {sorted(unknown)}")
85
+
86
+ # --- type expressions -------------------------------------------------
87
+
88
+ def expr(self, schema: Schema, where: str) -> str:
89
+ """A type expression for `schema`. References are quoted, so order never matters."""
90
+ self.check_keywords(schema, where)
91
+ if "$ref" in schema:
92
+ return f'"{ref_name(schema)}"'
93
+ if "const" in schema:
94
+ return f"Literal[{schema['const']!r}]"
95
+ if "enum" in schema:
96
+ return f"Literal[{', '.join(repr(v) for v in schema['enum'])}]"
97
+ if "oneOf" in schema:
98
+ return self.union([self.expr(s, where) for s in schema["oneOf"]])
99
+ if "allOf" in schema:
100
+ raise SystemExit(f"{where}: allOf is only supported on named schemas")
101
+ types = schema.get("type")
102
+ if isinstance(types, list):
103
+ return self.union([self.expr({**schema, "type": t}, where) for t in types])
104
+ if types == "string":
105
+ return "str"
106
+ if types == "integer":
107
+ return "int"
108
+ if types == "number":
109
+ return "float"
110
+ if types == "boolean":
111
+ return "bool"
112
+ if types == "null":
113
+ return "None"
114
+ if types == "array":
115
+ if "prefixItems" in schema:
116
+ if schema.get("items") is not False:
117
+ raise SystemExit(f"{where}: tuples must close with items: false")
118
+ items = [self.expr(s, where) for s in schema["prefixItems"]]
119
+ return f"Tuple[{', '.join(items)}]"
120
+ if "items" in schema:
121
+ return f"Sequence[{self.expr(schema['items'], where)}]"
122
+ return "Sequence[Any]"
123
+ if types == "object" or "properties" in schema:
124
+ if "properties" in schema:
125
+ name = pascal(where)
126
+ self.typed_dict(name, schema)
127
+ return f'"{name}"'
128
+ extra = schema.get("additionalProperties")
129
+ if isinstance(extra, dict):
130
+ # An inline object value gets its own name, or it would take the map's.
131
+ return f"Mapping[str, {self.expr(extra, where + ' entry')}]"
132
+ return "Mapping[str, Any]"
133
+ if types is None:
134
+ # Any JSON value, such as an evaluation's raw engine response.
135
+ return "Any"
136
+ raise SystemExit(f"{where}: unsupported type {types!r}")
137
+
138
+ @staticmethod
139
+ def union(members: List[str]) -> str:
140
+ unique = list(dict.fromkeys(members))
141
+ if "None" in unique and len(unique) == 2:
142
+ other = next(m for m in unique if m != "None")
143
+ return f"Optional[{other}]"
144
+ return unique[0] if len(unique) == 1 else f"Union[{', '.join(unique)}]"
145
+
146
+ # --- named schemas ------------------------------------------------------
147
+
148
+ def named(self, name: str, schema: Schema) -> None:
149
+ self.check_keywords(schema, name)
150
+ all_of = typed_all_of(schema)
151
+ if all_of:
152
+ if len(all_of) != 1 or "$ref" not in all_of[0]:
153
+ raise SystemExit(f"{name}: allOf must be a single $ref")
154
+ base = ref_name(all_of[0])
155
+ base_schema = self.schemas[base]
156
+ if "oneOf" in base_schema:
157
+ # A union base: extend each variant, then union the results.
158
+ variants = []
159
+ for ref in base_schema["oneOf"]:
160
+ variant = ref_name(ref)
161
+ const = self.schemas[variant]["properties"]["type"]["const"]
162
+ variant_name = f"{name}{pascal(const)}"
163
+ self.typed_dict(variant_name, schema, base=variant, doc=schema.get("description"))
164
+ variants.append(f'"{variant_name}"')
165
+ self.alias(name, self.union(variants), schema.get("description"))
166
+ return
167
+ self.typed_dict(name, schema, base=base)
168
+ return
169
+ if "properties" in schema:
170
+ self.typed_dict(name, schema)
171
+ return
172
+ self.alias(name, self.expr(schema, name), schema.get("description"))
173
+
174
+ def alias(self, name: str, expr: str, doc: Optional[str]) -> None:
175
+ self.blocks[name] = f"{comment(doc)}{name} = {expr}\n"
176
+
177
+ def typed_dict(self, name: str, schema: Schema, base: Optional[str] = None, doc: Optional[str] = None) -> None:
178
+ # `required` only ever names the schema's own properties in this contract.
179
+ required = set(schema.get("required", []))
180
+ fields = []
181
+ for field, field_schema in schema.get("properties", {}).items():
182
+ field_doc = field_schema.get("description")
183
+ # `{$ref, description}` and `{$ref, type: object, minProperties}` are still the ref.
184
+ if "$ref" in field_schema:
185
+ field_schema = {"$ref": field_schema["$ref"]}
186
+ annotation = self.expr(field_schema, f"{name}_{field}")
187
+ if field not in required:
188
+ annotation = f"NotRequired[{annotation}]"
189
+ fields.append((field, annotation, field_doc))
190
+
191
+ doc = doc if doc is not None else schema.get("description")
192
+ self.bases[name] = [base] if base else []
193
+ if any(keyword.iskeyword(f) for f, _, _ in fields):
194
+ if base:
195
+ raise SystemExit(f"{name}: keyword field names need the functional syntax, which cannot inherit")
196
+ body = ", ".join(f"{f!r}: {a}" for f, a, _ in fields)
197
+ self.blocks[name] = f'{comment(doc)}{name} = TypedDict("{name}", {{{body}}})\n'
198
+ return
199
+ lines = [f"class {name}({base or 'TypedDict'}):"]
200
+ if name not in self.base_names:
201
+ # Final, so type checkers narrow a union such as `BackfillEstimate | JobStarted` with `"job_id" in r`.
202
+ lines.insert(0, "@final")
203
+ if doc:
204
+ body = "\n".join(f" {line}".rstrip() for line in doc.strip().splitlines())
205
+ lines.append(f' """{body.lstrip()}"""')
206
+ lines.append("")
207
+ for field, annotation, field_doc in fields:
208
+ if field_doc:
209
+ lines.extend(f" # {line}" for line in field_doc.strip().splitlines())
210
+ lines.append(f" {field}: {annotation}")
211
+ if not fields and not doc:
212
+ lines.append(" pass")
213
+ self.blocks[name] = "\n".join(lines) + "\n"
214
+
215
+ def render(self) -> str:
216
+ for name, schema in self.schemas.items():
217
+ self.named(name, schema)
218
+ emitted: List[str] = []
219
+ done: set[str] = set()
220
+
221
+ def emit(name: str) -> None:
222
+ if name in done:
223
+ return
224
+ for base in self.bases.get(name, []):
225
+ emit(base)
226
+ done.add(name)
227
+ emitted.append(self.blocks[name])
228
+
229
+ for name in self.blocks:
230
+ emit(name)
231
+ return HEADER + "\n\n".join(emitted)
232
+
233
+
234
+ def comment(doc: Optional[str]) -> str:
235
+ if not doc:
236
+ return ""
237
+ return "".join(f"# {line}".rstrip() + "\n" for line in doc.strip().splitlines())
238
+
239
+
240
+ HEADER = '''"""Types for every schema in the API contract (`api/openapi.yaml`).
241
+
242
+ Generated by `scripts/generate_types.py`. Do not edit by hand.
243
+
244
+ Request and response bodies are plain JSON: these types describe it for type
245
+ checkers and do nothing at runtime. Filters and `rank_by` are tuples.
246
+ """
247
+
248
+ # ruff: noqa
249
+ from typing import Any, Mapping, Optional, Sequence, Tuple, Union
250
+
251
+ from typing_extensions import Literal, NotRequired, TypedDict, final
252
+
253
+
254
+ '''
255
+
256
+
257
+ # References to the private main spec (`§7.2.8`, `§9 Plans`) and decision log (`D-v15-611`), which the
258
+ # package's comments never carry. The same patterns as docs/scripts/refs.ts.
259
+ REF = r"(?:§\d+(?:\.\d+)*(?: Plans)?|D-[a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)*-\d+)"
260
+ REFS = rf"{REF}(?:\s*(?:,|;|\band\b|\bto\b)\s*{REF})*"
261
+
262
+
263
+ def strip_internal_refs(value: Any) -> Any:
264
+ """Every string in `value` without spec or decision references, as the docs' API reference strips them."""
265
+ if isinstance(value, dict):
266
+ return {key: strip_internal_refs(item) for key, item in value.items()}
267
+ if isinstance(value, list):
268
+ return [strip_internal_refs(item) for item in value]
269
+ if not isinstance(value, str):
270
+ return value
271
+ value = re.sub(rf"\s*\({REFS}\)", "", value)
272
+ value = re.sub(rf",\s*{REFS}(?=\s*[,)])", "", value)
273
+ value = re.sub(rf"\({REFS}\s*,\s*", "(", value)
274
+ return re.sub(rf"\s*\bSee {REF}\.", "", value)
275
+
276
+
277
+ def render_brand(brand: Dict[str, str]) -> str:
278
+ """`_brand.py`: the product name and the default base URL, from `docs/brand.json`."""
279
+ name, domain = brand["productName"], brand["domain"]
280
+ return f'''"""The product name and the API's default base URL.
281
+
282
+ Generated from docs/brand.json by scripts/generate_types.py. Do not edit by hand. docs/brand.json is
283
+ the one place the product name and domain are set: edit it, then run this script, and `pnpm generate`
284
+ in sdks/typescript and in docs/.
285
+ """
286
+
287
+ PRODUCT_NAME = {json.dumps(name)}
288
+ DEFAULT_BASE_URL = {json.dumps(f"https://api.{domain}/v1")}
289
+ '''
290
+
291
+
292
+ def main() -> None:
293
+ document = strip_internal_refs(yaml.safe_load(OPENAPI.read_text()))
294
+ outputs = {
295
+ OUTPUT: Generator(document["components"]["schemas"]).render(),
296
+ BRAND_OUTPUT: render_brand(json.loads(BRAND.read_text())),
297
+ }
298
+ for path, source in outputs.items():
299
+ if "--check" not in sys.argv:
300
+ path.write_text(source)
301
+ elif path.read_text() != source:
302
+ raise SystemExit(f"{path} is stale: run scripts/generate_types.py")
303
+
304
+
305
+ if __name__ == "__main__":
306
+ main()
@@ -0,0 +1,72 @@
1
+ """Python SDK for the Brussle API.
2
+
3
+ from brussle import Client
4
+
5
+ db = Client(api_key=...)
6
+ ns = db.namespace("acme/prod/tenant_123")
7
+ ns.query(filters=And(("attributes.plan", "Eq", "pro"), ("answers.needs_escalation.p", "Gte", 0.85)), top_k=10)
8
+
9
+ Request and response bodies are the JSON of `api/openapi.yaml`, typed by
10
+ `brussle.types`. Filters and `rank_by` are tuples. A pushed event is checked
11
+ with `verify_webhook`, which returns it as `brussle.types.WebhookEvent`.
12
+ """
13
+
14
+ from . import types
15
+ from ._brand import DEFAULT_BASE_URL, PRODUCT_NAME
16
+ from .filters import And, Not, Or
17
+ from ._import import MAX_WRITE_BYTES, MAX_WRITE_DOCUMENTS, ImportFailure, ImportSummary
18
+ from ._client import (
19
+ MAX_RETRY_AFTER_SECONDS,
20
+ ApiError,
21
+ Client,
22
+ EnginesClient,
23
+ EventsClient,
24
+ GroupsClient,
25
+ JobsClient,
26
+ JudgmentsClient,
27
+ NamespaceClient,
28
+ NamespacesClient,
29
+ NonProductionPrefixesClient,
30
+ OutcomesClient,
31
+ StartersClient,
32
+ SubscriptionsClient,
33
+ TemplatesClient,
34
+ TenantSummaryClient,
35
+ WebhookEndpointsClient,
36
+ )
37
+ from ._version import __version__
38
+ from .webhooks import WebhookHeaders, WebhookVerificationError, verify_webhook
39
+
40
+ __all__ = [
41
+ "DEFAULT_BASE_URL",
42
+ "MAX_RETRY_AFTER_SECONDS",
43
+ "MAX_WRITE_BYTES",
44
+ "MAX_WRITE_DOCUMENTS",
45
+ "PRODUCT_NAME",
46
+ "And",
47
+ "ApiError",
48
+ "Client",
49
+ "EnginesClient",
50
+ "EventsClient",
51
+ "GroupsClient",
52
+ "ImportFailure",
53
+ "ImportSummary",
54
+ "JobsClient",
55
+ "JudgmentsClient",
56
+ "NamespaceClient",
57
+ "NamespacesClient",
58
+ "NonProductionPrefixesClient",
59
+ "Not",
60
+ "Or",
61
+ "OutcomesClient",
62
+ "StartersClient",
63
+ "SubscriptionsClient",
64
+ "TemplatesClient",
65
+ "TenantSummaryClient",
66
+ "WebhookEndpointsClient",
67
+ "WebhookHeaders",
68
+ "WebhookVerificationError",
69
+ "__version__",
70
+ "types",
71
+ "verify_webhook",
72
+ ]
@@ -0,0 +1,9 @@
1
+ """The product name and the API's default base URL.
2
+
3
+ Generated from docs/brand.json by scripts/generate_types.py. Do not edit by hand. docs/brand.json is
4
+ the one place the product name and domain are set: edit it, then run this script, and `pnpm generate`
5
+ in sdks/typescript and in docs/.
6
+ """
7
+
8
+ PRODUCT_NAME = "Brussle"
9
+ DEFAULT_BASE_URL = "https://api.brussle.com/v1"