astris 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.
- astris-0.1.0/LICENSE +21 -0
- astris-0.1.0/PKG-INFO +99 -0
- astris-0.1.0/README.md +79 -0
- astris-0.1.0/astris/__init__.py +4 -0
- astris-0.1.0/astris/app.py +136 -0
- astris-0.1.0/astris/cli.py +128 -0
- astris-0.1.0/astris/component.py +58 -0
- astris-0.1.0/astris/lib.py +71 -0
- astris-0.1.0/astris.egg-info/PKG-INFO +99 -0
- astris-0.1.0/astris.egg-info/SOURCES.txt +20 -0
- astris-0.1.0/astris.egg-info/dependency_links.txt +1 -0
- astris-0.1.0/astris.egg-info/entry_points.txt +2 -0
- astris-0.1.0/astris.egg-info/requires.txt +2 -0
- astris-0.1.0/astris.egg-info/top_level.txt +1 -0
- astris-0.1.0/pyproject.toml +44 -0
- astris-0.1.0/setup.cfg +4 -0
- astris-0.1.0/tests/test_app_core.py +36 -0
- astris-0.1.0/tests/test_build.py +71 -0
- astris-0.1.0/tests/test_cli.py +45 -0
- astris-0.1.0/tests/test_component.py +25 -0
- astris-0.1.0/tests/test_lib.py +24 -0
- astris-0.1.0/tests/test_run_dev.py +61 -0
astris-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Astris contributors
|
|
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.
|
astris-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: astris
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Minimal Python framework for building static websites with component-style APIs.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: fastapi>=0.129.0
|
|
18
|
+
Requires-Dist: uvicorn>=0.41.0
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# Astris
|
|
22
|
+
|
|
23
|
+
[](https://github.com/fmanzano/pystro/actions/workflows/tests.yml)
|
|
24
|
+
|
|
25
|
+
Astris is a minimal Python framework for building static websites using component-style APIs.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install astris
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick start with CLI
|
|
34
|
+
|
|
35
|
+
Create a new project scaffold:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
astris new my_site
|
|
39
|
+
cd my_site
|
|
40
|
+
uv run python main.py
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Build static files:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv run python main.py build
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Basic usage
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from astris import AstrisApp
|
|
53
|
+
from astris.lib import Body, H1, Html
|
|
54
|
+
|
|
55
|
+
app = AstrisApp()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.page("/")
|
|
59
|
+
def home():
|
|
60
|
+
return Html(children=[
|
|
61
|
+
Body(children=[
|
|
62
|
+
H1(children=["Hello from Astris"]),
|
|
63
|
+
])
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
app.run_dev()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv sync --group dev
|
|
75
|
+
uv pip install -e .
|
|
76
|
+
uv run --group dev pytest
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Continuous Integration
|
|
80
|
+
|
|
81
|
+
GitHub Actions runs tests on push and pull request events targeting main using Python 3.11, 3.12, and 3.13.
|
|
82
|
+
The workflow is defined in `.github/workflows/tests.yml`.
|
|
83
|
+
|
|
84
|
+
## Release checklist
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
make release-check
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Equivalent manual commands:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
uv sync --group dev
|
|
94
|
+
uv run --group dev pytest
|
|
95
|
+
uv run --group dev python -m build
|
|
96
|
+
uv run --group dev twine check dist/*.whl dist/*.tar.gz
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`example.py` in this repository is an internal framework demo and not the standard end-user workflow.
|
astris-0.1.0/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Astris
|
|
2
|
+
|
|
3
|
+
[](https://github.com/fmanzano/pystro/actions/workflows/tests.yml)
|
|
4
|
+
|
|
5
|
+
Astris is a minimal Python framework for building static websites using component-style APIs.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install astris
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start with CLI
|
|
14
|
+
|
|
15
|
+
Create a new project scaffold:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
astris new my_site
|
|
19
|
+
cd my_site
|
|
20
|
+
uv run python main.py
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Build static files:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv run python main.py build
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Basic usage
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from astris import AstrisApp
|
|
33
|
+
from astris.lib import Body, H1, Html
|
|
34
|
+
|
|
35
|
+
app = AstrisApp()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.page("/")
|
|
39
|
+
def home():
|
|
40
|
+
return Html(children=[
|
|
41
|
+
Body(children=[
|
|
42
|
+
H1(children=["Hello from Astris"]),
|
|
43
|
+
])
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
app.run_dev()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Development
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv sync --group dev
|
|
55
|
+
uv pip install -e .
|
|
56
|
+
uv run --group dev pytest
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Continuous Integration
|
|
60
|
+
|
|
61
|
+
GitHub Actions runs tests on push and pull request events targeting main using Python 3.11, 3.12, and 3.13.
|
|
62
|
+
The workflow is defined in `.github/workflows/tests.yml`.
|
|
63
|
+
|
|
64
|
+
## Release checklist
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
make release-check
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Equivalent manual commands:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv sync --group dev
|
|
74
|
+
uv run --group dev pytest
|
|
75
|
+
uv run --group dev python -m build
|
|
76
|
+
uv run --group dev twine check dist/*.whl dist/*.tar.gz
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`example.py` in this repository is an internal framework demo and not the standard end-user workflow.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict
|
|
6
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
7
|
+
|
|
8
|
+
import uvicorn
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
from fastapi.responses import HTMLResponse
|
|
11
|
+
|
|
12
|
+
from .component import Component
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AstrisApp:
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.routes: Dict[str, Component] = {}
|
|
18
|
+
self._fastapi_app = FastAPI()
|
|
19
|
+
|
|
20
|
+
def page(self, path: str):
|
|
21
|
+
"""Decorator to register a page (route)."""
|
|
22
|
+
|
|
23
|
+
def decorator(func):
|
|
24
|
+
component_tree = func()
|
|
25
|
+
self.routes[path] = component_tree
|
|
26
|
+
|
|
27
|
+
@self._fastapi_app.get(path, response_class=HTMLResponse)
|
|
28
|
+
async def serve_page():
|
|
29
|
+
return f"<!DOCTYPE html>{component_tree.render()}"
|
|
30
|
+
|
|
31
|
+
return func
|
|
32
|
+
|
|
33
|
+
return decorator
|
|
34
|
+
|
|
35
|
+
def _infer_import_string(self) -> str | None:
|
|
36
|
+
main_module = sys.modules.get("__main__")
|
|
37
|
+
main_file = getattr(main_module, "__file__", None)
|
|
38
|
+
if not main_file:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
module_name = Path(main_file).stem
|
|
42
|
+
return f"{module_name}:app._fastapi_app"
|
|
43
|
+
|
|
44
|
+
def run_dev(self, port=8000, reload=True):
|
|
45
|
+
"""Start the development server."""
|
|
46
|
+
print(f"🚀 Dev server running at http://localhost:{port}")
|
|
47
|
+
|
|
48
|
+
if reload:
|
|
49
|
+
import_string = self._infer_import_string()
|
|
50
|
+
if import_string:
|
|
51
|
+
uvicorn.run(
|
|
52
|
+
import_string,
|
|
53
|
+
host="0.0.0.0",
|
|
54
|
+
port=port,
|
|
55
|
+
reload=True,
|
|
56
|
+
reload_dirs=[os.getcwd()],
|
|
57
|
+
)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
print("⚠️ Could not infer the main module, starting without hot reload.")
|
|
61
|
+
|
|
62
|
+
uvicorn.run(self._fastapi_app, host="0.0.0.0", port=port)
|
|
63
|
+
|
|
64
|
+
def _route_to_filename(self, route: str) -> str:
|
|
65
|
+
normalized = route if route.startswith("/") else f"/{route}"
|
|
66
|
+
if normalized == "/":
|
|
67
|
+
return "index.html"
|
|
68
|
+
return f"{normalized.strip('/')}.html"
|
|
69
|
+
|
|
70
|
+
def _resolve_route(self, path: str) -> str | None:
|
|
71
|
+
if path in self.routes:
|
|
72
|
+
return path
|
|
73
|
+
|
|
74
|
+
without_slash = path.rstrip("/")
|
|
75
|
+
if without_slash and without_slash in self.routes:
|
|
76
|
+
return without_slash
|
|
77
|
+
|
|
78
|
+
with_slash = f"{without_slash}/"
|
|
79
|
+
if with_slash in self.routes:
|
|
80
|
+
return with_slash
|
|
81
|
+
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def _rewrite_static_links(self, current_route: str, html: str) -> str:
|
|
85
|
+
current_file = self._route_to_filename(current_route)
|
|
86
|
+
current_dir = os.path.dirname(current_file) or "."
|
|
87
|
+
|
|
88
|
+
def replace_href(match: re.Match[str]) -> str:
|
|
89
|
+
quote = match.group("quote")
|
|
90
|
+
href_value = match.group("href")
|
|
91
|
+
|
|
92
|
+
split = urlsplit(href_value)
|
|
93
|
+
if split.scheme or split.netloc or split.path.startswith("//"):
|
|
94
|
+
return match.group(0)
|
|
95
|
+
|
|
96
|
+
resolved_route = self._resolve_route(split.path)
|
|
97
|
+
if not resolved_route:
|
|
98
|
+
return match.group(0)
|
|
99
|
+
|
|
100
|
+
target_file = self._route_to_filename(resolved_route)
|
|
101
|
+
relative_target = os.path.relpath(target_file, start=current_dir).replace(
|
|
102
|
+
os.sep, "/"
|
|
103
|
+
)
|
|
104
|
+
rebuilt_href = urlunsplit(
|
|
105
|
+
("", "", relative_target, split.query, split.fragment)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return f"href={quote}{rebuilt_href}{quote}"
|
|
109
|
+
|
|
110
|
+
return re.sub(
|
|
111
|
+
r'href=(?P<quote>["\'])(?P<href>.*?)(?P=quote)',
|
|
112
|
+
replace_href,
|
|
113
|
+
html,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def build(self, output_dir="dist"):
|
|
117
|
+
"""Generate static HTML files."""
|
|
118
|
+
print(f"📦 Building site into ./{output_dir}...")
|
|
119
|
+
|
|
120
|
+
if not os.path.exists(output_dir):
|
|
121
|
+
os.makedirs(output_dir)
|
|
122
|
+
|
|
123
|
+
for path, component in self.routes.items():
|
|
124
|
+
filename = self._route_to_filename(path)
|
|
125
|
+
filepath = os.path.join(output_dir, filename)
|
|
126
|
+
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
|
127
|
+
|
|
128
|
+
rendered_html = self._rewrite_static_links(path, component.render())
|
|
129
|
+
|
|
130
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
131
|
+
f.write("<!DOCTYPE html>\n")
|
|
132
|
+
f.write(rendered_html)
|
|
133
|
+
|
|
134
|
+
print(f" ✅ Generated: {filepath}")
|
|
135
|
+
|
|
136
|
+
print("✨ Build completed successfully.")
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
MAIN_TEMPLATE = """import sys
|
|
9
|
+
|
|
10
|
+
from astris import AstrisApp
|
|
11
|
+
from astris.lib import Body, H1, Html
|
|
12
|
+
|
|
13
|
+
app = AstrisApp()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.page("/")
|
|
17
|
+
def home():
|
|
18
|
+
return Html(children=[
|
|
19
|
+
Body(children=[
|
|
20
|
+
H1(children=["Hello, world from Astris!"]),
|
|
21
|
+
])
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
if len(sys.argv) > 1 and sys.argv[1] == "build":
|
|
27
|
+
app.build()
|
|
28
|
+
else:
|
|
29
|
+
app.run_dev()
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
README_TEMPLATE = """# {project_name}
|
|
33
|
+
|
|
34
|
+
Project generated with `astris new`.
|
|
35
|
+
|
|
36
|
+
## Run in development
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv run python main.py
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Build static site
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv run python main.py build
|
|
46
|
+
```
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
GITIGNORE_TEMPLATE = """__pycache__/
|
|
50
|
+
*.pyc
|
|
51
|
+
.venv/
|
|
52
|
+
dist/
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
REQUIREMENTS_TEMPLATE = "astris\n"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CliError(ValueError):
|
|
59
|
+
"""CLI usage error for project generation."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def validate_project_name(project_name: str) -> None:
|
|
63
|
+
"""Validate that the provided project name is filesystem friendly."""
|
|
64
|
+
pattern = r"^[A-Za-z][A-Za-z0-9_-]*$"
|
|
65
|
+
if not re.match(pattern, project_name):
|
|
66
|
+
raise CliError(
|
|
67
|
+
"Invalid project name. Use letters, numbers, '_' or '-', and start with a letter."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def create_project(project_name: str, base_path: Path | None = None) -> Path:
|
|
72
|
+
"""Create a minimal Astris project scaffold."""
|
|
73
|
+
validate_project_name(project_name)
|
|
74
|
+
|
|
75
|
+
root = (base_path or Path.cwd()) / project_name
|
|
76
|
+
if root.exists():
|
|
77
|
+
raise CliError(f"Target directory already exists: {root}")
|
|
78
|
+
|
|
79
|
+
root.mkdir(parents=True, exist_ok=False)
|
|
80
|
+
|
|
81
|
+
(root / "main.py").write_text(MAIN_TEMPLATE, encoding="utf-8")
|
|
82
|
+
(root / "README.md").write_text(
|
|
83
|
+
README_TEMPLATE.format(project_name=project_name), encoding="utf-8"
|
|
84
|
+
)
|
|
85
|
+
(root / ".gitignore").write_text(GITIGNORE_TEMPLATE, encoding="utf-8")
|
|
86
|
+
(root / "requirements.txt").write_text(REQUIREMENTS_TEMPLATE, encoding="utf-8")
|
|
87
|
+
|
|
88
|
+
return root
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
92
|
+
"""Build the top-level Astris CLI parser."""
|
|
93
|
+
parser = argparse.ArgumentParser(
|
|
94
|
+
prog="astris",
|
|
95
|
+
description="Astris CLI",
|
|
96
|
+
)
|
|
97
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
98
|
+
|
|
99
|
+
new_parser = subparsers.add_parser("new", help="Create a new Astris project")
|
|
100
|
+
new_parser.add_argument("project_name", help="Name of the project directory")
|
|
101
|
+
|
|
102
|
+
return parser
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main(argv: list[str] | None = None) -> int:
|
|
106
|
+
"""CLI entrypoint."""
|
|
107
|
+
parser = build_parser()
|
|
108
|
+
args = parser.parse_args(argv)
|
|
109
|
+
|
|
110
|
+
if args.command == "new":
|
|
111
|
+
try:
|
|
112
|
+
created = create_project(args.project_name)
|
|
113
|
+
except CliError as error:
|
|
114
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
115
|
+
return 2
|
|
116
|
+
|
|
117
|
+
print(f"Created Astris project at {created}")
|
|
118
|
+
print("Next steps:")
|
|
119
|
+
print(f" cd {args.project_name}")
|
|
120
|
+
print(" uv run python main.py")
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
parser.print_help()
|
|
124
|
+
return 1
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Dict, List, Optional, Union
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Component(ABC):
|
|
6
|
+
"""Base class for all UI elements (widgets)."""
|
|
7
|
+
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def render(self) -> str:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
def __str__(self):
|
|
13
|
+
return self.render()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Text(Component):
|
|
17
|
+
"""Render plain text without tags."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, content: str):
|
|
20
|
+
self.content = content
|
|
21
|
+
|
|
22
|
+
def render(self) -> str:
|
|
23
|
+
return str(self.content)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Element(Component):
|
|
27
|
+
"""
|
|
28
|
+
Represents a generic HTML tag.
|
|
29
|
+
Works like a Flutter-style container that accepts children.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
tag: str = "div"
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self, children: Optional[List[Union[Component, str]]] = None, **attributes
|
|
36
|
+
):
|
|
37
|
+
self.children = children or []
|
|
38
|
+
self.attributes = self._process_attributes(attributes)
|
|
39
|
+
|
|
40
|
+
def _process_attributes(self, attrs: Dict) -> Dict:
|
|
41
|
+
processed = {}
|
|
42
|
+
for key, value in attrs.items():
|
|
43
|
+
normalized_key = key.replace("class_name", "class").replace("_", "-")
|
|
44
|
+
processed[normalized_key] = value
|
|
45
|
+
return processed
|
|
46
|
+
|
|
47
|
+
def render(self) -> str:
|
|
48
|
+
attrs_str = " ".join([f'{k}="{v}"' for k, v in self.attributes.items()])
|
|
49
|
+
attrs_str = f" {attrs_str}" if attrs_str else ""
|
|
50
|
+
|
|
51
|
+
children_html = ""
|
|
52
|
+
for child in self.children:
|
|
53
|
+
if isinstance(child, Component):
|
|
54
|
+
children_html += child.render()
|
|
55
|
+
else:
|
|
56
|
+
children_html += str(child)
|
|
57
|
+
|
|
58
|
+
return f"<{self.tag}{attrs_str}>{children_html}</{self.tag}>"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from .component import Element
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Html(Element):
|
|
5
|
+
tag = "html"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Head(Element):
|
|
9
|
+
tag = "head"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Body(Element):
|
|
13
|
+
tag = "body"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Div(Element):
|
|
17
|
+
tag = "div"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Span(Element):
|
|
21
|
+
tag = "span"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class H1(Element):
|
|
25
|
+
tag = "h1"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class H2(Element):
|
|
29
|
+
tag = "h2"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class P(Element):
|
|
33
|
+
tag = "p"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class A(Element):
|
|
37
|
+
tag = "a"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Ul(Element):
|
|
41
|
+
tag = "ul"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Li(Element):
|
|
45
|
+
tag = "li"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Button(Element):
|
|
49
|
+
tag = "button"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Title(Element):
|
|
53
|
+
tag = "title"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Container(Div):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Column(Div):
|
|
61
|
+
def __init__(self, children=None, **kwargs):
|
|
62
|
+
super().__init__(
|
|
63
|
+
children, style="display: flex; flex-direction: column;", **kwargs
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Row(Div):
|
|
68
|
+
def __init__(self, children=None, **kwargs):
|
|
69
|
+
super().__init__(
|
|
70
|
+
children, style="display: flex; flex-direction: row;", **kwargs
|
|
71
|
+
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: astris
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Minimal Python framework for building static websites with component-style APIs.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: fastapi>=0.129.0
|
|
18
|
+
Requires-Dist: uvicorn>=0.41.0
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# Astris
|
|
22
|
+
|
|
23
|
+
[](https://github.com/fmanzano/pystro/actions/workflows/tests.yml)
|
|
24
|
+
|
|
25
|
+
Astris is a minimal Python framework for building static websites using component-style APIs.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install astris
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick start with CLI
|
|
34
|
+
|
|
35
|
+
Create a new project scaffold:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
astris new my_site
|
|
39
|
+
cd my_site
|
|
40
|
+
uv run python main.py
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Build static files:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv run python main.py build
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Basic usage
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from astris import AstrisApp
|
|
53
|
+
from astris.lib import Body, H1, Html
|
|
54
|
+
|
|
55
|
+
app = AstrisApp()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.page("/")
|
|
59
|
+
def home():
|
|
60
|
+
return Html(children=[
|
|
61
|
+
Body(children=[
|
|
62
|
+
H1(children=["Hello from Astris"]),
|
|
63
|
+
])
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
app.run_dev()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv sync --group dev
|
|
75
|
+
uv pip install -e .
|
|
76
|
+
uv run --group dev pytest
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Continuous Integration
|
|
80
|
+
|
|
81
|
+
GitHub Actions runs tests on push and pull request events targeting main using Python 3.11, 3.12, and 3.13.
|
|
82
|
+
The workflow is defined in `.github/workflows/tests.yml`.
|
|
83
|
+
|
|
84
|
+
## Release checklist
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
make release-check
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Equivalent manual commands:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
uv sync --group dev
|
|
94
|
+
uv run --group dev pytest
|
|
95
|
+
uv run --group dev python -m build
|
|
96
|
+
uv run --group dev twine check dist/*.whl dist/*.tar.gz
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`example.py` in this repository is an internal framework demo and not the standard end-user workflow.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
astris/__init__.py
|
|
5
|
+
astris/app.py
|
|
6
|
+
astris/cli.py
|
|
7
|
+
astris/component.py
|
|
8
|
+
astris/lib.py
|
|
9
|
+
astris.egg-info/PKG-INFO
|
|
10
|
+
astris.egg-info/SOURCES.txt
|
|
11
|
+
astris.egg-info/dependency_links.txt
|
|
12
|
+
astris.egg-info/entry_points.txt
|
|
13
|
+
astris.egg-info/requires.txt
|
|
14
|
+
astris.egg-info/top_level.txt
|
|
15
|
+
tests/test_app_core.py
|
|
16
|
+
tests/test_build.py
|
|
17
|
+
tests/test_cli.py
|
|
18
|
+
tests/test_component.py
|
|
19
|
+
tests/test_lib.py
|
|
20
|
+
tests/test_run_dev.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
astris
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "astris"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Minimal Python framework for building static websites with component-style APIs."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"fastapi>=0.129.0",
|
|
24
|
+
"uvicorn>=0.41.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
astris = "astris.cli:main"
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=8.4.2",
|
|
33
|
+
"pytest-cov>=7.0.0",
|
|
34
|
+
"build>=1.2.2.post1",
|
|
35
|
+
"twine>=6.2.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
include = ["astris*"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|
|
43
|
+
python_files = ["test_*.py"]
|
|
44
|
+
addopts = "-q"
|
astris-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import types
|
|
2
|
+
|
|
3
|
+
from astris import AstrisApp, Text
|
|
4
|
+
from astris.lib import Div
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_page_decorator_registers_route() -> None:
|
|
8
|
+
app = AstrisApp()
|
|
9
|
+
|
|
10
|
+
@app.page("/")
|
|
11
|
+
def home():
|
|
12
|
+
return Div(children=[Text("home")])
|
|
13
|
+
|
|
14
|
+
assert "/" in app.routes
|
|
15
|
+
assert app.routes["/"].render() == "<div>home</div>"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_route_helpers() -> None:
|
|
19
|
+
app = AstrisApp()
|
|
20
|
+
app.routes = {"/": Div(), "/about": Div(), "/blog/": Div()}
|
|
21
|
+
|
|
22
|
+
assert app._route_to_filename("/") == "index.html"
|
|
23
|
+
assert app._route_to_filename("/about") == "about.html"
|
|
24
|
+
assert app._route_to_filename("docs/setup") == "docs/setup.html"
|
|
25
|
+
|
|
26
|
+
assert app._resolve_route("/about") == "/about"
|
|
27
|
+
assert app._resolve_route("/blog") == "/blog/"
|
|
28
|
+
assert app._resolve_route("/missing") is None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_infer_import_string_from_main_module(monkeypatch) -> None:
|
|
32
|
+
app = AstrisApp()
|
|
33
|
+
fake_main = types.SimpleNamespace(__file__="/tmp/example.py")
|
|
34
|
+
monkeypatch.setitem(__import__("sys").modules, "__main__", fake_main)
|
|
35
|
+
|
|
36
|
+
assert app._infer_import_string() == "example:app._fastapi_app"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from astris import AstrisApp, Text
|
|
4
|
+
from astris.lib import A, Div
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_build_generates_static_files_and_doctype(tmp_path: Path) -> None:
|
|
8
|
+
app = AstrisApp()
|
|
9
|
+
|
|
10
|
+
@app.page("/")
|
|
11
|
+
def home():
|
|
12
|
+
return Div(
|
|
13
|
+
children=[
|
|
14
|
+
A(href="/about", children=["About"]),
|
|
15
|
+
Text(" "),
|
|
16
|
+
A(href="https://example.com", children=["Externo"]),
|
|
17
|
+
]
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
@app.page("/about")
|
|
21
|
+
def about():
|
|
22
|
+
return Div(
|
|
23
|
+
children=[
|
|
24
|
+
A(href="/", children=["Inicio"]),
|
|
25
|
+
A(href="/about?tab=team#top", children=["Anchor"]),
|
|
26
|
+
]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
output_dir = tmp_path / "site"
|
|
30
|
+
app.build(str(output_dir))
|
|
31
|
+
|
|
32
|
+
index_file = output_dir / "index.html"
|
|
33
|
+
about_file = output_dir / "about.html"
|
|
34
|
+
|
|
35
|
+
assert index_file.exists()
|
|
36
|
+
assert about_file.exists()
|
|
37
|
+
|
|
38
|
+
index_html = index_file.read_text(encoding="utf-8")
|
|
39
|
+
about_html = about_file.read_text(encoding="utf-8")
|
|
40
|
+
|
|
41
|
+
assert index_html.startswith("<!DOCTYPE html>")
|
|
42
|
+
assert about_html.startswith("<!DOCTYPE html>")
|
|
43
|
+
assert 'href="about.html"' in index_html
|
|
44
|
+
assert 'href="https://example.com"' in index_html
|
|
45
|
+
assert 'href="index.html"' in about_html
|
|
46
|
+
assert 'href="about.html?tab=team#top"' in about_html
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_build_rewrites_relative_links_for_nested_routes(tmp_path: Path) -> None:
|
|
50
|
+
app = AstrisApp()
|
|
51
|
+
|
|
52
|
+
@app.page("/about")
|
|
53
|
+
def about():
|
|
54
|
+
return Div(children=[Text("About")])
|
|
55
|
+
|
|
56
|
+
@app.page("/docs/getting-started")
|
|
57
|
+
def docs():
|
|
58
|
+
return Div(
|
|
59
|
+
children=[
|
|
60
|
+
A(href="/about", children=["About"]),
|
|
61
|
+
A(href="/missing", children=["Missing"]),
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
output_dir = tmp_path / "site"
|
|
66
|
+
app.build(str(output_dir))
|
|
67
|
+
|
|
68
|
+
docs_html = (output_dir / "docs/getting-started.html").read_text(encoding="utf-8")
|
|
69
|
+
|
|
70
|
+
assert 'href="../about.html"' in docs_html
|
|
71
|
+
assert 'href="/missing"' in docs_html
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from astris import cli
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_create_project_generates_expected_files(tmp_path: Path) -> None:
|
|
7
|
+
project_path = cli.create_project("demo_site", base_path=tmp_path)
|
|
8
|
+
|
|
9
|
+
assert project_path == tmp_path / "demo_site"
|
|
10
|
+
assert (project_path / "main.py").exists()
|
|
11
|
+
assert (project_path / "README.md").exists()
|
|
12
|
+
assert (project_path / ".gitignore").exists()
|
|
13
|
+
assert (project_path / "requirements.txt").exists()
|
|
14
|
+
|
|
15
|
+
main_content = (project_path / "main.py").read_text(encoding="utf-8")
|
|
16
|
+
assert "app = AstrisApp()" in main_content
|
|
17
|
+
assert '@app.page("/")' in main_content
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_create_project_fails_if_target_exists(tmp_path: Path) -> None:
|
|
21
|
+
existing = tmp_path / "demo_site"
|
|
22
|
+
existing.mkdir()
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
cli.create_project("demo_site", base_path=tmp_path)
|
|
26
|
+
assert False, "Expected CliError"
|
|
27
|
+
except cli.CliError as error:
|
|
28
|
+
assert "already exists" in str(error)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_create_project_rejects_invalid_name(tmp_path: Path) -> None:
|
|
32
|
+
try:
|
|
33
|
+
cli.create_project("123-invalid", base_path=tmp_path)
|
|
34
|
+
assert False, "Expected CliError"
|
|
35
|
+
except cli.CliError as error:
|
|
36
|
+
assert "Invalid project name" in str(error)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_main_new_command_returns_success(tmp_path: Path, monkeypatch) -> None:
|
|
40
|
+
monkeypatch.chdir(tmp_path)
|
|
41
|
+
|
|
42
|
+
exit_code = cli.main(["new", "hello_app"])
|
|
43
|
+
|
|
44
|
+
assert exit_code == 0
|
|
45
|
+
assert (tmp_path / "hello_app" / "main.py").exists()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from astris.component import Element, Text
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Div(Element):
|
|
5
|
+
tag = "div"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_text_render_and_str() -> None:
|
|
9
|
+
text = Text("hola")
|
|
10
|
+
|
|
11
|
+
assert text.render() == "hola"
|
|
12
|
+
assert str(text) == "hola"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_element_transforms_html_attributes() -> None:
|
|
16
|
+
element = Div(class_name="hero", data_test_id="banner")
|
|
17
|
+
|
|
18
|
+
assert element.attributes["class"] == "hero"
|
|
19
|
+
assert element.attributes["data-test-id"] == "banner"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_element_renders_children_components_and_strings() -> None:
|
|
23
|
+
element = Div(children=[Text("A"), "B", Div(children=["C"])])
|
|
24
|
+
|
|
25
|
+
assert element.render() == "<div>A B<div>C</div></div>".replace(" ", "")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from astris.lib import A, Column, Container, Head, Html, Row, Title
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_html_wrappers_render_expected_tags() -> None:
|
|
5
|
+
page = Html(
|
|
6
|
+
children=[
|
|
7
|
+
Head(children=[Title(children=["Demo"])]),
|
|
8
|
+
Container(children=[A(href="/", children=["Inicio"])]),
|
|
9
|
+
]
|
|
10
|
+
)
|
|
11
|
+
html = page.render()
|
|
12
|
+
|
|
13
|
+
assert "<html>" in html
|
|
14
|
+
assert "<head>" in html
|
|
15
|
+
assert "<title>Demo</title>" in html
|
|
16
|
+
assert '<a href="/">Inicio</a>' in html
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_column_and_row_include_default_flex_style() -> None:
|
|
20
|
+
column = Column(children=["x"])
|
|
21
|
+
row = Row(children=["y"])
|
|
22
|
+
|
|
23
|
+
assert "flex-direction: column" in column.render()
|
|
24
|
+
assert "flex-direction: row" in row.render()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from astris import AstrisApp
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_run_dev_uses_reload_import_string_when_available(monkeypatch) -> None:
|
|
7
|
+
app = AstrisApp()
|
|
8
|
+
calls = []
|
|
9
|
+
|
|
10
|
+
monkeypatch.setattr(app, "_infer_import_string", lambda: "example:app._fastapi_app")
|
|
11
|
+
|
|
12
|
+
def fake_run(*args, **kwargs):
|
|
13
|
+
calls.append((args, kwargs))
|
|
14
|
+
|
|
15
|
+
monkeypatch.setattr("astris.app.uvicorn.run", fake_run)
|
|
16
|
+
|
|
17
|
+
app.run_dev(port=9001, reload=True)
|
|
18
|
+
|
|
19
|
+
assert len(calls) == 1
|
|
20
|
+
args, kwargs = calls[0]
|
|
21
|
+
assert args[0] == "example:app._fastapi_app"
|
|
22
|
+
assert kwargs["reload"] is True
|
|
23
|
+
assert kwargs["port"] == 9001
|
|
24
|
+
assert kwargs["reload_dirs"] == [os.getcwd()]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_run_dev_falls_back_when_import_string_is_missing(monkeypatch) -> None:
|
|
28
|
+
app = AstrisApp()
|
|
29
|
+
calls = []
|
|
30
|
+
|
|
31
|
+
monkeypatch.setattr(app, "_infer_import_string", lambda: None)
|
|
32
|
+
|
|
33
|
+
def fake_run(*args, **kwargs):
|
|
34
|
+
calls.append((args, kwargs))
|
|
35
|
+
|
|
36
|
+
monkeypatch.setattr("astris.app.uvicorn.run", fake_run)
|
|
37
|
+
|
|
38
|
+
app.run_dev(port=9002, reload=True)
|
|
39
|
+
|
|
40
|
+
assert len(calls) == 1
|
|
41
|
+
args, kwargs = calls[0]
|
|
42
|
+
assert args[0] is app._fastapi_app
|
|
43
|
+
assert kwargs["port"] == 9002
|
|
44
|
+
assert "reload" not in kwargs
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_run_dev_without_reload_uses_fastapi_app(monkeypatch) -> None:
|
|
48
|
+
app = AstrisApp()
|
|
49
|
+
calls = []
|
|
50
|
+
|
|
51
|
+
def fake_run(*args, **kwargs):
|
|
52
|
+
calls.append((args, kwargs))
|
|
53
|
+
|
|
54
|
+
monkeypatch.setattr("astris.app.uvicorn.run", fake_run)
|
|
55
|
+
|
|
56
|
+
app.run_dev(port=9003, reload=False)
|
|
57
|
+
|
|
58
|
+
assert len(calls) == 1
|
|
59
|
+
args, kwargs = calls[0]
|
|
60
|
+
assert args[0] is app._fastapi_app
|
|
61
|
+
assert kwargs["port"] == 9003
|