pyweb-stack 0.1.0__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.
Files changed (54) hide show
  1. pyweb/__init__.py +71 -0
  2. pyweb/app.py +37 -0
  3. pyweb/app_loader.py +139 -0
  4. pyweb/asgi.py +71 -0
  5. pyweb/auth.py +701 -0
  6. pyweb/bench.py +101 -0
  7. pyweb/browser.py +232 -0
  8. pyweb/build.py +295 -0
  9. pyweb/cache.py +201 -0
  10. pyweb/cli/__init__.py +465 -0
  11. pyweb/cli/__main__.py +3 -0
  12. pyweb/compiler/__init__.py +5 -0
  13. pyweb/compiler/ast.py +45 -0
  14. pyweb/compiler/codegen/__init__.py +9 -0
  15. pyweb/compiler/codegen/ir.py +50 -0
  16. pyweb/compiler/errors.py +29 -0
  17. pyweb/compiler/lower.py +733 -0
  18. pyweb/compiler/parser.py +532 -0
  19. pyweb/compiler/pipeline.py +158 -0
  20. pyweb/compiler/pyjs.py +1087 -0
  21. pyweb/compiler/rpc.py +35 -0
  22. pyweb/context.py +193 -0
  23. pyweb/css.py +115 -0
  24. pyweb/db/__init__.py +652 -0
  25. pyweb/db/migrate.py +143 -0
  26. pyweb/decorators.py +49 -0
  27. pyweb/deploy.py +102 -0
  28. pyweb/forms.py +108 -0
  29. pyweb/hosting.py +139 -0
  30. pyweb/jobs.py +248 -0
  31. pyweb/live.py +88 -0
  32. pyweb/lsp.py +214 -0
  33. pyweb/models.py +269 -0
  34. pyweb/npm.py +270 -0
  35. pyweb/observability.py +313 -0
  36. pyweb/platform.py +69 -0
  37. pyweb/plugins.py +112 -0
  38. pyweb/py.typed +0 -0
  39. pyweb/realtime.py +189 -0
  40. pyweb/rpc.py +137 -0
  41. pyweb/runtime/browser/runtime.js +1151 -0
  42. pyweb/runtime/server/__init__.py +445 -0
  43. pyweb/security.py +162 -0
  44. pyweb/serve.py +335 -0
  45. pyweb/ssr.py +260 -0
  46. pyweb/sync.py +118 -0
  47. pyweb/testing.py +154 -0
  48. pyweb/uploads.py +94 -0
  49. pyweb_stack-0.1.0.dist-info/METADATA +198 -0
  50. pyweb_stack-0.1.0.dist-info/RECORD +54 -0
  51. pyweb_stack-0.1.0.dist-info/WHEEL +5 -0
  52. pyweb_stack-0.1.0.dist-info/entry_points.txt +2 -0
  53. pyweb_stack-0.1.0.dist-info/licenses/LICENSE +21 -0
  54. pyweb_stack-0.1.0.dist-info/top_level.txt +1 -0
