tigrbl_api_hpks 0.1.2.dev9__tar.gz → 0.1.2.dev11__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tigrbl_api_hpks
3
- Version: 0.1.2.dev9
3
+ Version: 0.1.2.dev11
4
4
  Summary: High-trust OpenPGP keyserver APIs built on the Tigrbl application engine.
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tigrbl_api_hpks"
3
- version = "0.1.2.dev9"
3
+ version = "0.1.2.dev11"
4
4
  description = "High-trust OpenPGP keyserver APIs built on the Tigrbl application engine."
5
5
  license = { file = "LICENSE" }
6
6
  readme = { file = "README.md", content-type = "text/markdown" }
@@ -5,17 +5,14 @@ from __future__ import annotations
5
5
  import datetime as dt
6
6
  import os
7
7
  import tempfile
8
- from typing import Any, AsyncIterator
8
+ from typing import Any
9
9
  from urllib.parse import parse_qs
10
10
 
11
- from tigrbl import TigrblApp
12
- from tigrbl.router import Router
13
- from tigrbl.router.decorators import route
14
- from tigrbl.engine.shortcuts import engine as build_engine
15
- from tigrbl.responses import Response
11
+ from tigrbl import Router, TigrblApp, Response, Request
12
+ from tigrbl.decorators.router import route
13
+ from tigrbl.shortcuts.engine import engine as build_engine
16
14
  from tigrbl.runtime.status import HTTPException
17
- from tigrbl.security.dependencies import Depends
18
- from tigrbl.requests import Request
15
+
19
16
 
20
17
  from .ops import pks as pks_ops
21
18
  from .tables import OpenPGPKey
@@ -30,6 +27,18 @@ def _normalize_fingerprint(value: str) -> str:
30
27
  return stripped.upper()
31
28
 
32
29
 
