pieui 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 (77) hide show
  1. pie/__init__.py +27 -0
  2. pie/__main__.py +99 -0
  3. pie/async_page.py +381 -0
  4. pie/card.py +218 -0
  5. pie/cli.py +244 -0
  6. pie/code/__init__.py +8 -0
  7. pie/code/build.py +26 -0
  8. pie/code/card_add.py +87 -0
  9. pie/code/create.py +64 -0
  10. pie/code/services/__init__.py +3 -0
  11. pie/code/services/models.py +13 -0
  12. pie/code/services/storage.py +379 -0
  13. pie/code/templates/pages/components/ajax_complex_component.py.j2 +14 -0
  14. pie/code/templates/pages/components/ajax_complex_container_component.py.j2 +14 -0
  15. pie/code/templates/pages/components/ajax_component.py.j2 +13 -0
  16. pie/code/templates/pages/components/ajax_container_component.py.j2 +14 -0
  17. pie/code/templates/pages/components/ajax_io_complex_component.py.j2 +19 -0
  18. pie/code/templates/pages/components/ajax_io_complex_container_component.py.j2 +19 -0
  19. pie/code/templates/pages/components/ajax_io_component.py.j2 +18 -0
  20. pie/code/templates/pages/components/ajax_io_container_component.py.j2 +18 -0
  21. pie/code/templates/pages/components/complex_component.py.j2 +9 -0
  22. pie/code/templates/pages/components/complex_container_component.py.j2 +10 -0
  23. pie/code/templates/pages/components/component.py.j2 +8 -0
  24. pie/code/templates/pages/components/container_component.py.j2 +10 -0
  25. pie/code/templates/pages/components/input_ajax_complex_component.py.j2 +17 -0
  26. pie/code/templates/pages/components/input_ajax_complex_container_component.py.j2 +17 -0
  27. pie/code/templates/pages/components/input_ajax_component.py.j2 +16 -0
  28. pie/code/templates/pages/components/input_ajax_io_complex_component.py.j2 +22 -0
  29. pie/code/templates/pages/components/input_ajax_io_complex_container_component.py.j2 +22 -0
  30. pie/code/templates/pages/components/input_ajax_io_component.py.j2 +21 -0
  31. pie/code/templates/pages/components/input_complex_component.py.j2 +9 -0
  32. pie/code/templates/pages/components/input_complex_container_component.py.j2 +10 -0
  33. pie/code/templates/pages/components/input_component.py.j2 +11 -0
  34. pie/code/templates/pages/components/input_io_complex_component.py.j2 +18 -0
  35. pie/code/templates/pages/components/input_io_complex_container_component.py.j2 +18 -0
  36. pie/code/templates/pages/components/input_io_component.py.j2 +17 -0
  37. pie/code/templates/pages/components/io_complex_component.py.j2 +15 -0
  38. pie/code/templates/pages/components/io_complex_container_component.py.j2 +15 -0
  39. pie/code/templates/pages/components/io_component.py.j2 +14 -0
  40. pie/code/templates/pages/components/io_container_component.py.j2 +15 -0
  41. pie/code/templates/pages/main.py.j2 +9 -0
  42. pie/code/templates/pages/page.py.j2 +0 -0
  43. pie/code/templates/web.py.j2 +26 -0
  44. pie/code/web.py +28 -0
  45. pie/components/__init__.py +0 -0
  46. pie/components/ajax_group.py +29 -0
  47. pie/components/hidden.py +40 -0
  48. pie/components/html_embed.py +70 -0
  49. pie/components/io_log.py +30 -0
  50. pie/components/one_of.py +28 -0
  51. pie/components/union.py +87 -0
  52. pie/config.py +24 -0
  53. pie/fastweb.py +1334 -0
  54. pie/hub/__init__.py +2 -0
  55. pie/hub/centrifuge_helpers/__init__.py +0 -0
  56. pie/hub/centrifuge_helpers/cent_client_wrapper.py +13 -0
  57. pie/hub/centrifuge_helpers/session_wrapper.py +69 -0
  58. pie/hub/checkpoint/__init__.py +12 -0
  59. pie/hub/checkpoint/archive.py +125 -0
  60. pie/hub/checkpoint/gdrive.py +42 -0
  61. pie/hub/checkpoint/mega_path.py +76 -0
  62. pie/hub/checkpoint/mega_url.py +16 -0
  63. pie/hub/checkpoint/path.py +16 -0
  64. pie/hub/checkpoint/pretrained_checkpoint.py +184 -0
  65. pie/hub/checkpoint/pretrained_jit_model.py +28 -0
  66. pie/hub/checkpoint/rclone.py +47 -0
  67. pie/hub/checkpoint/url.py +73 -0
  68. pie/hub/checkpoint/yadisk.py +12 -0
  69. pie/hub/svgpil/SVGImage.py +572 -0
  70. pie/hub/svgpil/__init__.py +0 -0
  71. pie/hub/svgpil/svg_stream.py +0 -0
  72. pie/hub/svgpil/test_draw.py +7 -0
  73. pie/types.py +83 -0
  74. pieui-0.1.0.dist-info/METADATA +230 -0
  75. pieui-0.1.0.dist-info/RECORD +77 -0
  76. pieui-0.1.0.dist-info/WHEEL +4 -0
  77. pieui-0.1.0.dist-info/entry_points.txt +3 -0
