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 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
+ [![Tests](https://github.com/fmanzano/pystro/actions/workflows/tests.yml/badge.svg)](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
+ [![Tests](https://github.com/fmanzano/pystro/actions/workflows/tests.yml/badge.svg)](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,4 @@
1
+ from .app import AstrisApp
2
+ from .component import Component, Element, Text
3
+
4
+ __all__ = ["AstrisApp", "Component", "Element", "Text"]
@@ -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
+ [![Tests](https://github.com/fmanzano/pystro/actions/workflows/tests.yml/badge.svg)](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,2 @@
1
+ [console_scripts]
2
+ astris = astris.cli:main
@@ -0,0 +1,2 @@
1
+ fastapi>=0.129.0
2
+ uvicorn>=0.41.0
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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