dbl-gateway 0.3.2__py3-none-any.whl

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,105 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+ from .errors import ProviderError
9
+
10
+
11
+ def execute(message: str, model_id: str) -> str:
12
+ api_key = os.getenv("OPENAI_API_KEY", "").strip()
13
+ if not api_key:
14
+ raise ProviderError("missing OpenAI credentials")
15
+ headers = {"Authorization": f"Bearer {api_key}"}
16
+ if _use_responses(model_id):
17
+ return _execute_responses(message, model_id, headers)
18
+ return _execute_chat(message, model_id, headers)
19
+
20
+
21
+ def _use_responses(model_id: str) -> bool:
22
+ if model_id.startswith("gpt-5"):
23
+ return True
24
+ return model_id in _responses_models()
25
+
26
+
27
+ def _responses_models() -> list[str]:
28
+ raw = os.getenv("OPENAI_RESPONSES_MODEL_IDS", "").strip()
29
+ if not raw:
30
+ return ["gpt-5.2"]
31
+ return [item.strip() for item in raw.split(",") if item.strip()]
32
+
33
+
34
+ def _execute_responses(message: str, model_id: str, headers: dict[str, str]) -> str:
35
+ payload: dict[str, Any] = {
36
+ "model": model_id,
37
+ "input": [
38
+ {
39
+ "role": "user",
40
+ "content": [{"type": "input_text", "text": message}],
41
+ }
42
+ ],
43
+ }
44
+ with httpx.Client(timeout=30.0) as client:
45
+ resp = client.post("https://api.openai.com/v1/responses", json=payload, headers=headers)
46
+ if resp.status_code >= 400:
47
+ _raise_openai(resp, "openai.responses failed")
48
+ data = resp.json()
49
+ return _parse_response_text(data)
50
+
51
+
52
+ def _execute_chat(message: str, model_id: str, headers: dict[str, str]) -> str:
53
+ payload: dict[str, Any] = {
54
+ "model": model_id,
55
+ "messages": [{"role": "user", "content": message}],
56
+ "temperature": 0.2,
57
+ "max_tokens": 256,
58
+ }
59
+ with httpx.Client(timeout=30.0) as client:
60
+ resp = client.post("https://api.openai.com/v1/chat/completions", json=payload, headers=headers)
61
+ if resp.status_code >= 400:
62
+ _raise_openai(resp, "openai.chat failed")
63
+ data = resp.json()
64
+ return _parse_chat_text(data)
65
+
66
+
67
+ def _parse_chat_text(data: dict[str, Any]) -> str:
68
+ choices = data.get("choices", [])
69
+ if not choices:
70
+ return ""
71
+ message = choices[0].get("message", {})
72
+ content = message.get("content")
73
+ return content if isinstance(content, str) else ""
74
+
75
+
76
+ def _parse_response_text(data: dict[str, Any]) -> str:
77
+ outputs = data.get("output", [])
78
+ parts: list[str] = []
79
+ for item in outputs:
80
+ content = item.get("content", [])
81
+ for entry in content:
82
+ if entry.get("type") == "output_text":
83
+ text = entry.get("text")
84
+ if isinstance(text, str):
85
+ parts.append(text)
86
+ return "\n".join(parts)
87
+
88
+
89
+ def _raise_openai(resp: httpx.Response, where: str) -> None:
90
+ code = None
91
+ msg = None
92
+ try:
93
+ j = resp.json()
94
+ err = j.get("error") if isinstance(j, dict) else None
95
+ if isinstance(err, dict):
96
+ code = err.get("code")
97
+ msg = err.get("message")
98
+ except Exception:
99
+ pass
100
+ detail = msg or resp.text[:500]
101
+ raise ProviderError(
102
+ f"{where}: {detail}",
103
+ status_code=resp.status_code,
104
+ code=str(code) if code else None,
105
+ )
@@ -0,0 +1 @@
1
+ __all__ = ["base", "factory", "sqlite"]
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+ from ..models import EventRecord, Snapshot
6
+
7
+ # Deprecated: moved to dbl_gateway.ports.store_port
8
+
9
+
10
+ class StorePort(Protocol):
11
+ def append(
12
+ self,
13
+ *,
14
+ kind: str,
15
+ lane: str,
16
+ actor: str,
17
+ intent_type: str,
18
+ stream_id: str,
19
+ correlation_id: str,
20
+ payload: dict[str, object],
21
+ ) -> EventRecord:
22
+ ...
23
+
24
+ def snapshot(
25
+ self,
26
+ *,
27
+ limit: int,
28
+ offset: int,
29
+ stream_id: str | None = None,
30
+ lane: str | None = None,
31
+ ) -> Snapshot:
32
+ ...
33
+
34
+ def close(self) -> None:
35
+ ...
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ from ..adapters.store_adapter_sqlite import SQLiteStoreAdapter
7
+ from ..ports.store_port import StorePort
8
+
9
+
10
+ def create_store(db_path: Path | None = None) -> StorePort:
11
+ path = db_path or Path(os.getenv("DBL_GATEWAY_DB", ".\\data\\trail.sqlite"))
12
+ return SQLiteStoreAdapter.from_path(path)
@@ -0,0 +1,200 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sqlite3
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from ..digest import event_digest, v_digest
10
+
11
+ from ..models import EventRecord, Snapshot
12
+
13
+
14
+ class SQLiteStore:
15
+ def __init__(self, db_path: Path) -> None:
16
+ self._db_path = db_path
17
+ self._db_path.parent.mkdir(parents=True, exist_ok=True)
18
+ self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
19
+ self._conn.execute("PRAGMA journal_mode=WAL;")
20
+ self._conn.execute("PRAGMA synchronous=NORMAL;")
21
+ self._init_schema()
22
+
23
+ def close(self) -> None:
24
+ self._conn.close()
25
+
26
+ def _init_schema(self) -> None:
27
+ with self._conn:
28
+ self._conn.execute(
29
+ """
30
+ CREATE TABLE IF NOT EXISTS events (
31
+ idx INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ kind TEXT NOT NULL,
33
+ lane TEXT NOT NULL,
34
+ actor TEXT NOT NULL,
35
+ intent_type TEXT NOT NULL,
36
+ stream_id TEXT NOT NULL,
37
+ correlation_id TEXT NOT NULL,
38
+ payload_json TEXT NOT NULL,
39
+ digest TEXT NOT NULL,
40
+ canon_len INTEGER NOT NULL,
41
+ created_at_utc TEXT NOT NULL
42
+ )
43
+ """
44
+ )
45
+ self._ensure_column("events", "lane", "TEXT NOT NULL DEFAULT 'unknown'")
46
+ self._ensure_column("events", "actor", "TEXT NOT NULL DEFAULT 'unknown'")
47
+ self._ensure_column("events", "intent_type", "TEXT NOT NULL DEFAULT ''")
48
+ self._ensure_column("events", "stream_id", "TEXT NOT NULL DEFAULT 'default'")
49
+ self._conn.execute("CREATE INDEX IF NOT EXISTS events_stream_id ON events(stream_id)")
50
+ self._conn.execute("CREATE INDEX IF NOT EXISTS events_lane ON events(lane)")
51
+ self._conn.execute("CREATE INDEX IF NOT EXISTS events_kind ON events(kind)")
52
+ self._conn.execute(
53
+ "CREATE INDEX IF NOT EXISTS events_correlation_id ON events(correlation_id)"
54
+ )
55
+
56
+ def _ensure_column(self, table: str, column: str, ddl: str) -> None:
57
+ cols = self._conn.execute(f"PRAGMA table_info({table})").fetchall()
58
+ existing = {row[1] for row in cols}
59
+ if column in existing:
60
+ return
61
+ self._conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {ddl}")
62
+
63
+ def append(
64
+ self,
65
+ *,
66
+ kind: str,
67
+ lane: str,
68
+ actor: str,
69
+ intent_type: str,
70
+ stream_id: str,
71
+ correlation_id: str,
72
+ payload: dict[str, object],
73
+ ) -> EventRecord:
74
+ digest_ref, canon_len = event_digest(kind, correlation_id, payload)
75
+ payload_json = json.dumps(
76
+ payload,
77
+ ensure_ascii=True,
78
+ sort_keys=True,
79
+ separators=(",", ":"),
80
+ allow_nan=False,
81
+ )
82
+ created_at = datetime.now(timezone.utc).isoformat()
83
+ with self._conn:
84
+ self._conn.execute(
85
+ """
86
+ INSERT INTO events (
87
+ kind,
88
+ lane,
89
+ actor,
90
+ intent_type,
91
+ stream_id,
92
+ correlation_id,
93
+ payload_json,
94
+ digest,
95
+ canon_len,
96
+ created_at_utc
97
+ )
98
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
99
+ """,
100
+ (
101
+ kind,
102
+ lane,
103
+ actor,
104
+ intent_type,
105
+ stream_id,
106
+ correlation_id,
107
+ payload_json,
108
+ digest_ref,
109
+ canon_len,
110
+ created_at,
111
+ ),
112
+ )
113
+ cur = self._conn.execute("SELECT idx FROM events WHERE rowid = last_insert_rowid()")
114
+ row = cur.fetchone()
115
+ idx = int(row[0]) if row else 0
116
+ index = max(0, idx - 1)
117
+ return {
118
+ "index": index,
119
+ "kind": kind,
120
+ "lane": lane,
121
+ "actor": actor,
122
+ "intent_type": intent_type,
123
+ "stream_id": stream_id,
124
+ "correlation_id": correlation_id,
125
+ "payload": payload,
126
+ "digest": digest_ref,
127
+ "canon_len": canon_len,
128
+ }
129
+
130
+ def snapshot(
131
+ self,
132
+ *,
133
+ limit: int,
134
+ offset: int,
135
+ stream_id: str | None = None,
136
+ lane: str | None = None,
137
+ ) -> Snapshot:
138
+ events = self._fetch_events(limit=limit, offset=offset, stream_id=stream_id, lane=lane)
139
+ length = self._count_events()
140
+ v_digest_value = self._v_digest_all()
141
+ return {
142
+ "length": length,
143
+ "offset": offset,
144
+ "limit": limit,
145
+ "v_digest": v_digest_value,
146
+ "events": events,
147
+ }
148
+
149
+ def _fetch_events(
150
+ self,
151
+ *,
152
+ limit: int,
153
+ offset: int,
154
+ stream_id: str | None,
155
+ lane: str | None,
156
+ ) -> list[EventRecord]:
157
+ query = (
158
+ "SELECT idx, kind, lane, actor, intent_type, stream_id, correlation_id, "
159
+ "payload_json, digest, canon_len FROM events"
160
+ )
161
+ params: list[Any] = []
162
+ filters: list[str] = []
163
+ if stream_id:
164
+ filters.append("stream_id = ?")
165
+ params.append(stream_id)
166
+ if lane:
167
+ filters.append("lane = ?")
168
+ params.append(lane)
169
+ if filters:
170
+ query += " WHERE " + " AND ".join(filters)
171
+ query += " ORDER BY idx ASC LIMIT ? OFFSET ?"
172
+ params.extend([limit, offset])
173
+ rows = self._conn.execute(query, params).fetchall()
174
+ events: list[EventRecord] = []
175
+ for row in rows:
176
+ payload = json.loads(row[7])
177
+ events.append(
178
+ {
179
+ "index": max(0, int(row[0]) - 1),
180
+ "kind": str(row[1]),
181
+ "lane": str(row[2]),
182
+ "actor": str(row[3]),
183
+ "intent_type": str(row[4]),
184
+ "stream_id": str(row[5]),
185
+ "correlation_id": str(row[6]),
186
+ "payload": payload,
187
+ "digest": str(row[8]),
188
+ "canon_len": int(row[9]),
189
+ }
190
+ )
191
+ return events
192
+
193
+ def _count_events(self) -> int:
194
+ row = self._conn.execute("SELECT COUNT(*) FROM events").fetchone()
195
+ return int(row[0]) if row else 0
196
+
197
+ def _v_digest_all(self) -> str:
198
+ rows = self._conn.execute("SELECT idx, digest FROM events ORDER BY idx ASC").fetchall()
199
+ indexed = [(max(0, int(idx) - 1), str(digest)) for idx, digest in rows]
200
+ return v_digest(indexed)
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping, TypedDict
4
+
5
+
6
+ INTERFACE_VERSION = 1
7
+
8
+
9
+ class IntentPayload(TypedDict):
10
+ stream_id: str
11
+ lane: str
12
+ actor: str
13
+ intent_type: str
14
+ payload: dict[str, Any]
15
+ requested_model_id: str | None
16
+
17
+
18
+ class IntentEnvelope(TypedDict):
19
+ interface_version: int
20
+ correlation_id: str
21
+ payload: IntentPayload
22
+
23
+
24
+ def parse_intent_envelope(body: Mapping[str, Any]) -> IntentEnvelope:
25
+ interface_version = body.get("interface_version")
26
+ if not isinstance(interface_version, int):
27
+ raise ValueError("interface_version must be an int")
28
+ if interface_version != INTERFACE_VERSION:
29
+ raise ValueError("unsupported interface_version")
30
+ correlation_id = body.get("correlation_id")
31
+ if not isinstance(correlation_id, str) or correlation_id.strip() == "":
32
+ raise ValueError("correlation_id must be a non-empty string")
33
+ payload = body.get("payload")
34
+ if not isinstance(payload, Mapping):
35
+ raise ValueError("payload must be an object")
36
+ stream_id = payload.get("stream_id")
37
+ lane = payload.get("lane")
38
+ actor = payload.get("actor")
39
+ intent_type = payload.get("intent_type")
40
+ inner_payload = payload.get("payload")
41
+ if not isinstance(stream_id, str) or stream_id.strip() == "":
42
+ raise ValueError("payload.stream_id must be a non-empty string")
43
+ if not isinstance(lane, str) or lane.strip() == "":
44
+ raise ValueError("payload.lane must be a non-empty string")
45
+ if not isinstance(actor, str) or actor.strip() == "":
46
+ raise ValueError("payload.actor must be a non-empty string")
47
+ if not isinstance(intent_type, str) or intent_type.strip() == "":
48
+ raise ValueError("payload.intent_type must be a non-empty string")
49
+ if not isinstance(inner_payload, Mapping):
50
+ raise ValueError("payload.payload must be an object")
51
+ requested_model_id = payload.get("requested_model_id")
52
+ if requested_model_id is not None and not isinstance(requested_model_id, str):
53
+ raise ValueError("payload.requested_model_id must be a string")
54
+ return {
55
+ "interface_version": interface_version,
56
+ "correlation_id": correlation_id.strip(),
57
+ "payload": {
58
+ "stream_id": stream_id.strip(),
59
+ "lane": lane.strip(),
60
+ "actor": actor.strip(),
61
+ "intent_type": intent_type.strip(),
62
+ "payload": dict(inner_payload),
63
+ "requested_model_id": requested_model_id.strip() if isinstance(requested_model_id, str) else None,
64
+ },
65
+ }
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: dbl-gateway
3
+ Version: 0.3.2
4
+ Summary: DBL Gateway
5
+ License: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: dbl-core<0.4.0,>=0.3.2
10
+ Requires-Dist: dbl-policy<0.2.0,>=0.1.0
11
+ Requires-Dist: dbl-main<0.4.0,>=0.3.0
12
+ Requires-Dist: dbl-ingress<0.2.0,>=0.1.1
13
+ Requires-Dist: kl-kernel-logic<0.6.0,>=0.5.0
14
+ Requires-Dist: fastapi>=0.110.0
15
+ Requires-Dist: uvicorn>=0.27.0
16
+ Requires-Dist: httpx>=0.27.0
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest<9,>=8; extra == "dev"
19
+ Requires-Dist: ruff<1,>=0.5; extra == "dev"
20
+ Requires-Dist: mypy<2,>=1.10; extra == "dev"
21
+ Requires-Dist: dbl-reference>=0.2.0; extra == "dev"
22
+ Provides-Extra: oidc
23
+ Requires-Dist: python-jose[cryptography]<4,>=3.3; extra == "oidc"
24
+ Dynamic: license-file
25
+
26
+ # DBL Gateway 0.3.2
27
+
28
+ Authoritative DBL and KL gateway. This service is the single writer for append-only trails,
29
+ applies policy via dbl-policy, and executes via kl-kernel-logic. UI and boundary services
30
+ consume its snapshots and emit INTENT only.
31
+
32
+ This release stabilizes the 0.3.x stackline and does not introduce new wire contracts.
33
+
34
+ Compatible stack versions:
35
+ - dbl-core==0.3.2
36
+ - dbl-policy==0.1.0
37
+ - dbl-main==0.3.0
38
+ - kl-kernel-logic==0.5.0
39
+
40
+ ## Quickstart (PowerShell)
41
+
42
+ ```powershell
43
+ py -3.11 -m venv .venv
44
+ .venv\Scripts\Activate.ps1
45
+ python -m pip install -e ".[dev]"
46
+ ```
47
+
48
+ Run the gateway:
49
+ ```powershell
50
+ dbl-gateway serve --db .\data\trail.sqlite --host 127.0.0.1 --port 8010
51
+ ```
52
+
53
+ Run with uvicorn:
54
+ ```powershell
55
+ $env:DBL_GATEWAY_DB=".\data\trail.sqlite"
56
+ py -3.11 -m uvicorn dbl_gateway.app:app --host 127.0.0.1 --port 8010
57
+ ```
58
+
59
+ ## Endpoints
60
+
61
+ Write:
62
+ - POST `/ingress/intent`
63
+
64
+ Read:
65
+ - GET `/snapshot`
66
+ - GET `/capabilities`
67
+ - GET `/healthz`
68
+
69
+ ## Environment contract
70
+
71
+ See `docs/env_contract.md`.
72
+
73
+ ## Notes
74
+
75
+ - The gateway is the only component that performs governance and execution.
76
+ - All stabilization is expressed explicitly via DECISION events.
77
+ - Boundary and UI clients do not import dbl-core or dbl-policy.
78
+ - The gateway uses dbl-core for canonicalization and digest computation.
@@ -0,0 +1,33 @@
1
+ dbl_gateway/__init__.py,sha256=QjO_Lv-rNZTYa-KPi3CuEb761dTAQ3g_sUOdjAevUIo,19
2
+ dbl_gateway/admission.py,sha256=Ouzb0f-2HFRjaoan0IuXXVXV3SE-TkUbUxIvhv3k4_Q,2568
3
+ dbl_gateway/app.py,sha256=IBXjUXR9A-Jls2TlEWVjeUdCnzstk1dLtvRL_hSXL-s,18435
4
+ dbl_gateway/auth.py,sha256=zdPLt-M8QrdbGgEIQOzNdLhS8M4lE-L_I6mbyKWrA50,9745
5
+ dbl_gateway/capabilities.py,sha256=Pai3TtE5HseTp8dWvQ56ctJzR33rJbL2BUzcGHKFtNk,2340
6
+ dbl_gateway/digest.py,sha256=Qk9qg9cMCLGDWdsvW9tqnjB99YDjC77QwbAM_6_ExxU,1056
7
+ dbl_gateway/execution.py,sha256=y0lCozH2-tCHzfLMOrUg6klv66cLsZnUDKUfSF5jJYk,376
8
+ dbl_gateway/governance.py,sha256=GdHckCHxYfpHrB0F43dvWbztStOCgzNh5wINeYtyFx4,456
9
+ dbl_gateway/models.py,sha256=uZA_JhWOTp1Hfd11XtfQZ_7mLZIsojZxw5GiXBpwjN8,411
10
+ dbl_gateway/projection.py,sha256=GRk0LreZPon0r2wHMom7Uo2-p4mTs5CGcJ27mHoQb2U,1393
11
+ dbl_gateway/wire_contract.py,sha256=qBqqjn-gn851o35Uozm6UXC5cBdF1IdSZ4IHwgAfDoY,2561
12
+ dbl_gateway/adapters/__init__.py,sha256=c4tpiK-xLrOAnULXWFGY46cVXGD4Y9x0Fk34ymVRhwU,253
13
+ dbl_gateway/adapters/execution_adapter_kl.py,sha256=q7JzXliPF83PW-5f3iD2ALR95X-4U9zwLgSYtkGSBC8,4551
14
+ dbl_gateway/adapters/policy_adapter_dbl_policy.py,sha256=yJLZFVZbn7yXKJKeK2m97kcdYNIw_GGACtKv0c7jRUA,3089
15
+ dbl_gateway/adapters/store_adapter_sqlite.py,sha256=KM-rjZrYtvBf0oHExZ6aKfq6wurC3QzkoTTFDv4cIeQ,1228
16
+ dbl_gateway/ports/__init__.py,sha256=cVdnUJacFE-UvHriNfSulJrpMstbhwibdz4n6LMwYMU,261
17
+ dbl_gateway/ports/execution_port.py,sha256=ooHyFS2xoPE1uV9ZT7vj3O3nbM5lffLVrMcN3KcYqj4,498
18
+ dbl_gateway/ports/policy_port.py,sha256=z3zhzw_EOKFaXyDcq4rm02ts6F7hzJsScB_23xI1f_I,436
19
+ dbl_gateway/ports/store_port.py,sha256=UfJMVtAdp4ObPj35EzhP2vZlGtVlx7U3gC5-qMIlcU8,614
20
+ dbl_gateway/providers/__init__.py,sha256=yUmn-w9JEaQ0odrBzC2i9oXYAFRWoHtGt-7m0YlYCsU,44
21
+ dbl_gateway/providers/anthropic.py,sha256=yE3EagKV_wEoXSbVzghrrmt7fwJr-PARxzY384Zz_3s,1765
22
+ dbl_gateway/providers/errors.py,sha256=wF6cQz8Xpc0euwsxDDwo2lZpNzWT5t79-NKQJ3yJNe8,236
23
+ dbl_gateway/providers/openai.py,sha256=a3aPuDWT0jHGtTtkrv1HJKHkmBqiJcwPoLeGjbQRMUo,3231
24
+ dbl_gateway/store/__init__.py,sha256=FlMtW1cV8HO7eqEveaB-YeoUOkXKGGE-AisbQx6uSnQ,40
25
+ dbl_gateway/store/base.py,sha256=R7OzghriIZeCeU5uNG2z2g_gUPpABncSDi9ctDlCk8g,667
26
+ dbl_gateway/store/factory.py,sha256=YkYWKPKfceNxy30SHQxl7MEDsAVyIZFBmRIdeyrB2t8,365
27
+ dbl_gateway/store/sqlite.py,sha256=4j2rYZyGtjfBtoIs25gk9bIncQFtI3r2NgYQcrydcoI,6916
28
+ dbl_gateway-0.3.2.dist-info/licenses/LICENSE,sha256=mEyqjxpmVyq7poufV6nrAhQGN9Qz11eOjrZAyZsH3wc,1070
29
+ dbl_gateway-0.3.2.dist-info/METADATA,sha256=8xs7elrgoGx2o0BE_V3M2cuwQ-5O1-0c2kTvnGu4jSg,2147
30
+ dbl_gateway-0.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
+ dbl_gateway-0.3.2.dist-info/entry_points.txt,sha256=Q5_vkct0R-of9dTZX1wfm2cLV5ntFWSzxw78xUb0lNU,53
32
+ dbl_gateway-0.3.2.dist-info/top_level.txt,sha256=FzRFtg710dkEyyNPGCXfRuRDe55_E1ixMxNS4lhD1A4,12
33
+ dbl_gateway-0.3.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dbl-gateway = dbl_gateway.app:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lukas Pfister
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ dbl_gateway