pie/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ __version__ = "0.1.0"
2
+
3
+ __author__ = "George K."
4
+
5
+
6
+ from .fastweb import Web
7
+ from .async_page import AsyncPage
8
+ from .card import Card, InputCard
9
+ from .components.hidden import HiddenCard
10
+ from .components.union import UnionCard
11
+ from .components.ajax_group import AjaxGroupCard
12
+ from .components.html_embed import HTMLEmbedCard
13
+ from .components.one_of import OneOfCard
14
+ from .components.io_log import IOLogCard
15
+
16
+
17
+ __all__ = ["__version__",
18
+ "Web",
19
+ "AsyncPage",
20
+ "InputCard",
21
+ "Card",
22
+ "HiddenCard",
23
+ "UnionCard",
24
+ "AjaxGroupCard",
25
+ "HTMLEmbedCard",
26
+ "OneOfCard",
27
+ "IOLogCard"]
pie/__main__.py ADDED
@@ -0,0 +1,99 @@
1
+ """PieDemo CLI
2
+
3
+ Command-line interface for the PieDemo framework.
4
+ """
5
+
6
+ import argparse
7
+ import sys
8
+ from typing import Optional
9
+
10
+ from .code import handle_card_add, handle_build, handle_create, handle_web, handle_web_verify
11
+
12
+
13
+ CARD_TYPES = {"simple", "complex", "container", "complex-container"}
14
+
15
+
16
+ def build_parser() -> argparse.ArgumentParser:
17
+ parser = argparse.ArgumentParser(
18
+ prog="pie",
19
+ description="Pie - Framework for building interactive web applications",
20
+ )
21
+
22
+ subparsers = parser.add_subparsers(dest="command", help="Available commands", required=True)
23
+
24
+ # piedemo web module:app [verify|build]
25
+ web_parser = subparsers.add_parser("web", help="Run or lint a web application")
26
+ web_parser.add_argument("web", help="Web in format module:attribute, e.g. some:app")
27
+ web_subparsers = web_parser.add_subparsers(dest="web_command")
28
+ web_subparsers.add_parser("verify", help="Verify the web application")
29
+ web_subparsers.add_parser("build", help="Build static JSON from a Web application")
30
+
31
+ card_parser = subparsers.add_parser("card", help="Manage card templates")
32
+ card_subparsers = card_parser.add_subparsers(dest="card_command", required=True)
33
+ card_add_parser = card_subparsers.add_parser("add", help="Create a card file from a template")
34
+ card_add_parser.add_argument("card_type", choices=sorted(CARD_TYPES))
35
+ card_add_parser.add_argument("card_name", metavar="NAME")
36
+ card_add_parser.add_argument("--io", action="store_true", dest="use_io")
37
+ card_add_parser.add_argument("--ajax", action="store_true", dest="use_ajax")
38
+
39
+ create_parser = subparsers.add_parser("create", help="Create a new Pie project")
40
+ create_parser.add_argument("project_name", metavar="NAME")
41
+
42
+ return parser
43
+
44
+
45
+ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
46
+ return build_parser().parse_args(argv)
47
+
48
+
49
+ def main(
50
+ command: str,
51
+ web: Optional[str] = None,
52
+ web_command: Optional[str] = None,
53
+ card_command: Optional[str] = None,
54
+ card_type: Optional[str] = None,
55
+ card_name: Optional[str] = None,
56
+ project_name: Optional[str] = None,
57
+ use_io: bool = False,
58
+ use_ajax: bool = False,
59
+ ) -> int:
60
+ try:
61
+ if command == "web":
62
+ if web is None:
63
+ raise ValueError("Web path is required")
64
+ if web_command == "verify":
65
+ handle_web_verify(web)
66
+ elif web_command == "build":
67
+ handle_build(web)
68
+ else:
69
+ handle_web(web)
70
+ return 0
71
+
72
+ if command == "page":
73
+ return 0
74
+ if command == "card":
75
+ if card_command == "add":
76
+ handle_card_add(
77
+ card_name=card_name,
78
+ card_type=card_type,
79
+ use_io=use_io,
80
+ use_ajax=use_ajax,
81
+ )
82
+ return 0
83
+ if command == "create":
84
+ if project_name is None:
85
+ raise ValueError("Project name is required")
86
+ handle_create(project_name)
87
+ return 0
88
+ if command == "init":
89
+ return 0
90
+ if command == "login":
91
+ return 0
92
+ return 0
93
+ except ValueError as error:
94
+ print(error, file=sys.stderr)
95
+ return 1
96
+
97
+
98
+ if __name__ == "__main__":
99
+ raise SystemExit(main(**vars(parse_args())))
pie/async_page.py ADDED
@@ -0,0 +1,381 @@
1
+ import asyncio
2
+ import inspect
3
+ import json
4
+ import sys
5
+ import traceback
6
+ from typing import Any, AsyncGenerator, Callable, Dict, List, Literal, Optional, Union, TypeAlias
7
+ from fastapi.responses import StreamingResponse
8
+ from pie.card import Card
9
+ from pie.types import SocketIOEvent
10
+
11
+ try:
12
+ from fastapi_socketio import SocketManager
13
+ SocketManagerType: TypeAlias = SocketManager
14
+ except ImportError:
15
+ print(
16
+ "FastAPI socketio not installed. Emitting events is not available",
17
+ file=sys.stderr,
18
+ )
19
+ SocketManagerType: TypeAlias = Any
20
+
21
+ try:
22
+ from faststream.redis.fastapi import RedisBroker
23
+ from redis.asyncio import StrictRedis
24
+
25
+ RedisBrokerType: TypeAlias = RedisBroker
26
+ StrictRedisType: TypeAlias = StrictRedis
27
+ except ImportError:
28
+ print(
29
+ "FastStream & redis not installed. Cache and Background processing are not available",
30
+ file=sys.stderr,
31
+ )
32
+ RedisBrokerType: TypeAlias = Any
33
+ StrictRedisType: TypeAlias = Any
34
+
35
+
36
+ class AsyncPage(object):
37
+ """
38
+ Base class for asynchronous pages.
39
+
40
+ Provides methods for:
41
+ - filling and parsing data
42
+ - generating events
43
+ - SocketIO or Centrifuge integration
44
+ - caching via Redis
45
+ - AJAX registration
46
+ """
47
+
48
+ def __init__(self, is_typed: bool = False) -> None:
49
+ super(AsyncPage, self).__init__()
50
+ """
51
+ Initialize AsyncPage with default attributes.
52
+ """
53
+ self.is_typed: bool = is_typed
54
+ self.global_data: Dict[str, Any] = {}
55
+ self.ajax_post: Dict[str, Callable[..., Any]] = {}
56
+ self.ajax_get: Dict[str, Callable[..., Any]] = {}
57
+ self.io: Optional[SocketManagerType] = None
58
+ self.broker: Optional[RedisBrokerType] = None
59
+ self.cache: Optional[StrictRedisType] = None
60
+
61
+ def get_possible_fields(self) -> Dict[str, Card]:
62
+ return {
63
+ name: value
64
+ for name, value in inspect.getmembers(self)
65
+ if not name.startswith("__") and isinstance(value, Card)
66
+ }
67
+
68
+ def get_io_cards(self) -> List[Card]:
69
+ """
70
+ Return all cards that support IO events (SocketIO or Centrifuge).
71
+
72
+ Raises:
73
+ NotImplementedError: If the page has no fields.
74
+
75
+ Returns:
76
+ List: IO-enabled cards.
77
+ """
78
+ if hasattr(self, "fields"):
79
+ return self._get_io_cards(self.fields)
80
+ raise NotImplementedError("In " + self.__class__.__name__)
81
+
82
+ @staticmethod
83
+ def _get_io_cards(fields: Card) -> List[Card]:
84
+ """
85
+ Helper to extract IO-enabled cards from a Card object.
86
+
87
+ Args:
88
+ fields: Card object.
89
+
90
+ Returns:
91
+ List: Cards supporting IO events.
92
+ """
93
+ return [
94
+ card
95
+ for card in fields.children()
96
+ if hasattr(card, "name")
97
+ and (
98
+ (hasattr(card, "use_socketio_support") and card.use_socketio_support)
99
+ or (
100
+ hasattr(card, "use_centrifuge_support")
101
+ and card.use_centrifuge_support
102
+ )
103
+ )
104
+ ]
105
+
106
+ async def get_content(self, **kwargs: Any):
107
+ """
108
+ Return filled content of the page.
109
+
110
+ Raises:
111
+ NotImplementedError: If the page has no fields.
112
+
113
+ Returns:
114
+ dict: Page content.
115
+ """
116
+ if hasattr(self, "fields"):
117
+ return self.fill(self.fields, {})
118
+ raise NotImplementedError()
119
+
120
+ async def process(self, **data: Any):
121
+ """
122
+ Process form submission.
123
+
124
+ Raises:
125
+ NotImplementedError
126
+ """
127
+ raise NotImplementedError()
128
+
129
+ @staticmethod
130
+ def fill(
131
+ fields: Card,
132
+ data: Dict[str, Any],
133
+ inplace: bool = False,
134
+ generate: bool = True,
135
+ ):
136
+ """
137
+ Fill a card or V1 fields with data.
138
+
139
+ Args:
140
+ fields: Card or legacy fields.
141
+ data: Dictionary with input values.
142
+ inplace: Modify fields in place.
143
+ generate: Generate content after filling.
144
+ hook: Optional function to run after filling fields.
145
+
146
+ Returns:
147
+ Generated content or modified fields.
148
+ """
149
+ return fields.fill(data, inplace=inplace, generate=generate)
150
+
151
+ @staticmethod
152
+ def parse(fields: Card,
153
+ data: Dict[str, Any],
154
+ strict: bool = True) -> Dict[str, Any]:
155
+ """
156
+ Parse input data into Card or legacy field structure.
157
+
158
+ Args:
159
+ fields: Card or legacy fields.
160
+ data: Input dictionary.
161
+ strict: Raise exceptions on invalid values.
162
+
163
+ Returns:
164
+ Parsed data dictionary.
165
+ """
166
+ return fields.parse_all(data)
167
+
168
+ @staticmethod
169
+ def redirect_url(to: Optional[str], **kwargs: Any) -> Optional[str]:
170
+ """
171
+ Build a redirect URL with optional query parameters.
172
+
173
+ Args:
174
+ to: Base URL.
175
+ kwargs: Query parameters.
176
+
177
+ Returns:
178
+ str: Full redirect URL.
179
+ """
180
+ if to is None:
181
+ return None
182
+ if len(kwargs) == 0 or all([v is None for v in kwargs.values()]):
183
+ return to
184
+ params = "&".join([f"{k}={v}" for k, v in kwargs.items() if v is not None])
185
+
186
+ if "?" in to:
187
+ if to.endswith("&"):
188
+ return f"{to}{params}"
189
+ else:
190
+ return f"{to}&{params}"
191
+ else:
192
+ return f"{to}?{params}"
193
+
194
+ async def emit(
195
+ self,
196
+ event: Union[List[SocketIOEvent], SocketIOEvent],
197
+ to: Optional[str] = None,
198
+ ) -> None:
199
+ """
200
+ Emit SocketIO event(s) to connected clients.
201
+
202
+ Args:
203
+ event: Single or list of events.
204
+ to: Target recipient or broadcast.
205
+ """
206
+ if self.io is None:
207
+ raise RuntimeError("SocketIO and Centrifuge not supported!")
208
+
209
+ if not isinstance(event, list):
210
+ events = [event]
211
+ else:
212
+ events = event
213
+
214
+ await asyncio.gather(*[self.io.emit(ev.name, ev.data, to=to)
215
+ for ev in events])
216
+
217
+ async def reload(self, to: Optional[str] = None) -> None:
218
+ """
219
+ Trigger a page reload via SocketIO.
220
+
221
+ Args:
222
+ to: Target recipient or broadcast.
223
+ """
224
+ if self.io is None:
225
+ raise RuntimeError("SocketIO not supported!")
226
+ print("Reload: ")
227
+ await self.io.emit("piereload", {}, to=to)
228
+
229
+ def register_ajax(
230
+ self,
231
+ pathname: str,
232
+ ajax_fn: Callable[..., Any],
233
+ method: Literal["GET", "POST"] = "POST",
234
+ ) -> str:
235
+ """
236
+ Register an AJAX endpoint.
237
+
238
+ Args:
239
+ pathname: URL path.
240
+ ajax_fn: Handler function.
241
+ method: HTTP method ("POST" or "GET").
242
+
243
+ Returns:
244
+ str: Pathname.
245
+ """
246
+ if method == "POST":
247
+ self.ajax_post[pathname[1:]] = ajax_fn
248
+ elif method == "GET":
249
+ self.ajax_get[pathname[1:]] = ajax_fn
250
+ return pathname
251
+
252
+ @staticmethod
253
+ def create_ajax_event_streaming(
254
+ event_generator: AsyncGenerator[SocketIOEvent, None],
255
+ ) -> StreamingResponse:
256
+ """
257
+ Wrap async generator as a streaming response.
258
+
259
+ Args:
260
+ event_generator: Async generator of SocketIO events.
261
+
262
+ Returns:
263
+ StreamingResponse: Streaming events to the client.
264
+ """
265
+
266
+ async def wrapped_generator() -> AsyncGenerator[str, None]:
267
+ try:
268
+ async for event in event_generator:
269
+ yield event.to_json() + "\n"
270
+ except asyncio.CancelledError:
271
+ print("Streaming cancelled by client", file=sys.stderr)
272
+ raise
273
+ except Exception:
274
+ traceback.print_exc(file=sys.stderr)
275
+
276
+ return StreamingResponse(wrapped_generator(), media_type="text/plain",
277
+ headers={
278
+ "Cache-Control": "no-cache",
279
+ "X-Accel-Buffering": "no",
280
+ })
281
+
282
+ def lock(self, name: str, timeout: int = 120) -> Any:
283
+ """
284
+ Create a Redis lock.
285
+
286
+ Args:
287
+ name: Lock name.
288
+ timeout: Lock timeout in seconds.
289
+
290
+ Returns:
291
+ Redis lock object.
292
+ """
293
+ if self.cache is None:
294
+ raise RuntimeError("Lock not supported!")
295
+ return self.cache.lock(f"lock:{name}", timeout=timeout)
296
+
297
+ async def page_lock(self, name: str, timeout: int = 120) -> Any:
298
+ """
299
+ Create a page-scoped Redis lock.
300
+
301
+ Args:
302
+ name: Lock name.
303
+ timeout: Lock timeout.
304
+
305
+ Returns:
306
+ Redis lock object.
307
+ """
308
+ return self.lock(f"{self.__class__.__name__}:{name}", timeout=timeout)
309
+
310
+ async def save_value(
311
+ self,
312
+ scope: str,
313
+ key: str,
314
+ value: Any,
315
+ encode: bool = False,
316
+ ) -> Any:
317
+ """
318
+ Save a string value to Redis.
319
+
320
+ Args:
321
+ scope: Key prefix.
322
+ key: Key name.
323
+ value: Value to save (must be str).
324
+ encode: JSON encode value.
325
+
326
+ Returns:
327
+ bool: Result of Redis set.
328
+ """
329
+ if not isinstance(value, str):
330
+ raise TypeError("Value must be a string")
331
+ if scope in ["lock"]:
332
+ raise RuntimeError("scope must not be `lock`")
333
+ if ":" in scope:
334
+ raise RuntimeError("scope must not contain ':')")
335
+ if encode:
336
+ value = json.dumps(value)
337
+ return await self.cache.set(f"{scope}:{key}", value)
338
+
339
+ async def load_value(
340
+ self,
341
+ scope: str,
342
+ key: str,
343
+ decode: bool = False,
344
+ ) -> Any:
345
+ """
346
+ Load a value from Redis.
347
+
348
+ Args:
349
+ scope: Key prefix.
350
+ key: Key name.
351
+ decode: JSON decode value.
352
+
353
+ Returns:
354
+ str or decoded object.
355
+ """
356
+ if scope in ["lock"]:
357
+ raise RuntimeError("scope must not be `lock`")
358
+ if ":" in scope:
359
+ raise RuntimeError("scope must not contain ':')")
360
+ value = await self.cache.get(f"{scope}:{key}")
361
+ if decode:
362
+ value = json.loads(value)
363
+ return value
364
+
365
+ def debug(self) -> None:
366
+ """
367
+ Run a development server for debugging the page.
368
+ """
369
+ from pie.fastweb import Web
370
+
371
+ web = Web(
372
+ {
373
+ "": self,
374
+ },
375
+ use_socketio_support=True,
376
+ enable_cors=True,
377
+ disable_serving=True,
378
+ serving_url="http://localhost:3000",
379
+ cors_origins=["http://localhost:8008", "http://localhost:3000"],
380
+ )
381
+ web.run(host="localhost", port=8008, debug=False)