toypy 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 (66) hide show
  1. toypy-0.1.0/LICENSE +21 -0
  2. toypy-0.1.0/MANIFEST.in +5 -0
  3. toypy-0.1.0/PKG-INFO +80 -0
  4. toypy-0.1.0/README.md +57 -0
  5. toypy-0.1.0/pyproject.toml +40 -0
  6. toypy-0.1.0/setup.cfg +4 -0
  7. toypy-0.1.0/src/toy/__init__.py +47 -0
  8. toypy-0.1.0/src/toy/app.py +347 -0
  9. toypy-0.1.0/src/toy/bootstrap.py +73 -0
  10. toypy-0.1.0/src/toy/cli.py +150 -0
  11. toypy-0.1.0/src/toy/concurrency.py +69 -0
  12. toypy-0.1.0/src/toy/datafile.py +186 -0
  13. toypy-0.1.0/src/toy/inject.py +103 -0
  14. toypy-0.1.0/src/toy/injected.py +53 -0
  15. toypy-0.1.0/src/toy/jwt_tools.py +146 -0
  16. toypy-0.1.0/src/toy/loader.py +241 -0
  17. toypy-0.1.0/src/toy/mongo.py +120 -0
  18. toypy-0.1.0/src/toy/registry.py +176 -0
  19. toypy-0.1.0/src/toy/reload_entry.py +42 -0
  20. toypy-0.1.0/src/toy/response.py +97 -0
  21. toypy-0.1.0/src/toy/routing_dsl.py +212 -0
  22. toypy-0.1.0/src/toy/scaffold/GUIDE.md +226 -0
  23. toypy-0.1.0/src/toy/scaffold/MONGO.md +170 -0
  24. toypy-0.1.0/src/toy/scaffold/__init__.py +1 -0
  25. toypy-0.1.0/src/toy/scaffold/_vscode/settings.json +17 -0
  26. toypy-0.1.0/src/toy/scaffold/_vscode/toy.code-snippets +51 -0
  27. toypy-0.1.0/src/toy/scaffold/controllers/usersController.py +4 -0
  28. toypy-0.1.0/src/toy/scaffold/globalVariables.data +7 -0
  29. toypy-0.1.0/src/toy/scaffold/helpers/__init__.py +1 -0
  30. toypy-0.1.0/src/toy/scaffold/helpers/example.py +2 -0
  31. toypy-0.1.0/src/toy/scaffold/main.py +51 -0
  32. toypy-0.1.0/src/toy/scaffold/middlewares/__init__.py +1 -0
  33. toypy-0.1.0/src/toy/scaffold/middlewares/example.py +4 -0
  34. toypy-0.1.0/src/toy/scaffold/models/__init__.py +1 -0
  35. toypy-0.1.0/src/toy/scaffold/models/example.py +9 -0
  36. toypy-0.1.0/src/toy/scaffold/pyrightconfig.json +8 -0
  37. toypy-0.1.0/src/toy/scaffold/routes/__init__.py +1 -0
  38. toypy-0.1.0/src/toy/scaffold/routes/api.py +10 -0
  39. toypy-0.1.0/src/toy/scaffold/routes/health.py +5 -0
  40. toypy-0.1.0/src/toy/scaffold/routes/users.py +11 -0
  41. toypy-0.1.0/src/toy/scaffold/services/__init__.py +1 -0
  42. toypy-0.1.0/src/toy/scaffold/services/example.py +4 -0
  43. toypy-0.1.0/src/toy/scaffold/settings.py +18 -0
  44. toypy-0.1.0/src/toy/scaffold/sockets/__init__.py +1 -0
  45. toypy-0.1.0/src/toy/scaffold/sockets/events.py +18 -0
  46. toypy-0.1.0/src/toy/scaffold/static/.gitkeep +1 -0
  47. toypy-0.1.0/src/toy/scaffold/typings/__builtins__.pyi +112 -0
  48. toypy-0.1.0/src/toy/scaffold/uploads/.gitkeep +1 -0
  49. toypy-0.1.0/src/toy/sockets.py +148 -0
  50. toypy-0.1.0/src/toy/useable.py +164 -0
  51. toypy-0.1.0/src/toypy.egg-info/PKG-INFO +80 -0
  52. toypy-0.1.0/src/toypy.egg-info/SOURCES.txt +64 -0
  53. toypy-0.1.0/src/toypy.egg-info/dependency_links.txt +1 -0
  54. toypy-0.1.0/src/toypy.egg-info/entry_points.txt +2 -0
  55. toypy-0.1.0/src/toypy.egg-info/requires.txt +11 -0
  56. toypy-0.1.0/src/toypy.egg-info/top_level.txt +1 -0
  57. toypy-0.1.0/tests/test_app.py +55 -0
  58. toypy-0.1.0/tests/test_cli.py +36 -0
  59. toypy-0.1.0/tests/test_concurrency.py +15 -0
  60. toypy-0.1.0/tests/test_datafile.py +83 -0
  61. toypy-0.1.0/tests/test_jwt.py +9 -0
  62. toypy-0.1.0/tests/test_reload.py +46 -0
  63. toypy-0.1.0/tests/test_response.py +27 -0
  64. toypy-0.1.0/tests/test_routing_dsl.py +110 -0
  65. toypy-0.1.0/tests/test_useable.py +102 -0
  66. toypy-0.1.0/tests/test_useable_loader.py +60 -0
