pywire 0.1.0__py3-none-any.whl → 0.1.1__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 (101) hide show
  1. {pywire-0.1.0.dist-info → pywire-0.1.1.dist-info}/METADATA +23 -1
  2. pywire-0.1.1.dist-info/RECORD +9 -0
  3. pywire/__init__.py +0 -2
  4. pywire/cli/__init__.py +0 -1
  5. pywire/cli/generators.py +0 -48
  6. pywire/cli/main.py +0 -309
  7. pywire/cli/tui.py +0 -563
  8. pywire/cli/validate.py +0 -26
  9. pywire/client/.prettierignore +0 -8
  10. pywire/client/.prettierrc +0 -7
  11. pywire/client/build.mjs +0 -73
  12. pywire/client/eslint.config.js +0 -46
  13. pywire/client/package.json +0 -39
  14. pywire/client/pnpm-lock.yaml +0 -2971
  15. pywire/client/src/core/app.ts +0 -263
  16. pywire/client/src/core/dom-updater.test.ts +0 -78
  17. pywire/client/src/core/dom-updater.ts +0 -321
  18. pywire/client/src/core/index.ts +0 -5
  19. pywire/client/src/core/transport-manager.test.ts +0 -179
  20. pywire/client/src/core/transport-manager.ts +0 -159
  21. pywire/client/src/core/transports/base.ts +0 -122
  22. pywire/client/src/core/transports/http.ts +0 -142
  23. pywire/client/src/core/transports/index.ts +0 -13
  24. pywire/client/src/core/transports/websocket.ts +0 -97
  25. pywire/client/src/core/transports/webtransport.ts +0 -149
  26. pywire/client/src/dev/dev-app.ts +0 -93
  27. pywire/client/src/dev/error-trace.test.ts +0 -97
  28. pywire/client/src/dev/error-trace.ts +0 -76
  29. pywire/client/src/dev/index.ts +0 -4
  30. pywire/client/src/dev/status-overlay.ts +0 -63
  31. pywire/client/src/events/handler.test.ts +0 -318
  32. pywire/client/src/events/handler.ts +0 -454
  33. pywire/client/src/pywire.core.ts +0 -22
  34. pywire/client/src/pywire.dev.ts +0 -27
  35. pywire/client/tsconfig.json +0 -17
  36. pywire/client/vitest.config.ts +0 -15
  37. pywire/compiler/__init__.py +0 -6
  38. pywire/compiler/ast_nodes.py +0 -304
  39. pywire/compiler/attributes/__init__.py +0 -6
  40. pywire/compiler/attributes/base.py +0 -24
  41. pywire/compiler/attributes/conditional.py +0 -37
  42. pywire/compiler/attributes/events.py +0 -55
  43. pywire/compiler/attributes/form.py +0 -37
  44. pywire/compiler/attributes/loop.py +0 -75
  45. pywire/compiler/attributes/reactive.py +0 -34
  46. pywire/compiler/build.py +0 -28
  47. pywire/compiler/build_artifacts.py +0 -342
  48. pywire/compiler/codegen/__init__.py +0 -5
  49. pywire/compiler/codegen/attributes/__init__.py +0 -6
  50. pywire/compiler/codegen/attributes/base.py +0 -19
  51. pywire/compiler/codegen/attributes/events.py +0 -35
  52. pywire/compiler/codegen/directives/__init__.py +0 -6
  53. pywire/compiler/codegen/directives/base.py +0 -16
  54. pywire/compiler/codegen/directives/path.py +0 -53
  55. pywire/compiler/codegen/generator.py +0 -2341
  56. pywire/compiler/codegen/template.py +0 -2178
  57. pywire/compiler/directives/__init__.py +0 -7
  58. pywire/compiler/directives/base.py +0 -20
  59. pywire/compiler/directives/component.py +0 -33
  60. pywire/compiler/directives/context.py +0 -93
  61. pywire/compiler/directives/layout.py +0 -49
  62. pywire/compiler/directives/no_spa.py +0 -24
  63. pywire/compiler/directives/path.py +0 -71
  64. pywire/compiler/directives/props.py +0 -88
  65. pywire/compiler/exceptions.py +0 -19
  66. pywire/compiler/interpolation/__init__.py +0 -6
  67. pywire/compiler/interpolation/base.py +0 -28
  68. pywire/compiler/interpolation/jinja.py +0 -272
  69. pywire/compiler/parser.py +0 -750
  70. pywire/compiler/paths.py +0 -29
  71. pywire/compiler/preprocessor.py +0 -43
  72. pywire/core/wire.py +0 -119
  73. pywire/py.typed +0 -0
  74. pywire/runtime/__init__.py +0 -7
  75. pywire/runtime/aioquic_server.py +0 -194
  76. pywire/runtime/app.py +0 -889
  77. pywire/runtime/compile_error_page.py +0 -195
  78. pywire/runtime/debug.py +0 -203
  79. pywire/runtime/dev_server.py +0 -434
  80. pywire/runtime/dev_server.py.broken +0 -268
  81. pywire/runtime/error_page.py +0 -64
  82. pywire/runtime/error_renderer.py +0 -23
  83. pywire/runtime/escape.py +0 -23
  84. pywire/runtime/files.py +0 -40
  85. pywire/runtime/helpers.py +0 -97
  86. pywire/runtime/http_transport.py +0 -253
  87. pywire/runtime/loader.py +0 -272
  88. pywire/runtime/logging.py +0 -72
  89. pywire/runtime/page.py +0 -384
  90. pywire/runtime/pydantic_integration.py +0 -52
  91. pywire/runtime/router.py +0 -229
  92. pywire/runtime/server.py +0 -25
  93. pywire/runtime/style_collector.py +0 -31
  94. pywire/runtime/upload_manager.py +0 -76
  95. pywire/runtime/validation.py +0 -449
  96. pywire/runtime/websocket.py +0 -665
  97. pywire/runtime/webtransport_handler.py +0 -195
  98. pywire-0.1.0.dist-info/RECORD +0 -104
  99. {pywire-0.1.0.dist-info → pywire-0.1.1.dist-info}/WHEEL +0 -0
  100. {pywire-0.1.0.dist-info → pywire-0.1.1.dist-info}/entry_points.txt +0 -0
  101. {pywire-0.1.0.dist-info → pywire-0.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,31 +0,0 @@
1
- """Style collector for scoped CSS."""
2
-
3
- from typing import Dict
4
-
5
-
6
- class StyleCollector:
7
- """Collects and deduplicates scoped styles per request."""
8
-
9
- def __init__(self) -> None:
10
- self._styles: Dict[str, str] = {} # scope_id -> css
11
-
12
- def add(self, scope_id: str, css: str) -> bool:
13
- """Add CSS for scope_id. Returns True if new (first occurrence)."""
14
- if scope_id in self._styles:
15
- return False
16
- self._styles[scope_id] = css
17
- return True
18
-
19
- def render(self) -> str:
20
- """Render all collected styles as a single <style> block."""
21
- if not self._styles:
22
- return ""
23
-
24
- # Sort by scope_id to ensure deterministic output (important for tests)
25
- # But maybe preserving insertion order is better?
26
- # Actually insertion order implicitly handles dependency order if children render first?
27
- # No, children render inside parent body.
28
- # But we collect during render.
29
- # Let's just join values. Dicts preserve insertion order in modern Python.
30
- combined = "\n".join(self._styles.values())
31
- return f"<style>{combined}</style>"
@@ -1,76 +0,0 @@
1
- import shutil
2
- import tempfile
3
- import uuid
4
- from pathlib import Path
5
- from typing import Optional
6
-
7
- from starlette.datastructures import UploadFile
8
-
9
- from pywire.runtime.files import FileUpload
10
-
11
-
12
- class UploadManager:
13
- """
14
- Manages temporary storage of uploaded files using generated IDs.
15
- Files are stored in a temporary directory and accessed by ID.
16
- Ideally, these should be cleaned up after request processing or via a TTL mechanism.
17
- For this implementation, we rely on OS temp cleaning or process restart for now.
18
- """
19
-
20
- def __init__(self) -> None:
21
- self._temp_dir = Path(tempfile.mkdtemp(prefix="pywire_uploads_"))
22
-
23
- def save(self, file: UploadFile) -> str:
24
- """
25
- Save an uploaded file and return a unique ID.
26
- """
27
- upload_id = str(uuid.uuid4())
28
- file_path = self._temp_dir / upload_id
29
-
30
- # Save content
31
- with open(file_path, "wb") as f:
32
- shutil.copyfileobj(file.file, f)
33
-
34
- # Store metadata in a separate file or just keep it simple?
35
- # We need filename, content_type, size.
36
- # Let's write a metadata file next to it.
37
- # Format: filename\ncontent_type
38
- # Size comes from file size.
39
-
40
- meta_path = file_path.with_suffix(".meta")
41
- with open(meta_path, "w") as f:
42
- f.write(f"{file.filename}\n{file.content_type}")
43
-
44
- return upload_id
45
-
46
- def get(self, upload_id: str) -> Optional[FileUpload]:
47
- """
48
- Retrieve a file by ID.
49
- """
50
- file_path = self._temp_dir / upload_id
51
- meta_path = file_path.with_suffix(".meta")
52
-
53
- if not file_path.exists() or not meta_path.exists():
54
- return None
55
-
56
- try:
57
- with open(meta_path, "r") as f:
58
- lines = f.read().splitlines()
59
- filename = lines[0]
60
- content_type = (
61
- lines[1] if len(lines) > 1 else "application/octet-stream"
62
- )
63
-
64
- return FileUpload(
65
- filename=filename,
66
- content_type=content_type,
67
- size=file_path.stat().st_size,
68
- content=file_path.read_bytes(),
69
- )
70
- except Exception as e:
71
- print(f"Error retrieving upload {upload_id}: {e}")
72
- return None
73
-
74
-
75
- # Global instance
76
- upload_manager = UploadManager()
@@ -1,449 +0,0 @@
1
- """Server-side form validation matching HTML5 constraints."""
2
-
3
- import re
4
- from dataclasses import dataclass
5
- from datetime import date
6
- from decimal import Decimal, InvalidOperation
7
- from enum import Enum
8
- from typing import Any, Callable, Dict, List, Optional, Tuple, Type
9
-
10
- from pywire.runtime.files import FileUpload
11
- from pywire.runtime.upload_manager import upload_manager
12
-
13
-
14
- @dataclass
15
- class FieldRules:
16
- """Runtime validation rules for a single field."""
17
-
18
- required: bool = False
19
- required_expr: Optional[str] = None # Python expression for conditional required
20
- pattern: Optional[str] = None
21
- minlength: Optional[int] = None
22
- maxlength: Optional[int] = None
23
- min_value: Optional[str] = None
24
- min_expr: Optional[str] = None # Python expression for dynamic min
25
- max_value: Optional[str] = None
26
- max_expr: Optional[str] = None # Python expression for dynamic max
27
- step: Optional[str] = None
28
- input_type: str = "text"
29
- title: Optional[str] = None # Custom error message
30
- max_size: Optional[int] = None # Max file size in bytes
31
- allowed_types: Optional[List[str]] = None # Allowed MIME types or extensions
32
-
33
-
34
- @dataclass
35
- class FormValidationSchema:
36
- """Runtime schema containing all validation rules for a form."""
37
-
38
- fields: Dict[str, FieldRules]
39
- model_name: Optional[str] = None
40
-
41
-
42
- class FormValidator:
43
- """Server-side form validation matching HTML5 constraints."""
44
-
45
- # Email regex (simplified but sufficient for most cases)
46
- EMAIL_PATTERN = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
47
-
48
- # URL regex (simplified)
49
- URL_PATTERN = re.compile(r"^https?://[^\s/$.?#].[^\s]*$", re.IGNORECASE)
50
-
51
- def validate_field(
52
- self,
53
- name: str,
54
- value: Any,
55
- rules: FieldRules,
56
- state_getter: Optional[Callable[[str], Any]] = None,
57
- ) -> Optional[str]:
58
- """
59
- Validate a single field against rules.
60
-
61
- Args:
62
- name: Field name
63
- value: Field value (string from form)
64
- rules: Validation rules
65
- state_getter: Optional function to evaluate expressions against page state
66
-
67
- Returns:
68
- Error message string, or None if valid.
69
- """
70
- # Handle conditional required
71
- is_required = rules.required
72
- if rules.required_expr and state_getter:
73
- try:
74
- is_required = bool(state_getter(rules.required_expr))
75
- except Exception:
76
- is_required = rules.required
77
-
78
- # Check required
79
- if is_required:
80
- if value is None or (isinstance(value, str) and value.strip() == ""):
81
- return rules.title or "This field is required"
82
-
83
- # If empty and not required, skip other validations
84
- if value is None or (isinstance(value, str) and value.strip() == ""):
85
- return None
86
-
87
- str_value = str(value).strip()
88
-
89
- # Pattern validation
90
- if rules.pattern:
91
- try:
92
- if not re.fullmatch(rules.pattern, str_value):
93
- return rules.title or "Value does not match the required pattern"
94
- except re.error:
95
- pass # Invalid regex, skip
96
-
97
- # Length validations
98
- if rules.minlength is not None:
99
- if len(str_value) < rules.minlength:
100
- return rules.title or f"Must be at least {rules.minlength} characters"
101
-
102
- if rules.maxlength is not None:
103
- if len(str_value) > rules.maxlength:
104
- return rules.title or f"Must be at most {rules.maxlength} characters"
105
-
106
- # Type-based validation
107
- if rules.input_type == "email":
108
- if not self.EMAIL_PATTERN.match(str_value):
109
- return rules.title or "Please enter a valid email address"
110
-
111
- elif rules.input_type == "url":
112
- if not self.URL_PATTERN.match(str_value):
113
- return rules.title or "Please enter a valid URL"
114
-
115
- elif rules.input_type == "number":
116
- return self._validate_number(str_value, rules, state_getter)
117
-
118
- elif rules.input_type == "date":
119
- return self._validate_date(str_value, rules, state_getter)
120
-
121
- elif rules.input_type == "file":
122
- # File validation
123
- if isinstance(value, FileUpload):
124
- # Check size
125
- if rules.max_size is not None and value.size > rules.max_size:
126
- size_mb = rules.max_size / (1024 * 1024)
127
- return rules.title or f"File is too large (max {size_mb:.1f}MB)"
128
-
129
- # Check type
130
- if rules.allowed_types:
131
- # Simple MIME type check
132
- # rules.allowed_types is list like ['image/*', 'application/pdf', '.jpg']
133
- allowed = False
134
- for pattern in rules.allowed_types:
135
- pattern = pattern.strip()
136
- if pattern.startswith("."):
137
- # Extension check
138
- if value.filename.lower().endswith(pattern.lower()):
139
- allowed = True
140
- break
141
- elif pattern.endswith("/*"):
142
- # Wildcard MIME check (e.g. image/*)
143
- base_type = pattern[:-2] # remove /*
144
- if value.content_type.startswith(base_type):
145
- allowed = True
146
- break
147
- else:
148
- # Exact MIME check
149
- if value.content_type == pattern:
150
- allowed = True
151
- break
152
-
153
- if not allowed:
154
- return (
155
- rules.title
156
- or f"File type not allowed. Accepted: {', '.join(rules.allowed_types)}"
157
- )
158
-
159
- # Range validation for non-typed fields
160
- if rules.input_type == "text":
161
- # Only apply if min/max look numeric
162
- if rules.min_value or rules.max_value or rules.min_expr or rules.max_expr:
163
- try:
164
- num_value = Decimal(str_value)
165
- return self._validate_numeric_range(num_value, rules, state_getter)
166
- except InvalidOperation:
167
- pass # Not a number, skip range validation
168
-
169
- return None
170
-
171
- def _validate_number(
172
- self,
173
- str_value: str,
174
- rules: FieldRules,
175
- state_getter: Optional[Callable[[str], Any]] = None,
176
- ) -> Optional[str]:
177
- """Validate a number input."""
178
- try:
179
- num_value = Decimal(str_value)
180
- except InvalidOperation:
181
- return rules.title or "Please enter a valid number"
182
-
183
- return self._validate_numeric_range(num_value, rules, state_getter)
184
-
185
- def _validate_numeric_range(
186
- self,
187
- num_value: Decimal,
188
- rules: FieldRules,
189
- state_getter: Optional[Callable[[str], Any]] = None,
190
- ) -> Optional[str]:
191
- """Validate numeric range constraints."""
192
- # Get min value (static or dynamic)
193
- min_val = None
194
- if rules.min_expr and state_getter:
195
- try:
196
- min_val = Decimal(str(state_getter(rules.min_expr)))
197
- except (InvalidOperation, Exception):
198
- pass
199
- elif rules.min_value:
200
- try:
201
- min_val = Decimal(rules.min_value)
202
- except InvalidOperation:
203
- pass
204
-
205
- if min_val is not None and num_value < min_val:
206
- return rules.title or f"Value must be at least {min_val}"
207
-
208
- # Get max value (static or dynamic)
209
- max_val = None
210
- if rules.max_expr and state_getter:
211
- try:
212
- max_val = Decimal(str(state_getter(rules.max_expr)))
213
- except (InvalidOperation, Exception):
214
- pass
215
- elif rules.max_value:
216
- try:
217
- max_val = Decimal(rules.max_value)
218
- except InvalidOperation:
219
- pass
220
-
221
- if max_val is not None and num_value > max_val:
222
- return rules.title or f"Value must be at most {max_val}"
223
-
224
- # Step validation
225
- if rules.step:
226
- try:
227
- step = Decimal(rules.step)
228
- if step > 0:
229
- base = min_val if min_val is not None else Decimal("0")
230
- diff = num_value - base
231
- if diff % step != 0:
232
- return rules.title or f"Value must be a multiple of {step}"
233
- except InvalidOperation:
234
- pass
235
-
236
- return None
237
-
238
- def _validate_date(
239
- self,
240
- str_value: str,
241
- rules: FieldRules,
242
- state_getter: Optional[Callable[[str], Any]] = None,
243
- ) -> Optional[str]:
244
- """Validate a date input."""
245
- try:
246
- date_value = date.fromisoformat(str_value)
247
- except ValueError:
248
- return rules.title or "Please enter a valid date (YYYY-MM-DD)"
249
-
250
- # Get min date (static or dynamic)
251
- min_date = None
252
- if rules.min_expr and state_getter:
253
- try:
254
- min_str = str(state_getter(rules.min_expr))
255
- min_date = date.fromisoformat(min_str)
256
- except (ValueError, Exception):
257
- pass
258
- elif rules.min_value:
259
- try:
260
- min_date = date.fromisoformat(rules.min_value)
261
- except ValueError:
262
- pass
263
-
264
- if min_date is not None and date_value < min_date:
265
- return rules.title or f"Date must be on or after {min_date.isoformat()}"
266
-
267
- # Get max date (static or dynamic)
268
- max_date = None
269
- if rules.max_expr and state_getter:
270
- try:
271
- max_str = str(state_getter(rules.max_expr))
272
- max_date = date.fromisoformat(max_str)
273
- except (ValueError, Exception):
274
- pass
275
- elif rules.max_value:
276
- try:
277
- max_date = date.fromisoformat(rules.max_value)
278
- except ValueError:
279
- pass
280
-
281
- if max_date is not None and date_value > max_date:
282
- return rules.title or f"Date must be on or before {max_date.isoformat()}"
283
-
284
- return None
285
-
286
- def validate_form(
287
- self,
288
- data: Dict[str, Any],
289
- schema: Dict[str, FieldRules],
290
- state_getter: Callable[[str], Any],
291
- ) -> Tuple[Dict[str, Any], Dict[str, str]]:
292
- """
293
- Validate data against schema.
294
- Returns: (cleaned_data, errors)
295
- """
296
- errors: Dict[str, str] = {}
297
- cleaned_data: Dict[str, Any] = {}
298
-
299
- # 1. Validate fields present in schema
300
- for field_name, rules in schema.items():
301
- value = data.get(field_name)
302
-
303
- # Helper to evaluate rules against state
304
- def eval_rule(attr_name: str) -> Any:
305
- expr = getattr(rules, f"{attr_name}_expr")
306
- if expr:
307
- return state_getter(expr)
308
- return getattr(rules, attr_name)
309
-
310
- # Check required
311
- is_required = eval_rule("required")
312
- if is_required and (value is None or value == ""):
313
- errors[field_name] = f"{rules.title or field_name} is required."
314
- continue
315
-
316
- # If empty and not required, skip other validations
317
- if value is None or value == "":
318
- if rules.input_type == "checkbox":
319
- cleaned_data[field_name] = False
320
- else:
321
- cleaned_data[field_name] = None
322
- continue
323
-
324
- # Type conversion (strings to int/float/bool)
325
- try:
326
- converted_value = self._convert_value(value, rules.input_type)
327
- cleaned_data[field_name] = converted_value
328
- except ValueError:
329
- errors[field_name] = (
330
- f"{rules.title or field_name} must be a valid {rules.input_type}."
331
- )
332
- continue
333
-
334
- # Validate rules against converted value
335
- error = self.validate_field(
336
- field_name, converted_value, rules, state_getter
337
- ) # Use original state_getter for validate_field
338
- if error:
339
- errors[field_name] = error
340
-
341
- # 2. Pass through data not in schema?
342
- # For strict typing, maybe we only want schema fields?
343
- # But for flexibility, let's merge original data for non-schema fields.
344
- final_data = data.copy()
345
- final_data.update(cleaned_data)
346
-
347
- return final_data, errors
348
-
349
- def _convert_value(self, value: Any, input_type: str) -> Any:
350
- """Convert string value to appropriate type."""
351
- if value is None or value == "":
352
- return None
353
-
354
- if input_type == "number":
355
- # Try int first, then float? Or just float?
356
- # HTML input type="number" can be either.
357
- try:
358
- if isinstance(value, str) and "." in value:
359
- return float(value)
360
- return int(value)
361
- except ValueError:
362
- # If int conversion fails, try float as a last resort
363
- return float(value)
364
-
365
- elif input_type == "checkbox":
366
- # Checkbox value usually "on" or "true" string, but handled by client framework?
367
- # If it comes from FormData, unchecked might be missing (handled in
368
- # validate_form required check).
369
- # Checked might be "on".
370
- # Convert to boolean. Common values for "true" are "on", "true", 1.
371
- if isinstance(value, str):
372
- return value.lower() in ("on", "true", "1")
373
- return bool(value)
374
-
375
- elif input_type == "file":
376
- # File uploads come as dicts from client.
377
- # If it has _upload_id, resolve it via UploadManager.
378
- # If it has content (old way), use from_dict.
379
- if isinstance(value, dict):
380
- if "_upload_id" in value:
381
- file = upload_manager.get(value["_upload_id"])
382
- if file:
383
- return file
384
- return None # Pending or expired?
385
- elif "content" in value:
386
- return FileUpload.from_dict(value)
387
- return value
388
-
389
- return value
390
-
391
- def convert_to_type(self, value: Any, target_type: Type) -> Any:
392
- """Convert value to specific Python type hints (e.g. Enums)."""
393
- if value is None:
394
- return None
395
-
396
- # Enum conversion
397
- if isinstance(target_type, type) and issubclass(target_type, Enum):
398
- # Try matching by value first
399
- try:
400
- return target_type(value)
401
- except ValueError:
402
- pass
403
-
404
- # Try matching by name
405
- if isinstance(value, str):
406
- try:
407
- return target_type[value]
408
- except KeyError:
409
- pass
410
- try:
411
- return target_type[value.upper()]
412
- except KeyError:
413
- pass
414
-
415
- # Return original if generic match fails, likely invalid
416
- return value
417
-
418
- return value
419
-
420
- return value
421
-
422
- @staticmethod
423
- def parse_nested_data(flat_data: Dict[str, Any]) -> Dict[str, Any]:
424
- """
425
- Parse flat form data with dot notation into nested dicts.
426
-
427
- Example:
428
- {'customer.name': 'John', 'customer.email': 'john@example.com'}
429
- ->
430
- {'customer': {'name': 'John', 'email': 'john@example.com'}}
431
- """
432
- result: Dict[str, Any] = {}
433
-
434
- for key, value in flat_data.items():
435
- parts = key.split(".")
436
- current = result
437
-
438
- for i, part in enumerate(parts[:-1]):
439
- if part not in current:
440
- current[part] = {}
441
- current = current[part]
442
-
443
- current[parts[-1]] = value
444
-
445
- return result
446
-
447
-
448
- # Global validator instance
449
- form_validator = FormValidator()