pyweb/rpc.py ADDED
@@ -0,0 +1,137 @@
1
+ """Production RPC protocol: error codes, tracing, retries, rate limits.
2
+
3
+ Wire format (unchanged from prototype, extended)::
4
+
5
+ POST /__pyweb/rpc/<name>
6
+ Headers: Content-Type: application/json
7
+ X-Request-Id (optional, echoed back)
8
+ traceparent (optional W3C trace context, echoed/propagated)
9
+ X-CSRF-Token (required for cookie-authed calls to protected RPCs)
10
+ Authorization: Bearer <session> (or pyweb_session cookie)
11
+ Body: {"args": {...}}
12
+
13
+ Success: 200 {"result": ...}
14
+ Stream: 200 application/x-ndjson, one {"chunk": ...} per line
15
+ Error: <status> {"error": {"code": ..., "message": ...}}
16
+
17
+ Why this exists: automatic RPC must feel local *without* hiding failure
18
+ semantics. Every failure maps to a stable machine-readable ``code`` plus
19
+ an HTTP status, so browser code, retries, and observability can react
20
+ correctly. Alternatives considered: gRPC/Connect (heavier, worse browser
21
+ story), tRPC-style (JS-only). Plain JSON-over-HTTP keeps ``curl``,
22
+ proxies, and edge caches working.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import random
28
+ import threading
29
+ import time
30
+ import uuid
31
+
32
+
33
+ class Code:
34
+ VALIDATION = "validation_error" # 422 — bad args
35
+ AUTH = "unauthenticated" # 401 — no/invalid session
36
+ FORBIDDEN = "forbidden" # 403 — missing role/permission
37
+ NOT_FOUND = "not_found" # 404 — unknown rpc
38
+ RATE_LIMIT = "rate_limited" # 429 — slow down
39
+ TIMEOUT = "timeout" # 504 — fn exceeded rpc_timeout
40
+ CONFLICT = "conflict" # 409 — optimistic-lock mismatch
41
+ CSRF = "csrf_failed" # 403 — bad/missing CSRF token
42
+ INTERNAL = "internal" # 500 — anything else
43
+
44
+
45
+ _STATUS = {
46
+ Code.VALIDATION: 422, Code.AUTH: 401, Code.FORBIDDEN: 403,
47
+ Code.NOT_FOUND: 404, Code.RATE_LIMIT: 429, Code.TIMEOUT: 504,
48
+ Code.CONFLICT: 409, Code.CSRF: 403, Code.INTERNAL: 500,
49
+ }
50
+
51
+
52
+ class RPCError(Exception):
53
+ """Raised by server functions to return a typed cross-boundary error."""
54
+
55
+ def __init__(self, code: str = Code.INTERNAL, message: str = "",
56
+ *, status: int | None = None, details: dict | None = None):
57
+ super().__init__(message or code)
58
+ self.code = code
59
+ self.status = status if status is not None else _STATUS.get(code, 500)
60
+ self.details = details or {}
61
+
62
+ def to_dict(self):
63
+ return {"code": self.code, "message": str(self),
64
+ "details": self.details}
65
+
66
+
67
+ def status_for(code: str) -> int:
68
+ return _STATUS.get(code, 500)
69
+
70
+
71
+ def new_request_id() -> str:
72
+ return uuid.uuid4().hex[:12]
73
+
74
+
75
+ def parse_traceparent(value: str | None) -> tuple[str, str] | None:
76
+ """Parse W3C ``traceparent`` into (trace_id, parent_span_id)."""
77
+ try:
78
+ parts = (value or "").split("-")
79
+ if len(parts) >= 3 and len(parts[1]) == 32 and len(parts[2]) == 16:
80
+ return parts[1], parts[2]
81
+ except Exception:
82
+ pass
83
+ return None
84
+
85
+
86
+ def make_traceparent(trace_id: str, span_id: str) -> str:
87
+ return f"00-{trace_id}-{span_id}-01"
88
+
89
+
90
+ class RateLimiter:
91
+ """Fixed-window per-key rate limiter (memory; Redis adapter optional)."""
92
+
93
+ def __init__(self, max_calls: int = 60, window: float = 60.0,
94
+ time_fn=None):
95
+ self.max_calls = max_calls
96
+ self.window = window
97
+ self._now = time_fn or time.monotonic
98
+ self._hits: dict[str, list[float]] = {}
99
+ self._lock = threading.Lock()
100
+
101
+ def allow(self, key: str) -> tuple[bool, int]:
102
+ """Return (allowed, retry_after_seconds)."""
103
+ now = self._now()
104
+ with self._lock:
105
+ hits = [t for t in self._hits.get(key, []) if now - t < self.window]
106
+ if len(hits) >= self.max_calls:
107
+ retry = int(self.window - (now - hits[0])) + 1
108
+ self._hits[key] = hits
109
+ return False, max(retry, 1)
110
+ hits.append(now)
111
+ self._hits[key] = hits
112
+ return True, 0
113
+
114
+
115
+ class RetryPolicy:
116
+ """Client retry policy: which statuses are safe to retry + backoff."""
117
+
118
+ RETRYABLE = {502, 503, 504}
119
+
120
+ def __init__(self, attempts: int = 3, base_delay: float = 0.1,
121
+ max_delay: float = 2.0, jitter: float = 0.1):
122
+ self.attempts = max(1, attempts)
123
+ self.base_delay = base_delay
124
+ self.max_delay = max_delay
125
+ self.jitter = jitter
126
+
127
+ def delay(self, attempt: int) -> float:
128
+ d = min(self.base_delay * (2 ** attempt), self.max_delay)
129
+ return d + random.uniform(0, self.jitter)
130
+
131
+ def should_retry(self, attempt: int, status: int | None,
132
+ exc: Exception | None = None) -> bool:
133
+ if attempt + 1 >= self.attempts:
134
+ return False
135
+ if exc is not None:
136
+ return True # network-level failure: safe to retry idempotent reads
137
+ return status in self.RETRYABLE