toypy-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Toy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ # Include all scaffold assets (including _vscode templates)
2
+ graft src/toy/scaffold
3
+ global-exclude __pycache__
4
+ global-exclude *.py[cod]
5
+ global-exclude .pytest_cache
toypy-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: toypy
3
+ Version: 0.1.0
4
+ Summary: Toy — Express-like Python backend framework with MongoDB, JWT, and WebSockets
5
+ Author: Toy
6
+ License: MIT
7
+ Project-URL: Homepage, https://pypi.org/project/toypy/
8
+ Keywords: toy,toypy,mongodb,express,jwt,websocket,backend
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: uvicorn[standard]>=0.30
13
+ Requires-Dist: starlette>=0.38
14
+ Requires-Dist: motor>=3.5
15
+ Requires-Dist: pymongo>=4.8
16
+ Requires-Dist: PyJWT>=2.8
17
+ Requires-Dist: python-dotenv>=1.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: httpx>=0.27; extra == "dev"
20
+ Requires-Dist: pytest>=8.0; extra == "dev"
21
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
22
+ Dynamic: license-file
23
+
24
+ # Toy
25
+
26
+ Express-like Python backend for beginners — MongoDB, JWT, WebSockets, nested file routing.
27
+
28
+ Default port: **313**.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install toypy
34
+ ```
35
+
36
+ ## Create a project
37
+
38
+ ```bash
39
+ mkdir my-api
40
+ cd my-api
41
+ toy make
42
+ python main.py
43
+ ```
44
+
45
+ Or: `toy run`
46
+
47
+ ## Beginner style
48
+
49
+ Put this one line at the top of files you edit (`main.py`, routes, controllers):
50
+
51
+ ```python
52
+ from toy.injected import * # framework wiring — you can ignore this line
53
+
54
+ db = connect_mongo()
55
+
56
+ use("/api", "routes/api.py")
57
+
58
+ @get("/health")
59
+ def health(req):
60
+ return makeResponse(200, data={"ok": True})
61
+
62
+ app.run(host=settings.HOST, port=settings.PORT, reload=settings.RELOAD)
63
+ ```
64
+
65
+ With `RELOAD = True` in `settings.py`, saving a file restarts the server automatically. Set `RELOAD = False` in production.
66
+
67
+ Nest routes up to **5 layers** with `use("/prefix", "file.py")`.
68
+ Attach controllers like Node: `get("/", usersController.list)`.
69
+
70
+ See **GUIDE.md** and **MONGO.md** after `toy make`.
71
+
72
+ ## Commands
73
+
74
+ ```text
75
+ toy make # scaffold project
76
+ toy make --force # overwrite scaffold files
77
+ toy run # run main.py with the loader
78
+ toy --help
79
+ toy --version
80
+ ```
toypy-0.1.0/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # Toy
2
+
3
+ Express-like Python backend for beginners — MongoDB, JWT, WebSockets, nested file routing.
4
+
5
+ Default port: **313**.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install toypy
11
+ ```
12
+
13
+ ## Create a project
14
+
15
+ ```bash
16
+ mkdir my-api
17
+ cd my-api
18
+ toy make
19
+ python main.py
20
+ ```
21
+
22
+ Or: `toy run`
23
+
24
+ ## Beginner style
25
+
26
+ Put this one line at the top of files you edit (`main.py`, routes, controllers):
27
+
28
+ ```python
29
+ from toy.injected import * # framework wiring — you can ignore this line
30
+
31
+ db = connect_mongo()
32
+
33
+ use("/api", "routes/api.py")
34
+
35
+ @get("/health")
36
+ def health(req):
37
+ return makeResponse(200, data={"ok": True})
38
+
39
+ app.run(host=settings.HOST, port=settings.PORT, reload=settings.RELOAD)
40
+ ```
41
+
42
+ With `RELOAD = True` in `settings.py`, saving a file restarts the server automatically. Set `RELOAD = False` in production.
43
+
44
+ Nest routes up to **5 layers** with `use("/prefix", "file.py")`.
45
+ Attach controllers like Node: `get("/", usersController.list)`.
46
+
47
+ See **GUIDE.md** and **MONGO.md** after `toy make`.
48
+
49
+ ## Commands
50
+
51
+ ```text
52
+ toy make # scaffold project
53
+ toy make --force # overwrite scaffold files
54
+ toy run # run main.py with the loader
55
+ toy --help
56
+ toy --version
57
+ ```
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "toypy"
7
+ version = "0.1.0"
8
+ description = "Toy — Express-like Python backend framework with MongoDB, JWT, and WebSockets"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Toy" }]
13
+ keywords = ["toy", "toypy", "mongodb", "express", "jwt", "websocket", "backend"]
14
+ dependencies = [
15
+ "uvicorn[standard]>=0.30",
16
+ "starlette>=0.38",
17
+ "motor>=3.5",
18
+ "pymongo>=4.8",
19
+ "PyJWT>=2.8",
20
+ "python-dotenv>=1.0",
21
+ ]
22
+
23
+ [project.optional-dependencies]
24
+ dev = ["httpx>=0.27", "pytest>=8.0", "pytest-asyncio>=0.23"]
25
+
26
+ [project.scripts]
27
+ toy = "toy.cli:main"
28
+
29
+ [project.urls]
30
+ Homepage = "https://pypi.org/project/toypy/"
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
34
+
35
+ [tool.setuptools.package-data]
36
+ toy = ["scaffold/*", "scaffold/**/*"]
37
+
38
+ [tool.pytest.ini_options]
39
+ asyncio_mode = "auto"
40
+ testpaths = ["tests"]
toypy-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,47 @@
1
+ """toy — Express-like Python backend with MongoDB, JWT, and WebSockets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ from .app import App, BoneRequest
8
+ from .concurrency import map_pool, run_in_pool, run_tasks
9
+ from .jwt_tools import jwt, protect, sign, verify
10
+ from .loader import run_project
11
+ from .mongo import MongoDB, close_mongo, connect_mongo, get_db
12
+ from .response import BoneResponse, makeResponse
13
+ from .routing_dsl import MAX_MOUNT_DEPTH, delete, get, patch, post, put, use
14
+ from .sockets import Socket, SocketManager, sockets
15
+ from .useable import useable, useable_InFolder
16
+
17
+ __all__ = [
18
+ "App",
19
+ "BoneRequest",
20
+ "BoneResponse",
21
+ "MAX_MOUNT_DEPTH",
22
+ "MongoDB",
23
+ "Socket",
24
+ "SocketManager",
25
+ "close_mongo",
26
+ "connect_mongo",
27
+ "delete",
28
+ "get",
29
+ "get_db",
30
+ "jwt",
31
+ "makeResponse",
32
+ "map_pool",
33
+ "patch",
34
+ "post",
35
+ "protect",
36
+ "put",
37
+ "run_in_pool",
38
+ "run_project",
39
+ "run_tasks",
40
+ "sign",
41
+ "sockets",
42
+ "use",
43
+ "useable",
44
+ "useable_InFolder",
45
+ "verify",
46
+ "__version__",
47
+ ]
@@ -0,0 +1,347 @@
1
+ """toy App — Express-like HTTP server on Starlette/Uvicorn."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import os
7
+ from contextlib import asynccontextmanager
8
+ from pathlib import Path
9
+ from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
10
+
11
+ from starlette.applications import Starlette
12
+ from starlette.middleware.cors import CORSMiddleware
13
+ from starlette.requests import Request
14
+ from starlette.routing import Route, WebSocketRoute
15
+ from starlette.staticfiles import StaticFiles
16
+
17
+ from .response import as_http_response, makeResponse
18
+
19
+ Handler = Callable[..., Any]
20
+ Middleware = Callable[..., Any]
21
+
22
+
23
+ class BoneRequest:
24
+ """Thin wrapper around Starlette Request with Express-like helpers."""
25
+
26
+ def __init__(self, request: Request):
27
+ self._request = request
28
+ self.user: Any = None
29
+ self.state: Dict[str, Any] = {}
30
+
31
+ @property
32
+ def method(self) -> str:
33
+ return self._request.method
34
+
35
+ @property
36
+ def url(self) -> Any:
37
+ return self._request.url
38
+
39
+ @property
40
+ def path(self) -> str:
41
+ return self._request.url.path
42
+
43
+ @property
44
+ def headers(self) -> Any:
45
+ return self._request.headers
46
+
47
+ @property
48
+ def query(self) -> Dict[str, str]:
49
+ return dict(self._request.query_params)
50
+
51
+ @property
52
+ def params(self) -> Dict[str, Any]:
53
+ return dict(self._request.path_params)
54
+
55
+ @property
56
+ def path_params(self) -> Dict[str, Any]:
57
+ return dict(self._request.path_params)
58
+
59
+ @property
60
+ def cookies(self) -> Dict[str, str]:
61
+ return dict(self._request.cookies)
62
+
63
+ @property
64
+ def client(self) -> Any:
65
+ return self._request.client
66
+
67
+ @property
68
+ def raw(self) -> Request:
69
+ return self._request
70
+
71
+ async def json(self) -> Any:
72
+ try:
73
+ return await self._request.json()
74
+ except Exception:
75
+ return {}
76
+
77
+ async def body(self) -> bytes:
78
+ return await self._request.body()
79
+
80
+ async def form(self) -> Any:
81
+ return await self._request.form()
82
+
83
+ def get(self, key: str, default: Any = None) -> Any:
84
+ return self.query.get(key, self.params.get(key, default))
85
+
86
+
87
+ class App:
88
+ """
89
+ toy application.
90
+
91
+ Example
92
+ -------
93
+ app = App()
94
+
95
+ @app.get(\"/health\")
96
+ async def health(req):
97
+ return makeResponse(200, data={\"ok\": True})
98
+
99
+ app.run() # port 313
100
+ """
101
+
102
+ def __init__(self, title: str = "toy"):
103
+ self.title = title
104
+ self._routes: List[Tuple[str, str, Handler, List[Middleware]]] = []
105
+ self._middlewares: List[Middleware] = []
106
+ self._startup: List[Callable] = []
107
+ self._shutdown: List[Callable] = []
108
+ self._socket_manager = None
109
+ self._static_mounts: List[Tuple[str, str]] = []
110
+ self._starlette: Optional[Starlette] = None
111
+
112
+ def use(self, middleware: Middleware) -> "App":
113
+ """Register global middleware: ``async def mw(req, next): ...``."""
114
+ self._middlewares.append(middleware)
115
+ return self
116
+
117
+ def on_startup(self, fn: Callable) -> Callable:
118
+ self._startup.append(fn)
119
+ return fn
120
+
121
+ def on_shutdown(self, fn: Callable) -> Callable:
122
+ self._shutdown.append(fn)
123
+ return fn
124
+
125
+ def static(self, url_path: str, directory: str) -> "App":
126
+ self._static_mounts.append((url_path, directory))
127
+ return self
128
+
129
+ def _add_route(
130
+ self,
131
+ method: str,
132
+ path: str,
133
+ handler: Optional[Handler] = None,
134
+ middlewares: Optional[Sequence[Middleware]] = None,
135
+ ):
136
+ mws = list(middlewares or [])
137
+
138
+ def decorator(fn: Handler) -> Handler:
139
+ self._routes.append((method.upper(), path, fn, mws))
140
+ return fn
141
+
142
+ if handler is not None:
143
+ return decorator(handler)
144
+ return decorator
145
+
146
+ def get(self, path: str, *middlewares: Middleware):
147
+ return self._add_route("GET", path, middlewares=middlewares)
148
+
149
+ def post(self, path: str, *middlewares: Middleware):
150
+ return self._add_route("POST", path, middlewares=middlewares)
151
+
152
+ def put(self, path: str, *middlewares: Middleware):
153
+ return self._add_route("PUT", path, middlewares=middlewares)
154
+
155
+ def patch(self, path: str, *middlewares: Middleware):
156
+ return self._add_route("PATCH", path, middlewares=middlewares)
157
+
158
+ def delete(self, path: str, *middlewares: Middleware):
159
+ return self._add_route("DELETE", path, middlewares=middlewares)
160
+
161
+ def route(self, path: str, methods: Sequence[str] = ("GET",), *middlewares: Middleware):
162
+ def decorator(fn: Handler) -> Handler:
163
+ for method in methods:
164
+ self._add_route(method, path, fn, middlewares)
165
+ return fn
166
+
167
+ return decorator
168
+
169
+ def _build_handler(self, handler: Handler, route_mws: List[Middleware]) -> Callable:
170
+ chain = list(self._middlewares) + list(route_mws)
171
+
172
+ async def endpoint(request: Request):
173
+ req = BoneRequest(request)
174
+
175
+ async def call_handler(r: BoneRequest):
176
+ result = handler(r)
177
+ if inspect.isawaitable(result):
178
+ result = await result
179
+ return result
180
+
181
+ async def invoke(index: int, r: BoneRequest):
182
+ if index >= len(chain):
183
+ return await call_handler(r)
184
+
185
+ mw = chain[index]
186
+
187
+ async def next_handler(next_req: BoneRequest = r):
188
+ return await invoke(index + 1, next_req)
189
+
190
+ result = mw(r, next_handler)
191
+ if inspect.isawaitable(result):
192
+ result = await result
193
+ return result
194
+
195
+ try:
196
+ value = await invoke(0, req)
197
+ return as_http_response(value)
198
+ except Exception as err:
199
+ return makeResponse(500, error=str(err)).to_starlette()
200
+
201
+ return endpoint
202
+
203
+ def asgi(self) -> Starlette:
204
+ if self._starlette is not None:
205
+ return self._starlette
206
+
207
+ routes = []
208
+ # Group by path for multiple methods
209
+ by_path: Dict[str, Dict[str, Tuple[Handler, List[Middleware]]]] = {}
210
+ for method, path, handler, mws in self._routes:
211
+ by_path.setdefault(path, {})[method] = (handler, mws)
212
+
213
+ for path, methods_map in by_path.items():
214
+ # Starlette Route accepts one endpoint; dispatch by method inside
215
+ async def dispatcher(request: Request, _map=methods_map):
216
+ method = request.method.upper()
217
+ if method == "HEAD" and "GET" in _map:
218
+ method = "GET"
219
+ if method not in _map:
220
+ return makeResponse(405, error=f"Method {request.method} not allowed").to_starlette()
221
+ handler, mws = _map[method]
222
+ endpoint = self._build_handler(handler, mws)
223
+ return await endpoint(request)
224
+
225
+ routes.append(Route(path, dispatcher, methods=list(methods_map.keys()) + (["HEAD"] if "GET" in methods_map else [])))
226
+
227
+ if self._socket_manager is not None:
228
+ routes.append(WebSocketRoute("/ws", self._socket_manager.endpoint))
229
+ routes.append(WebSocketRoute("/socket", self._socket_manager.endpoint))
230
+
231
+ @asynccontextmanager
232
+ async def lifespan(_app):
233
+ for fn in self._startup:
234
+ result = fn()
235
+ if inspect.isawaitable(result):
236
+ await result
237
+ yield
238
+ for fn in self._shutdown:
239
+ result = fn()
240
+ if inspect.isawaitable(result):
241
+ await result
242
+
243
+ app = Starlette(routes=routes, lifespan=lifespan)
244
+ app.add_middleware(
245
+ CORSMiddleware,
246
+ allow_origins=["*"],
247
+ allow_credentials=True,
248
+ allow_methods=["*"],
249
+ allow_headers=["*"],
250
+ )
251
+ for url_path, directory in self._static_mounts:
252
+ if os.path.isdir(directory):
253
+ app.mount(url_path, StaticFiles(directory=directory), name=url_path.strip("/").replace("/", "_") or "static")
254
+
255
+ self._starlette = app
256
+ return app
257
+
258
+ def run(
259
+ self,
260
+ host: Optional[str] = None,
261
+ port: Optional[int] = None,
262
+ reload: Optional[bool] = None,
263
+ **uvicorn_kwargs: Any,
264
+ ) -> None:
265
+ """
266
+ Start the server.
267
+
268
+ Default port is 313 (override with PORT env/settings or port=).
269
+ When ``reload`` is True, Uvicorn watches the project and restarts on file changes.
270
+ """
271
+ import inspect
272
+ import uvicorn
273
+
274
+ # Allow settings.RELOAD / env when arg omitted
275
+ if reload is None:
276
+ env_reload = os.environ.get("RELOAD") or os.environ.get("TOY_RELOAD")
277
+ if env_reload is not None and env_reload != "":
278
+ reload = str(env_reload).strip().lower() in {"1", "true", "yes", "on"}
279
+ else:
280
+ reload = _setting_bool("RELOAD", False)
281
+
282
+ # Skip starting when reload factory is loading main.py
283
+ if os.environ.get("TOY_SKIP_RUN") == "1":
284
+ return
285
+
286
+ resolved_host = host or os.environ.get("HOST") or _setting("HOST", "0.0.0.0")
287
+ if port is None:
288
+ port_val = os.environ.get("PORT") or _setting("PORT", "313")
289
+ port = int(port_val)
290
+
291
+ if not reload:
292
+ uvicorn.run(self.asgi(), host=resolved_host, port=port, reload=False, **uvicorn_kwargs)
293
+ return
294
+
295
+ # Resolve project root from caller main.py
296
+ project_root = Path.cwd()
297
+ main_name = "main.py"
298
+ frame = inspect.currentframe()
299
+ try:
300
+ if frame and frame.f_back:
301
+ caller_file = frame.f_back.f_globals.get("__file__")
302
+ if caller_file:
303
+ caller_path = Path(str(caller_file)).resolve()
304
+ project_root = caller_path.parent
305
+ main_name = caller_path.name
306
+ finally:
307
+ del frame
308
+
309
+ os.environ["TOY_PROJECT"] = str(project_root)
310
+ os.environ["TOY_MAIN"] = main_name
311
+
312
+ uvicorn.run(
313
+ "toy.reload_entry:create_app",
314
+ factory=True,
315
+ host=resolved_host,
316
+ port=port,
317
+ reload=True,
318
+ reload_dirs=[str(project_root)],
319
+ **uvicorn_kwargs,
320
+ )
321
+
322
+
323
+ def _setting(name: str, default: str) -> str:
324
+ try:
325
+ import settings as user_settings # type: ignore
326
+
327
+ value = getattr(user_settings, name, None)
328
+ if value is not None and value != "":
329
+ return str(value)
330
+ except Exception:
331
+ pass
332
+ return default
333
+
334
+
335
+ def _setting_bool(name: str, default: bool) -> bool:
336
+ raw = _setting(name, "")
337
+ if raw == "":
338
+ try:
339
+ import settings as user_settings # type: ignore
340
+
341
+ value = getattr(user_settings, name, None)
342
+ if isinstance(value, bool):
343
+ return value
344
+ except Exception:
345
+ pass
346
+ return default
347
+ return str(raw).strip().lower() in {"1", "true", "yes", "on"}
@@ -0,0 +1,73 @@
1
+ """
2
+ Bootstrap Toy project loading.
3
+
4
+ Scaffold ``main.py`` starts with:
5
+
6
+ from toy.injected import * # framework wiring — you can ignore this line
7
+
8
+ That import loads builtins for the editor and re-loads ``main.py`` under the
9
+ project loader at runtime.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import inspect
15
+ import sys
16
+ from pathlib import Path
17
+
18
+
19
+ def _caller_info() -> tuple[Path | None, str | None]:
20
+ frame = inspect.currentframe()
21
+ try:
22
+ while frame is not None:
23
+ pkg = frame.f_globals.get("__package__") or ""
24
+ # Skip frames inside the toy package itself
25
+ if pkg == "toy" or pkg.startswith("toy."):
26
+ frame = frame.f_back
27
+ continue
28
+ filename = frame.f_code.co_filename
29
+ norm = filename.replace("\\", "/")
30
+ if filename and "importlib" not in norm:
31
+ path = Path(filename).resolve()
32
+ if path.suffix == ".py":
33
+ name = frame.f_globals.get("__name__")
34
+ return path, name
35
+ frame = frame.f_back
36
+ return None, None
37
+ finally:
38
+ del frame
39
+
40
+
41
+ def bootstrap_from_import(*, exit_after: bool = True) -> None:
42
+ """Re-exec the importing project file under the Toy loader."""
43
+ if getattr(sys, "_toy_in_loader", False):
44
+ return
45
+ if getattr(sys, "_toy_bootstrapped", False):
46
+ return
47
+
48
+ caller, mod_name = _caller_info()
49
+ if caller is None:
50
+ return
51
+ # Only auto-wire when the user runs the file (python main.py)
52
+ if mod_name != "__main__":
53
+ return
54
+
55
+ sys._toy_bootstrapped = True # type: ignore[attr-defined]
56
+ sys._toy_in_loader = True # type: ignore[attr-defined]
57
+ try:
58
+ from .loader import ProjectLoader
59
+ from .routing_dsl import set_context
60
+
61
+ loader = ProjectLoader(caller.parent)
62
+ loader.bootstrap_scan()
63
+ set_context(loader.ctx)
64
+ loader.load_file(caller)
65
+ finally:
66
+ sys._toy_in_loader = False # type: ignore[attr-defined]
67
+
68
+ if exit_after:
69
+ raise SystemExit(0)
70
+
71
+
72
+ # Auto-run on import when appropriate
73
+ bootstrap_from_import()