pipe-broker 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.
Files changed (61) hide show
  1. pipe_broker-0.1.0/PKG-INFO +45 -0
  2. pipe_broker-0.1.0/README.md +15 -0
  3. pipe_broker-0.1.0/pyproject.toml +58 -0
  4. pipe_broker-0.1.0/src/pipe/__init__.py +16 -0
  5. pipe_broker-0.1.0/src/pipe/api/__init__.py +10 -0
  6. pipe_broker-0.1.0/src/pipe/api/app.py +55 -0
  7. pipe_broker-0.1.0/src/pipe/api/dev.py +28 -0
  8. pipe_broker-0.1.0/src/pipe/api/routes.py +169 -0
  9. pipe_broker-0.1.0/src/pipe/api/server.py +28 -0
  10. pipe_broker-0.1.0/src/pipe/api/templates/status.html +29 -0
  11. pipe_broker-0.1.0/src/pipe/lib/__init__.py +44 -0
  12. pipe_broker-0.1.0/src/pipe/lib/core/__init__.py +30 -0
  13. pipe_broker-0.1.0/src/pipe/lib/core/base.py +111 -0
  14. pipe_broker-0.1.0/src/pipe/lib/core/chain.py +62 -0
  15. pipe_broker-0.1.0/src/pipe/lib/core/context.py +60 -0
  16. pipe_broker-0.1.0/src/pipe/lib/core/ops.py +62 -0
  17. pipe_broker-0.1.0/src/pipe/lib/core/pipe.py +154 -0
  18. pipe_broker-0.1.0/src/pipe/lib/detectors/__init__.py +11 -0
  19. pipe_broker-0.1.0/src/pipe/lib/detectors/generic.py +118 -0
  20. pipe_broker-0.1.0/src/pipe/lib/detectors/interface.py +25 -0
  21. pipe_broker-0.1.0/src/pipe/lib/experts/__init__.py +14 -0
  22. pipe_broker-0.1.0/src/pipe/lib/experts/generic.py +111 -0
  23. pipe_broker-0.1.0/src/pipe/lib/experts/interface.py +25 -0
  24. pipe_broker-0.1.0/src/pipe/lib/experts/legal/__init__.py +36 -0
  25. pipe_broker-0.1.0/src/pipe/lib/experts/legal/authority.py +26 -0
  26. pipe_broker-0.1.0/src/pipe/lib/experts/legal/catalog.py +98 -0
  27. pipe_broker-0.1.0/src/pipe/lib/experts/legal/operations.py +218 -0
  28. pipe_broker-0.1.0/src/pipe/lib/experts/workflow.py +42 -0
  29. pipe_broker-0.1.0/src/pipe/lib/providers/__init__.py +34 -0
  30. pipe_broker-0.1.0/src/pipe/lib/providers/flights.py +162 -0
  31. pipe_broker-0.1.0/src/pipe/lib/providers/fx.py +116 -0
  32. pipe_broker-0.1.0/src/pipe/lib/providers/geocode.py +120 -0
  33. pipe_broker-0.1.0/src/pipe/lib/providers/interface.py +25 -0
  34. pipe_broker-0.1.0/src/pipe/lib/providers/openrouter.py +118 -0
  35. pipe_broker-0.1.0/src/pipe/lib/providers/satellite.py +177 -0
  36. pipe_broker-0.1.0/src/pipe/lib/providers/stt.py +120 -0
  37. pipe_broker-0.1.0/src/pipe/lib/providers/weather.py +106 -0
  38. pipe_broker-0.1.0/src/pipe/lib/shared/__init__.py +56 -0
  39. pipe_broker-0.1.0/src/pipe/lib/shared/context.py +104 -0
  40. pipe_broker-0.1.0/src/pipe/lib/shared/interface.py +25 -0
  41. pipe_broker-0.1.0/src/pipe/lib/shared/jurisdiction.py +141 -0
  42. pipe_broker-0.1.0/src/pipe/lib/shared/locale.py +95 -0
  43. pipe_broker-0.1.0/src/pipe/lib/shared/registry.py +89 -0
  44. pipe_broker-0.1.0/src/pipe/lib/shared/scope.py +122 -0
  45. pipe_broker-0.1.0/src/pipe/lib/shared/types.py +136 -0
  46. pipe_broker-0.1.0/src/pipe/lib/utils/__init__.py +62 -0
  47. pipe_broker-0.1.0/src/pipe/lib/utils/finance/__init__.py +40 -0
  48. pipe_broker-0.1.0/src/pipe/lib/utils/finance/currency.py +179 -0
  49. pipe_broker-0.1.0/src/pipe/lib/utils/finance/math.py +141 -0
  50. pipe_broker-0.1.0/src/pipe/lib/utils/finance/validate.py +53 -0
  51. pipe_broker-0.1.0/src/pipe/lib/utils/flow.py +77 -0
  52. pipe_broker-0.1.0/src/pipe/lib/utils/geo/__init__.py +22 -0
  53. pipe_broker-0.1.0/src/pipe/lib/utils/geo/bbox.py +61 -0
  54. pipe_broker-0.1.0/src/pipe/lib/utils/geo/distance.py +57 -0
  55. pipe_broker-0.1.0/src/pipe/lib/utils/geo/geohash.py +69 -0
  56. pipe_broker-0.1.0/src/pipe/lib/utils/search/__init__.py +26 -0
  57. pipe_broker-0.1.0/src/pipe/lib/utils/search/relevance/__init__.py +30 -0
  58. pipe_broker-0.1.0/src/pipe/lib/utils/search/relevance/_common.py +103 -0
  59. pipe_broker-0.1.0/src/pipe/lib/utils/search/relevance/aeo.py +220 -0
  60. pipe_broker-0.1.0/src/pipe/lib/utils/search/relevance/geo.py +208 -0
  61. pipe_broker-0.1.0/src/pipe/lib/utils/search/relevance/seo.py +258 -0
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: pipe-broker
3
+ Version: 0.1.0
4
+ Summary: Atomic, data-in/data-out operations for multilingual translation and domain-expertise knowledge export.
5
+ License: MIT
6
+ Keywords: pipe,operations,translation,nlp,fastapi,atomic,streaming
7
+ Author: amaagracodes
8
+ Requires-Python: >=3.11
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Topic :: Text Processing :: Linguistic
17
+ Requires-Dist: babel
18
+ Requires-Dist: dspy (>=3.3,<4)
19
+ Requires-Dist: fastapi
20
+ Requires-Dist: httpx
21
+ Requires-Dist: jinja2
22
+ Requires-Dist: python-multipart
23
+ Requires-Dist: uvicorn
24
+ Project-URL: Documentation, https://github.com/amaagracodes/pipe
25
+ Project-URL: Homepage, https://github.com/amaagracodes/pipe
26
+ Project-URL: Issues, https://github.com/amaagracodes/pipe/issues
27
+ Project-URL: Repository, https://github.com/amaagracodes/pipe
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Pipe
31
+
32
+ Atomic, data-in/data-out operations for multilingual translation and domain-expertise knowledge export.
33
+
34
+ The core library lives in `src/pipe` — installable as `pipe-broker`, imported as `pipe`. A thin, stateless API adapter in `src/api` runs as a Cloudflare Python Worker.
35
+
36
+ Build uses `poetry-core`; dependencies are managed with `uv`.
37
+
38
+ ## Usage
39
+
40
+ ```python
41
+ import pipe
42
+
43
+ pipe.echo("hi")
44
+ ```
45
+
@@ -0,0 +1,15 @@
1
+ # Pipe
2
+
3
+ Atomic, data-in/data-out operations for multilingual translation and domain-expertise knowledge export.
4
+
5
+ The core library lives in `src/pipe` — installable as `pipe-broker`, imported as `pipe`. A thin, stateless API adapter in `src/api` runs as a Cloudflare Python Worker.
6
+
7
+ Build uses `poetry-core`; dependencies are managed with `uv`.
8
+
9
+ ## Usage
10
+
11
+ ```python
12
+ import pipe
13
+
14
+ pipe.echo("hi")
15
+ ```
@@ -0,0 +1,58 @@
1
+ [project]
2
+ name = "pipe-broker"
3
+ version = "0.1.0"
4
+ description = "Atomic, data-in/data-out operations for multilingual translation and domain-expertise knowledge export."
5
+ authors = [{ name = "amaagracodes" }]
6
+ readme = "README.md"
7
+ requires-python = ">=3.11"
8
+ license = { text = "MIT" }
9
+ keywords = ["pipe", "operations", "translation", "nlp", "fastapi", "atomic", "streaming"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Topic :: Software Development :: Libraries :: Python Modules",
18
+ "Topic :: Text Processing :: Linguistic",
19
+ ]
20
+ # All runtime deps are installed together — one install path, no optional
21
+ # extras to forget. The service (Cloud Run, real CPython) always needs the
22
+ # serving layer (FastAPI/Jinja2/uvicorn) and the LLM backend (DSPy, which
23
+ # brings its own LiteLLM client that speaks OpenRouter via openrouter/<model>).
24
+ dependencies = [
25
+ "fastapi",
26
+ "jinja2",
27
+ "uvicorn",
28
+ "dspy>=3.3,<4",
29
+ "httpx",
30
+ "python-multipart",
31
+ "babel",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/amaagracodes/pipe"
36
+ Repository = "https://github.com/amaagracodes/pipe"
37
+ Documentation = "https://github.com/amaagracodes/pipe"
38
+ Issues = "https://github.com/amaagracodes/pipe/issues"
39
+
40
+ # Dev-only tooling (uv reads this): tests + a local ASGI runner.
41
+ # The website is built with MkDocs Material (the same static-site generator
42
+ # DSPy's docs use). It is a docs/build-time dependency only — the runtime
43
+ # service just serves the already-built static output from public/.
44
+ [dependency-groups]
45
+ dev = ["pytest>=8", "uvicorn>=0.30"]
46
+ docs = ["mkdocs-material>=9.5"]
47
+
48
+ [build-system]
49
+ requires = ["poetry-core>=2.0"]
50
+ build-backend = "poetry.core.masonry.api"
51
+
52
+ [tool.poetry]
53
+ packages = [{ include = "pipe", from = "src" }]
54
+
55
+
56
+ [tool.pytest.ini_options]
57
+ # Do not write .pytest_cache or __pycache__ during test runs.
58
+ addopts = "-p no:cacheprovider"
@@ -0,0 +1,16 @@
1
+ """pipe — atomic, data-in/data-out operations for multilingual translation
2
+ and domain-expertise knowledge export.
3
+
4
+ One top-level package with two parts:
5
+ * pipe.lib — the pure, dependency-free core ops (imported here for a flat
6
+ public API: ``import pipe; pipe.echo("hi")``).
7
+ * pipe.api — the optional serving adapter (FastAPI / Cloudflare Worker).
8
+ NOT imported here, so the core stays dependency-free; import it explicitly
9
+ (``from pipe.api.app import create_app``) with the ``[api]`` extra installed.
10
+ """
11
+
12
+ from pipe.lib import Chain, Echo, EchoStream, Pipe, echo, echo_stream
13
+
14
+ __all__ = ["Pipe", "Chain", "Echo", "EchoStream", "echo", "echo_stream"]
15
+
16
+ __version__ = "0.1.0"
@@ -0,0 +1,10 @@
1
+ """pipe.api — a thin, stateless serving layer over the pipe.lib core ops.
2
+
3
+ An *adapter*: it wires the atomic operations in ``pipe.lib`` to an HTTP surface
4
+ (FastAPI) suitable for running as a Cloudflare Python Worker. It owns no
5
+ business logic. Requires the ``[api]`` extra (fastapi, jinja2).
6
+ """
7
+
8
+ from pipe.api.app import create_app
9
+
10
+ __all__ = ["create_app"]
@@ -0,0 +1,55 @@
1
+ """FastAPI application factory.
2
+
3
+ Builds the app and mounts routes. Two frontend paths coexist by design:
4
+
5
+ * Static assets (``public/``) — the landing page and any purely-static files.
6
+ On Cloudflare these are served via the Workers ASSETS binding; locally via
7
+ Starlette's StaticFiles. Best for content with no per-request data.
8
+
9
+ * Jinja2 templates (``pipe/api/templates/``) — for API-driven, server-rendered
10
+ pages that DO need per-request/dynamic data. Kept for API-based sites.
11
+ """
12
+
13
+ import os
14
+ from pathlib import Path
15
+
16
+ from fastapi import FastAPI
17
+ from fastapi.templating import Jinja2Templates
18
+
19
+ import pipe
20
+
21
+ _BASE_DIR = Path(__file__).resolve().parent
22
+ # The static frontend directory. Priced from an env var first (set in the
23
+ # container image to /app/public), falling back to the repo layout for local
24
+ # dev (src/pipe/api/app.py -> repo root is 4 levels up). When the package is
25
+ # pip-installed the repo-relative guess won't exist, which is why the container
26
+ # sets PIPE_PUBLIC_DIR explicitly.
27
+ PUBLIC_DIR = Path(
28
+ os.environ.get("PIPE_PUBLIC_DIR", str(Path(__file__).resolve().parents[3] / "public"))
29
+ )
30
+ # Server-side templates for dynamic, API-driven pages.
31
+ TEMPLATES = Jinja2Templates(directory=str(_BASE_DIR / "templates"))
32
+
33
+
34
+ def create_app() -> FastAPI:
35
+ # When served behind a reverse proxy under a path prefix (e.g.
36
+ # /proxy/8787), set PIPE_ROOT_PATH to that prefix so FastAPI generates the
37
+ # correct URLs for /docs, /openapi.json, etc. Empty (default) = served at
38
+ # the domain root, as on Cloudflare Workers.
39
+ root_path = os.environ.get("PIPE_ROOT_PATH", "")
40
+
41
+ app = FastAPI(
42
+ title="pipe-broker",
43
+ version=pipe.__version__,
44
+ description=(
45
+ "Atomic, data-in/data-out operations for multilingual translation "
46
+ "and domain-expertise knowledge export."
47
+ ),
48
+ root_path=root_path,
49
+ )
50
+
51
+ # Routes are registered separately to keep concerns split.
52
+ from pipe.api.routes import register_routes
53
+
54
+ register_routes(app)
55
+ return app
@@ -0,0 +1,28 @@
1
+ """Local development server.
2
+
3
+ Runs the exact same FastAPI app as the Cloudflare Worker, but via uvicorn so
4
+ it can be tested locally without the Workers/Pyodide runtime.
5
+
6
+ uv run --extra api --with uvicorn python -m pipe.api.dev
7
+
8
+ Binds 0.0.0.0 by default so it is reachable on localhost, LAN, and tailnet.
9
+ Override with PIPE_HOST / PIPE_PORT.
10
+ """
11
+
12
+ import os
13
+
14
+ import uvicorn
15
+
16
+ from pipe.api.app import create_app
17
+
18
+ app = create_app()
19
+
20
+
21
+ def main() -> None:
22
+ host = os.environ.get("PIPE_HOST", "0.0.0.0")
23
+ port = int(os.environ.get("PIPE_PORT", "8787"))
24
+ uvicorn.run(app, host=host, port=port, log_level="info")
25
+
26
+
27
+ if __name__ == "__main__":
28
+ main()
@@ -0,0 +1,169 @@
1
+ """HTTP route handlers.
2
+
3
+ Each handler is thin and async: parse input, call a ``pipe.lib`` atomic op,
4
+ return output. Three response styles coexist:
5
+
6
+ * JSON ops — /echo (buffered)
7
+ * streaming ops — /echo/stream (incremental data stream)
8
+ * server-rendered — /status (Jinja2 template, for API-driven dynamic pages)
9
+
10
+ The static frontend (public/) is mounted last at "/" so API routes and /docs
11
+ take priority; index.html is served at "/".
12
+ """
13
+
14
+ from fastapi import FastAPI, Request, UploadFile
15
+ from fastapi.responses import HTMLResponse, StreamingResponse
16
+ from fastapi.staticfiles import StaticFiles
17
+
18
+ import pipe
19
+ from pipe.api.app import PUBLIC_DIR, TEMPLATES
20
+
21
+
22
+ def register_routes(app: FastAPI) -> None:
23
+ @app.get("/echo", tags=["ops"])
24
+ async def echo(data: str = "") -> dict:
25
+ """Echo atomic op: returns the input unchanged (data in, data out)."""
26
+ return {"data": pipe.echo(data)}
27
+
28
+ @app.get("/echo/stream", tags=["ops"])
29
+ async def echo_stream(data: str = "", chunk_size: int = 16) -> StreamingResponse:
30
+ """Streaming echo: sends the input back incrementally as a data stream."""
31
+ return StreamingResponse(
32
+ pipe.echo_stream(data, chunk_size=chunk_size),
33
+ media_type="text/plain; charset=utf-8",
34
+ )
35
+
36
+ # --- LLM (OpenRouter) -------------------------------------------------
37
+
38
+ @app.post("/llm", tags=["llm"])
39
+ async def llm(prompt: str, model: str = "openai/gpt-4o-mini", max_tokens: int = 256) -> dict:
40
+ """Test route: send a prompt to OpenRouter and return the completion.
41
+
42
+ Reads the key from $OPENROUTER_API_KEY (injected in the deployment).
43
+ Returns a 4xx-shaped JSON error rather than a 500 when the LLM stack or
44
+ key is unavailable, so it's safe to probe.
45
+ """
46
+ from fastapi import HTTPException
47
+
48
+ from pipe.lib.providers import OpenRouterProvider
49
+
50
+ provider = OpenRouterProvider(model=model, max_tokens=max_tokens)
51
+ try:
52
+ text = await provider.acall(prompt)
53
+ except ModuleNotFoundError as exc:
54
+ raise HTTPException(status_code=501, detail=str(exc)) from exc
55
+ except RuntimeError as exc: # missing key
56
+ raise HTTPException(status_code=503, detail=str(exc)) from exc
57
+ except Exception as exc: # upstream/LLM error — don't leak internals
58
+ raise HTTPException(status_code=502, detail=f"LLM call failed: {type(exc).__name__}") from exc
59
+ return {"model": provider.model, "prompt": prompt, "completion": text}
60
+
61
+ # --- legal atomic ops -------------------------------------------------
62
+
63
+ @app.get("/legal/ops", tags=["legal"])
64
+ async def legal_ops() -> dict:
65
+ """List the available atomic legal operations and rule categories."""
66
+ from pipe.lib.experts import legal
67
+
68
+ return {
69
+ "operations": [
70
+ "detect_pii", "spot_clauses", "identify_statutes",
71
+ "extract_citations", "screen_privilege",
72
+ ],
73
+ "categories": sorted(legal.categories()),
74
+ "rule_count": len(legal.RULES),
75
+ }
76
+
77
+ @app.post("/legal/detect", tags=["legal"])
78
+ async def legal_detect(text: str, jurisdiction: str = "us", op: str = "detect_pii") -> dict:
79
+ """Run one atomic legal op over text for a given jurisdiction.
80
+
81
+ ``op`` is one of the names from ``GET /legal/ops``; ``jurisdiction`` is a
82
+ hierarchical code like ``us``, ``us/ny``, ``in/mh`` (optionally with
83
+ facets appended later). Returns the detections found.
84
+ """
85
+ from fastapi import HTTPException
86
+
87
+ from pipe.lib.experts.legal import (
88
+ DetectPII, ExtractCitations, IdentifyStatutes, ScreenPrivilege, SpotClauses,
89
+ )
90
+
91
+ ops = {
92
+ "detect_pii": DetectPII,
93
+ "spot_clauses": SpotClauses,
94
+ "identify_statutes": IdentifyStatutes,
95
+ "extract_citations": ExtractCitations,
96
+ "screen_privilege": ScreenPrivilege,
97
+ }
98
+ op_cls = ops.get(op)
99
+ if op_cls is None:
100
+ raise HTTPException(status_code=400, detail=f"unknown op {op!r}; choose from {sorted(ops)}")
101
+ result = op_cls(jurisdiction)(text)
102
+ return {
103
+ "op": op,
104
+ "jurisdiction": jurisdiction,
105
+ "detections": [
106
+ {
107
+ "kind": d.kind,
108
+ "label": d.label,
109
+ "severity": d.severity.name,
110
+ "span": [d.span.start, d.span.end] if d.span else None,
111
+ }
112
+ for d in result.detections
113
+ ],
114
+ "meta": result.document.meta,
115
+ }
116
+
117
+ # --- speech-to-text (OpenRouter) --------------------------------------
118
+
119
+ @app.post("/stt", tags=["llm"])
120
+ async def stt(
121
+ file: UploadFile,
122
+ model: str = "openai/whisper-large-v3",
123
+ language: str | None = None,
124
+ ) -> dict:
125
+ """Transcribe an uploaded audio file to text (audio bytes in, text out).
126
+
127
+ Accepts a multipart file upload; the audio format is inferred from the
128
+ filename extension (falls back to ``wav``). Reads the key from
129
+ $OPENROUTER_API_KEY. Errors map like /llm (503 missing key, 502 upstream).
130
+ """
131
+ from fastapi import HTTPException
132
+
133
+ from pipe.lib.providers import STT
134
+
135
+ audio = await file.read()
136
+ # Derive the container format from the upload's extension.
137
+ fmt = "wav"
138
+ if file.filename and "." in file.filename:
139
+ fmt = file.filename.rsplit(".", 1)[-1].lower()
140
+
141
+ provider = STT(model=model, language=language)
142
+ try:
143
+ text = await provider.acall(audio, format=fmt)
144
+ except RuntimeError as exc: # missing key
145
+ raise HTTPException(status_code=503, detail=str(exc)) from exc
146
+ except Exception as exc: # upstream/transcription error — don't leak internals
147
+ raise HTTPException(status_code=502, detail=f"STT call failed: {type(exc).__name__}") from exc
148
+ return {"model": provider.model, "format": fmt, "text": text}
149
+
150
+ @app.get("/status", response_class=HTMLResponse, include_in_schema=False)
151
+ async def status(request: Request, data: str = "pong"):
152
+ """Server-rendered (Jinja2) page — kept for dynamic, API-driven sites."""
153
+ return TEMPLATES.TemplateResponse(
154
+ request,
155
+ "status.html",
156
+ {
157
+ "version": pipe.__version__,
158
+ "echoed": pipe.echo(data),
159
+ "ops": ["echo", "echo_stream"],
160
+ },
161
+ )
162
+
163
+ # Static frontend, mounted LAST so the API routes above (and /docs,
164
+ # /openapi.json) match first. html=True serves index.html at "/". The
165
+ # public/ directory ships in the container image (and exists in local dev).
166
+ # The is_dir guard just keeps the app importable in environments where the
167
+ # directory isn't present (e.g. running the ops library without the site).
168
+ if PUBLIC_DIR.is_dir():
169
+ app.mount("/", StaticFiles(directory=str(PUBLIC_DIR), html=True), name="site")
@@ -0,0 +1,28 @@
1
+ """Production ASGI server entrypoint (container / Cloud Run).
2
+
3
+ Cloud Run injects the port to listen on via the ``PORT`` env var (default 8080)
4
+ and expects the process to bind 0.0.0.0. This module exposes the ASGI ``app``
5
+ for ``uvicorn pipe.api.server:app`` and a ``main()`` for direct execution.
6
+ """
7
+
8
+ import os
9
+
10
+ import uvicorn
11
+
12
+ from pipe.api.app import create_app
13
+
14
+ app = create_app()
15
+
16
+
17
+ def main() -> None:
18
+ port = int(os.environ.get("PORT", "8080"))
19
+ uvicorn.run(
20
+ "pipe.api.server:app",
21
+ host="0.0.0.0",
22
+ port=port,
23
+ log_level=os.environ.get("LOG_LEVEL", "info"),
24
+ )
25
+
26
+
27
+ if __name__ == "__main__":
28
+ main()
@@ -0,0 +1,29 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>pipe-broker · status</title>
7
+ <link rel="stylesheet" href="style.css" />
8
+ </head>
9
+ <body>
10
+ <div class="glow" aria-hidden="true"></div>
11
+ <main>
12
+ <section class="hero">
13
+ <span class="pill">server-rendered</span>
14
+ <h1 class="grad">pipe-broker status</h1>
15
+ <p class="sub">
16
+ This page is rendered server-side from a Jinja2 template — the path we
17
+ keep for dynamic, API-driven pages.
18
+ </p>
19
+ <div class="panel" style="max-width:32rem;margin:0 auto;text-align:left">
20
+ <p><strong>Version:</strong> {{ version }}</p>
21
+ <p><strong>Echoed value:</strong> {{ echoed }}</p>
22
+ <p><strong>Ops available:</strong>
23
+ {% for op in ops %}<code>{{ op }}</code>{% if not loop.last %}, {% endif %}{% endfor %}
24
+ </p>
25
+ </div>
26
+ </section>
27
+ </main>
28
+ </body>
29
+ </html>
@@ -0,0 +1,44 @@
1
+ """pipe.lib — the pure, stateless core: atomic data-in/data-out operations.
2
+
3
+ Zero runtime dependencies. No I/O, no framework, no global state. Safe to run
4
+ anywhere Python does, including Cloudflare's Pyodide runtime.
5
+
6
+ The interface primitives live in the modular :mod:`pipe.lib.core` subpackage
7
+ and are re-exported here for a flat import surface. Two ways to use an op:
8
+
9
+ * the interface — subclass :class:`Pipe`, implement ``forward``/``stream``,
10
+ invoke by calling the instance, and chain with ``>>``. This is the
11
+ DSPy-style module pattern: a small, composable, uniformly-called atomic
12
+ layer.
13
+ * the flat helpers — ``echo`` / ``echo_stream`` remain as functions for
14
+ callers that just want data in, data out.
15
+
16
+ Higher layers build on this: the generic modules :mod:`~pipe.lib.shared`,
17
+ :mod:`~pipe.lib.providers`, :mod:`~pipe.lib.experts`, :mod:`~pipe.lib.detectors`,
18
+ and the domain operations in :mod:`~pipe.lib.experts.legal`.
19
+ """
20
+
21
+ from pipe.lib.core import (
22
+ BasePipe,
23
+ Chain,
24
+ Echo,
25
+ EchoStream,
26
+ Pipe,
27
+ PipeMeta,
28
+ echo,
29
+ echo_stream,
30
+ )
31
+
32
+ __all__ = [
33
+ # interface layer
34
+ "BasePipe",
35
+ "Pipe",
36
+ "PipeMeta",
37
+ "Chain",
38
+ # concrete ops
39
+ "Echo",
40
+ "EchoStream",
41
+ # flat function API
42
+ "echo",
43
+ "echo_stream",
44
+ ]
@@ -0,0 +1,30 @@
1
+ """pipe.lib.core — the interface primitives for atomic ops.
2
+
3
+ A modular, DSPy-shaped module system split across small files:
4
+
5
+ * :mod:`~pipe.lib.core.base` — ``BasePipe`` (structural) + ``PipeMeta``.
6
+ * :mod:`~pipe.lib.core.pipe` — ``Pipe``, the atomic callable interface.
7
+ * :mod:`~pipe.lib.core.chain` — ``Chain``, the composite that sequences ops.
8
+ * :mod:`~pipe.lib.core.ops` — concrete ops (``Echo``, ``EchoStream``) and
9
+ the flat ``echo`` / ``echo_stream`` function API.
10
+ """
11
+
12
+ from pipe.lib.core.base import BasePipe, PipeMeta
13
+ from pipe.lib.core.chain import Chain
14
+ from pipe.lib.core.context import current_context, require_context, use_context
15
+ from pipe.lib.core.ops import Echo, EchoStream, echo, echo_stream
16
+ from pipe.lib.core.pipe import Pipe
17
+
18
+ __all__ = [
19
+ "BasePipe",
20
+ "PipeMeta",
21
+ "Pipe",
22
+ "Chain",
23
+ "Echo",
24
+ "EchoStream",
25
+ "echo",
26
+ "echo_stream",
27
+ "current_context",
28
+ "use_context",
29
+ "require_context",
30
+ ]
@@ -0,0 +1,111 @@
1
+ """pipe.lib.core.base — the structural layer + metaclass.
2
+
3
+ DSPy splits its abstraction in two:
4
+
5
+ * ``BaseModule`` — the *structural* layer. It knows nothing about calling;
6
+ it only walks an instance's attributes to discover nested sub-modules,
7
+ enabling introspection, composition and copying.
8
+ * ``Module`` — the *behavioural* layer (see :mod:`pipe.lib.core.pipe`).
9
+
10
+ ``BasePipe`` here is that structural layer for pipe, and ``PipeMeta`` is the
11
+ metaclass that guarantees base attributes exist on every ``Pipe`` — analogous
12
+ to DSPy's ``ProgramMeta``.
13
+
14
+ No third-party imports: safe under Cloudflare's Pyodide runtime.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import copy
20
+ from typing import TYPE_CHECKING, Any
21
+
22
+ if TYPE_CHECKING:
23
+ from pipe.lib.core.pipe import Pipe
24
+
25
+ __all__ = ["BasePipe", "PipeMeta"]
26
+
27
+
28
+ class BasePipe:
29
+ """Structural layer: attribute-walking, discovery, and copying.
30
+
31
+ Analogous to ``dspy.primitives.BaseModule``. Holds no calling logic — it
32
+ only understands how ``Pipe``s nest inside one another so that a composite
33
+ ``Pipe`` can be introspected and copied as a unit.
34
+ """
35
+
36
+ def named_pipes(self) -> list[tuple[str, "Pipe"]]:
37
+ """Return ``(name, pipe)`` for every ``Pipe`` reachable from ``self``.
38
+
39
+ Walks ``__dict__`` one level deep, descending into nested ``Pipe``s and
40
+ into lists/tuples/dicts of ``Pipe``s. Names use dotted / indexed paths
41
+ (``"encoder"``, ``"stages[0]"``, ``"routes['en']"``) so each sub-pipe
42
+ has a stable address. Mirrors ``BaseModule.named_parameters``.
43
+ """
44
+ from pipe.lib.core.pipe import Pipe
45
+
46
+ visited: set[int] = set()
47
+ found: list[tuple[str, "Pipe"]] = []
48
+
49
+ def add(name: str, value: Any) -> None:
50
+ if isinstance(value, Pipe):
51
+ if id(value) in visited:
52
+ return
53
+ visited.add(id(value))
54
+ found.append((name, value))
55
+ # Descend so composites of composites are fully discovered.
56
+ for sub_name, sub in value.named_pipes():
57
+ child = f"{name}.{sub_name}"
58
+ if id(sub) not in visited:
59
+ visited.add(id(sub))
60
+ found.append((child, sub))
61
+ elif isinstance(value, (list, tuple)):
62
+ for i, item in enumerate(value):
63
+ add(f"{name}[{i}]", item)
64
+ elif isinstance(value, dict):
65
+ for key, item in value.items():
66
+ add(f"{name}[{key!r}]", item)
67
+
68
+ for name, value in self.__dict__.items():
69
+ add(name, value)
70
+ return found
71
+
72
+ def pipes(self) -> list["Pipe"]:
73
+ """All nested ``Pipe`` instances (names dropped)."""
74
+ return [p for _, p in self.named_pipes()]
75
+
76
+ def deepcopy(self) -> "BasePipe":
77
+ """Deep copy, falling back to a shallow attribute copy if needed.
78
+
79
+ Mirrors ``BaseModule.deepcopy``: a plain ``copy.deepcopy`` when the
80
+ instance allows it, otherwise a best-effort attribute-by-attribute copy.
81
+ """
82
+ try:
83
+ return copy.deepcopy(self)
84
+ except Exception:
85
+ new = self.__class__.__new__(self.__class__)
86
+ for attr, value in self.__dict__.items():
87
+ try:
88
+ setattr(new, attr, copy.deepcopy(value))
89
+ except Exception:
90
+ setattr(new, attr, copy.copy(value))
91
+ return new
92
+
93
+
94
+ class PipeMeta(type):
95
+ """Metaclass guaranteeing base attributes exist on every ``Pipe``.
96
+
97
+ Analogous to DSPy's ``ProgramMeta``: it runs ``Pipe._base_init`` before the
98
+ subclass ``__init__`` so a subclass that forgets ``super().__init__()``
99
+ still has the attributes the framework relies on (here: ``name``).
100
+ """
101
+
102
+ def __call__(cls, *args: Any, **kwargs: Any) -> Any:
103
+ from pipe.lib.core.pipe import Pipe
104
+
105
+ obj = cls.__new__(cls, *args, **kwargs)
106
+ if isinstance(obj, cls):
107
+ Pipe._base_init(obj)
108
+ cls.__init__(obj, *args, **kwargs)
109
+ if not hasattr(obj, "name"):
110
+ obj.name = cls.__name__
111
+ return obj