30
+ def _query_first(request: Request, key: str, default: str = "") -> str:
31
+ """Extract the first value for a query parameter."""
32
+ values = request.query.get(key, [])
33
+ return values[0] if values else default
34
+
35
+
36
+ def _path_segment(request: Request, index: int) -> str:
37
+ """Extract a path segment by index (0-based, after splitting on '/')."""
38
+ parts = request.path.strip("/").split("/")
39
+ return parts[index] if index < len(parts) else ""
40
+
41
+
33
42
  def _response_text(
34
43
  content: str,
35
44
  *,
@@ -87,17 +96,13 @@ def build_app(
87
96
 
88
97
  router = Router(prefix="/pks")
89
98
 
90
- async def get_session() -> AsyncIterator[Any]:
91
- async with app.engine.asession() as session:
92
- yield session
93
-
94
99
  def _not_found(detail: str = "Not found") -> HTTPException:
95
100
  return HTTPException(status_code=404, detail=detail, headers=HPKS_CORS_HEADERS)
96
101
 
102
+ # ---- Legacy HKP endpoints ----
103
+
97
104
  @route(router, "/add", methods=["POST"])
98
- async def legacy_add(
99
- request: Request, *, db: Any = Depends(get_session)
100
- ) -> Response:
105
+ async def legacy_add(request: Request) -> Response:
101
106
  body = request.body.decode("utf-8")
102
107
  form_data = parse_qs(body, keep_blank_values=True)
103
108
  keytext = form_data.get("keytext", [""])[0]
@@ -107,72 +112,76 @@ def build_app(
107
112
  detail="keytext is required",
108
113
  headers=HPKS_CORS_HEADERS,
109
114
  )
110
- try:
111
- await pks_ops.ingest_armored(db=db, armored=keytext)
112
- except ValueError as exc: # pragma: no cover - defensive
113
- raise HTTPException(
114
- status_code=422, detail=str(exc), headers=HPKS_CORS_HEADERS
115
- ) from exc
115
+ async with app.engine.asession() as db:
116
+ try:
117
+ await pks_ops.ingest_armored(db=db, armored=keytext)
118
+ except ValueError as exc:
119
+ raise HTTPException(
120
+ status_code=422, detail=str(exc), headers=HPKS_CORS_HEADERS
121
+ ) from exc
116
122
  return _response_text("OK\n", headers=HPKS_CORS_HEADERS)
117
123
 
118
124
  @route(router, "/lookup", methods=["GET"])
119
- async def legacy_lookup(request: Request, *, db: Any = Depends(get_session)):
120
- op = request.query_param("op") or ""
121
- search = request.query_param("search") or ""
125
+ async def legacy_lookup(request: Request):
126
+ op = _query_first(request, "op")
127
+ search = _query_first(request, "search")
122
128
  op_name = op.lower()
123
129
  normalized = _normalize_fingerprint(search)
124
- if op_name == "index":
125
- results = await pks_ops.search_index(db=db, search=search)
126
- if not results:
127
- raise _not_found()
128
- body = pks_ops.render_legacy_index(results, search=search)
129
- return _response_text(body, headers=HPKS_CORS_HEADERS)
130
- if op_name == "get":
131
- if len(normalized) <= 8:
132
- raise _not_found()
133
- record = await pks_ops.lookup_by_fingerprint(db=db, fingerprint=normalized)
134
- if record is None:
135
- raise _not_found()
136
- return _response_text(
137
- record.ascii_armored,
138
- media_type="application/pgp-keys",
139
- headers=HPKS_CORS_HEADERS,
140
- )
141
- if op_name == "hget":
142
- record = await pks_ops.lookup_by_email_hash(db=db, email_hash=search)
143
- if record is None:
144
- raise _not_found()
145
- return _response_text(
146
- record.ascii_armored,
147
- media_type="application/pgp-keys",
148
- headers=HPKS_CORS_HEADERS,
149
- )
130
+ async with app.engine.asession() as db:
131
+ if op_name == "index":
132
+ results = await pks_ops.search_index(db=db, search=search)
133
+ if not results:
134
+ raise _not_found()
135
+ body = pks_ops.render_legacy_index(results, search=search)
136
+ return _response_text(body, headers=HPKS_CORS_HEADERS)
137
+ if op_name == "get":
138
+ if len(normalized) <= 8:
139
+ raise _not_found()
140
+ record = await pks_ops.lookup_by_fingerprint(
141
+ db=db, fingerprint=normalized
142
+ )
143
+ if record is None:
144
+ raise _not_found()
145
+ return _response_text(
146
+ record.ascii_armored,
147
+ media_type="application/pgp-keys",
148
+ headers=HPKS_CORS_HEADERS,
149
+ )
150
+ if op_name == "hget":
151
+ record = await pks_ops.lookup_by_email_hash(db=db, email_hash=search)
152
+ if record is None:
153
+ raise _not_found()
154
+ return _response_text(
155
+ record.ascii_armored,
156
+ media_type="application/pgp-keys",
157
+ headers=HPKS_CORS_HEADERS,
158
+ )
150
159
  raise HTTPException(
151
160
  status_code=400,
152
161
  detail=f"Unsupported legacy op '{op}'",
153
162
  headers=HPKS_CORS_HEADERS,
154
163
  )
155
164
 
165
+ # ---- V2 endpoints ----
166
+
167
+ # Pattern: /pks/{fingerprint} — the fingerprint is the 2nd segment
156
168
  @route(router, "/{fingerprint}", methods=["POST"])
157
- async def merge_fingerprint(
158
- request: Request,
159
- *,
160
- fingerprint: str,
161
- db: Any = Depends(get_session),
162
- ) -> Response:
169
+ async def merge_fingerprint(request: Request) -> Response:
170
+ fingerprint = _path_segment(request, 1)
163
171
  payload = await request.json() or {}
164
- try:
165
- record = await pks_ops.merge_json_payload(
166
- db=db, fingerprint=fingerprint, payload=payload
167
- )
168
- except ValueError as exc:
169
- raise HTTPException(
170
- status_code=422, detail=str(exc), headers=HPKS_CORS_HEADERS
171
- ) from exc
172
+ async with app.engine.asession() as db:
173
+ try:
174
+ record = await pks_ops.merge_json_payload(
175
+ db=db, fingerprint=fingerprint, payload=payload
176
+ )
177
+ except ValueError as exc:
178
+ raise HTTPException(
179
+ status_code=422, detail=str(exc), headers=HPKS_CORS_HEADERS
180
+ ) from exc
172
181
  return _response_json(pks_ops.to_v2_document(record), headers=HPKS_CORS_HEADERS)
173
182
 
174
183
  @route(router, "/v2/add", methods=["POST"])
175
- async def v2_add(request: Request, *, db: Any = Depends(get_session)) -> Response:
184
+ async def v2_add(request: Request) -> Response:
176
185
  content_type = request.headers.get("content-type", "")
177
186
  if "application/pgp-keys" not in content_type:
178
187
  raise HTTPException(
@@ -185,25 +194,32 @@ def build_app(
185
194
  raise HTTPException(
186
195
  status_code=400, detail="Empty payload", headers=HPKS_CORS_HEADERS
187
196
  )
188
- try:
189
- summary = await pks_ops.ingest_binary(db=db, bundle=bundle)
190
- except ValueError as exc:
191
- raise HTTPException(
192
- status_code=422, detail=str(exc), headers=HPKS_CORS_HEADERS
193
- ) from exc
197
+ async with app.engine.asession() as db:
198
+ try:
199
+ summary = await pks_ops.ingest_binary(db=db, bundle=bundle)
200
+ except ValueError as exc:
201
+ raise HTTPException(
202
+ status_code=422, detail=str(exc), headers=HPKS_CORS_HEADERS
203
+ ) from exc
194
204
  return _response_json(summary, status_code=202, headers=HPKS_CORS_HEADERS)
195
205
 
206
+ # Pattern: /pks/v2/index/{query} — the query is the 4th segment
196
207
  @route(router, "/v2/index/{query}", methods=["GET"])
197
- async def v2_index(query: str, *, db: Any = Depends(get_session)) -> Response:
198
- results = await pks_ops.search_index(db=db, search=query)
208
+ async def v2_index(request: Request) -> Response:
209
+ query = _path_segment(request, 3)
210
+ async with app.engine.asession() as db:
211
+ results = await pks_ops.search_index(db=db, search=query)
199
212
  if not results:
200
213
  raise _not_found()
201
214
  payload = [pks_ops.to_v2_document(rec) for rec in results]
202
215
  return _response_json(payload, headers=HPKS_CORS_HEADERS)
203
216
 
217
+ # Pattern: /pks/v2/vfpget/{fingerprint} — fingerprint is the 4th segment
204
218
  @route(router, "/v2/vfpget/{fingerprint}", methods=["GET"])
205
- async def vfpget(fingerprint: str, *, db: Any = Depends(get_session)) -> Response:
206
- record = await pks_ops.lookup_by_fingerprint(db=db, fingerprint=fingerprint)
219
+ async def vfpget(request: Request) -> Response:
220
+ fingerprint = _path_segment(request, 3)
221
+ async with app.engine.asession() as db:
222
+ record = await pks_ops.lookup_by_fingerprint(db=db, fingerprint=fingerprint)
207
223
  if record is None:
208
224
  raise _not_found()
209
225
  return _response_binary(
@@ -212,9 +228,12 @@ def build_app(
212
228
  headers=HPKS_CORS_HEADERS,
213
229
  )
214
230
 
231
+ # Pattern: /pks/v2/kidget/{keyid} — keyid is the 4th segment
215
232
  @route(router, "/v2/kidget/{keyid}", methods=["GET"])
216
- async def kidget(keyid: str, *, db: Any = Depends(get_session)) -> Response:
217
- record = await pks_ops.lookup_by_keyid(db=db, key_id=keyid)
233
+ async def kidget(request: Request) -> Response:
234
+ keyid = _path_segment(request, 3)
235
+ async with app.engine.asession() as db:
236
+ record = await pks_ops.lookup_by_keyid(db=db, key_id=keyid)
218
237
  if record is None or (record.version and record.version > 4):
219
238
  raise _not_found()
220
239
  return _response_binary(
@@ -223,9 +242,12 @@ def build_app(
223
242
  headers=HPKS_CORS_HEADERS,
224
243
  )
225
244
 
245
+ # Pattern: /pks/v2/authget/{identifier} — identifier is the 4th segment
226
246
  @route(router, "/v2/authget/{identifier}", methods=["GET"])
227
- async def authget(identifier: str, *, db: Any = Depends(get_session)) -> Response:
228
- matches = await pks_ops.search_index(db=db, search=identifier)
247
+ async def authget(request: Request) -> Response:
248
+ identifier = _path_segment(request, 3)
249
+ async with app.engine.asession() as db:
250
+ matches = await pks_ops.search_index(db=db, search=identifier)
229
251
  lowered = identifier.lower()
230
252
  record = next(
231
253
  (
@@ -243,10 +265,10 @@ def build_app(
243
265
  headers=HPKS_CORS_HEADERS,
244
266
  )
245
267
 
268
+ # Pattern: /pks/v2/prefixlog/{since} — since is the 4th segment
246
269
  @route(router, "/v2/prefixlog/{since}", methods=["GET"])
247
- async def prefixlog_route(
248
- since: str, *, db: Any = Depends(get_session)
249
- ) -> Response:
270
+ async def prefixlog_route(request: Request) -> Response:
271
+ since = _path_segment(request, 3)
250
272
  try:
251
273
  parsed = dt.datetime.fromisoformat(since)
252
274
  except ValueError:
@@ -260,12 +282,13 @@ def build_app(
260
282
  ) from None
261
283
  if parsed.tzinfo is None:
262
284
  parsed = parsed.replace(tzinfo=dt.timezone.utc)
263
- prefixes = await pks_ops.prefix_log(db=db, since=parsed)
285
+ async with app.engine.asession() as db:
286
+ prefixes = await pks_ops.prefix_log(db=db, since=parsed)
264
287
  body = "\r\n".join(prefixes)
265
288
  return _response_text(body, headers=HPKS_CORS_HEADERS)
266
289
 
267
290
  @route(router, "/v2/tokensend", methods=["POST"])
268
- async def tokensend(_: Request) -> Response:
291
+ async def tokensend(request: Request) -> Response:
269
292
  return Response(status_code=501, headers=list(HPKS_CORS_HEADERS.items()))
270
293
 
271
294
  app.include_router(router)
@@ -437,7 +437,11 @@ async def _merge_parsed_key(*, db: Any, parsed: ParsedKey) -> OpenPGPKey:
437
437
  payload["bits"] = parsed.bits or existing.bits
438
438
  payload["primary_uid"] = parsed.primary_uid or existing.primary_uid
439
439
  payload["version"] = parsed.version or existing.version
440
- ctx = {"db": db, "payload": payload}
440
+ ctx = {
441
+ "db": db,
442
+ "payload": payload,
443
+ "path_params": {"item_id": parsed.fingerprint},
444
+ }
441
445
  record = await OpenPGPKey.handlers.merge.handler(ctx)
442
446
  await _commit_session(db)
443
447
  return record
@@ -540,7 +544,11 @@ async def merge_json_payload(
540
544
  "primary_uid": primary_uid or existing.primary_uid,
541
545
  "version": payload.get("version") or existing.version,
542
546
  }
543
- ctx = {"db": db, "payload": merged_payload}
547
+ ctx = {
548
+ "db": db,
549
+ "payload": merged_payload,
550
+ "path_params": {"item_id": normalized},
551
+ }
544
552
  record = await OpenPGPKey.handlers.merge.handler(ctx)
545
553
  await _commit_session(db)
546
554
  return record
@@ -6,7 +6,7 @@ import datetime as dt
6
6
 
7
7
  from tigrbl.orm.mixins import Mergeable, Timestamped
8
8
  from tigrbl.specs import ColumnSpec, F, IO, S, acol
9
- from tigrbl.table import Base
9
+ from tigrbl.orm.tables import Base
10
10
  from tigrbl.types import (
11
11
  Boolean,
12
12
  Integer,