pyaether-cli 0.0.3__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.
- aether_cli/__init__.py +1 -0
- aether_cli/__main__.py +4 -0
- aether_cli/build_process.py +110 -0
- aether_cli/cli.py +197 -0
- aether_cli/configs.py +108 -0
- aether_cli/run_server.py +44 -0
- aether_cli/utils.py +50 -0
- pyaether_cli-0.0.3.dist-info/METADATA +23 -0
- pyaether_cli-0.0.3.dist-info/RECORD +11 -0
- pyaether_cli-0.0.3.dist-info/WHEEL +4 -0
- pyaether_cli-0.0.3.dist-info/entry_points.txt +2 -0
aether_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.3"
|
aether_cli/__main__.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from aether import render
|
|
4
|
+
from bs4 import BeautifulSoup
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
from .utils import load_build_function_instance
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _update_paths_in_soup(
|
|
11
|
+
console: Console,
|
|
12
|
+
soup: BeautifulSoup,
|
|
13
|
+
prefix: str,
|
|
14
|
+
static_assets_dir: Path,
|
|
15
|
+
static_css_dir: Path,
|
|
16
|
+
static_js_dir: Path,
|
|
17
|
+
output_dir: Path,
|
|
18
|
+
verbose: bool = False,
|
|
19
|
+
) -> BeautifulSoup:
|
|
20
|
+
if verbose:
|
|
21
|
+
console.print("Updating paths in HTML...")
|
|
22
|
+
|
|
23
|
+
# Update link, img, script & a tags
|
|
24
|
+
for tag_name, attribute in [
|
|
25
|
+
("link", "href"),
|
|
26
|
+
("img", "src"),
|
|
27
|
+
("script", "src"),
|
|
28
|
+
("a", "href"),
|
|
29
|
+
]:
|
|
30
|
+
for tag in soup.find_all(tag_name):
|
|
31
|
+
if attribute in tag.attrs:
|
|
32
|
+
if not tag[attribute].startswith(("http://", "https://", "/")):
|
|
33
|
+
old_path = Path(tag[attribute])
|
|
34
|
+
if Path("styles") in old_path.parents:
|
|
35
|
+
new_path = static_css_dir / old_path.relative_to("styles")
|
|
36
|
+
elif Path("js_scripts") in old_path.parents:
|
|
37
|
+
new_path = static_js_dir / old_path.relative_to("js_scripts")
|
|
38
|
+
elif Path("assets") in old_path.parents:
|
|
39
|
+
new_path = static_assets_dir / old_path.relative_to("assets")
|
|
40
|
+
elif Path("public") in old_path.parents:
|
|
41
|
+
new_path = output_dir / old_path.relative_to("public")
|
|
42
|
+
elif ".css" in old_path.suffixes:
|
|
43
|
+
new_path = static_css_dir / old_path.name
|
|
44
|
+
else:
|
|
45
|
+
# Note: Need to find a better way to handle pages condition. Right now, we are just shoving it in this else block.
|
|
46
|
+
# Note: Assuming the stem in href is same as the page_name mentioned in the config.
|
|
47
|
+
if ".html" in old_path.suffixes:
|
|
48
|
+
new_path = output_dir / old_path.relative_to(".")
|
|
49
|
+
else:
|
|
50
|
+
new_path = output_dir / old_path.name
|
|
51
|
+
|
|
52
|
+
new_path = new_path.relative_to(output_dir)
|
|
53
|
+
if prefix:
|
|
54
|
+
new_path = prefix / new_path
|
|
55
|
+
|
|
56
|
+
if verbose:
|
|
57
|
+
console.print(
|
|
58
|
+
f"Updating {attribute}: {old_path} -> /{new_path}"
|
|
59
|
+
)
|
|
60
|
+
tag[attribute] = f"/{new_path}"
|
|
61
|
+
|
|
62
|
+
return soup
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def builder(
|
|
66
|
+
console: Console,
|
|
67
|
+
output_html_file_name: str,
|
|
68
|
+
output_dir: Path,
|
|
69
|
+
prefix: str,
|
|
70
|
+
file_target: Path,
|
|
71
|
+
function_target: str,
|
|
72
|
+
static_assets_dir: Path,
|
|
73
|
+
static_css_dir: Path,
|
|
74
|
+
static_js_dir: Path,
|
|
75
|
+
verbose: bool = False,
|
|
76
|
+
) -> None:
|
|
77
|
+
if verbose:
|
|
78
|
+
console.print(
|
|
79
|
+
f"Loading build function instance for '{output_html_file_name}.html'..."
|
|
80
|
+
)
|
|
81
|
+
instance = load_build_function_instance(file_target, function_target)
|
|
82
|
+
|
|
83
|
+
if verbose:
|
|
84
|
+
console.print("Rendering HTML...")
|
|
85
|
+
|
|
86
|
+
rendered_html = render(instance())
|
|
87
|
+
soup = BeautifulSoup(rendered_html, "html.parser")
|
|
88
|
+
updated_soup = _update_paths_in_soup(
|
|
89
|
+
console=console,
|
|
90
|
+
soup=soup,
|
|
91
|
+
prefix=prefix,
|
|
92
|
+
output_dir=output_dir,
|
|
93
|
+
static_assets_dir=static_assets_dir,
|
|
94
|
+
static_css_dir=static_css_dir,
|
|
95
|
+
static_js_dir=static_js_dir,
|
|
96
|
+
verbose=verbose,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
output_html_path = output_dir / output_html_file_name
|
|
100
|
+
if not output_html_path.parent.exists():
|
|
101
|
+
if verbose:
|
|
102
|
+
console.print(f"Creating routing directory: {output_html_path.parent}")
|
|
103
|
+
|
|
104
|
+
output_html_path.parent.mkdir(parents=True)
|
|
105
|
+
|
|
106
|
+
if verbose:
|
|
107
|
+
console.print("Writing final HTML to file...")
|
|
108
|
+
|
|
109
|
+
with open(output_html_path, "w", encoding="utf-8") as file:
|
|
110
|
+
file.write(updated_soup.decode(pretty_print=True, formatter="html5"))
|
aether_cli/cli.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import signal
|
|
3
|
+
import threading
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.live import Live
|
|
9
|
+
from rich.spinner import Spinner
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .build_process import builder
|
|
14
|
+
from .configs import configs
|
|
15
|
+
from .run_server import tcp_server
|
|
16
|
+
from .utils import get_local_ip
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.group()
|
|
20
|
+
def main() -> None:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@main.command()
|
|
25
|
+
def version():
|
|
26
|
+
console = Console()
|
|
27
|
+
console.print(f"Aether CLI version: [green]{__version__}[/green]")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@main.command()
|
|
31
|
+
@click.option(
|
|
32
|
+
"--prefix",
|
|
33
|
+
type=str,
|
|
34
|
+
default="",
|
|
35
|
+
help="Inject 'base_path' prefix in the generated HTML file.",
|
|
36
|
+
)
|
|
37
|
+
@click.option("--verbose", is_flag=True, help="Enable verbose mode to echo steps.")
|
|
38
|
+
def build(prefix: str, verbose: bool) -> None:
|
|
39
|
+
console = Console()
|
|
40
|
+
|
|
41
|
+
def _create_dir_if_not_exists(directory: Path) -> None:
|
|
42
|
+
if not directory.exists():
|
|
43
|
+
if verbose:
|
|
44
|
+
console.print(f"Creating directory: {directory}")
|
|
45
|
+
directory.mkdir(parents=True)
|
|
46
|
+
|
|
47
|
+
_create_dir_if_not_exists(configs.build_config.output_dir)
|
|
48
|
+
|
|
49
|
+
static_dir = configs.build_config.output_dir / "static"
|
|
50
|
+
# _create_dir_if_not_exists(configs.build_config.output_dir)
|
|
51
|
+
|
|
52
|
+
static_css_dir = static_dir / "css"
|
|
53
|
+
_create_dir_if_not_exists(static_css_dir)
|
|
54
|
+
|
|
55
|
+
static_js_dir = static_dir / "js"
|
|
56
|
+
_create_dir_if_not_exists(static_js_dir)
|
|
57
|
+
|
|
58
|
+
static_assets_dir = static_dir / "assets"
|
|
59
|
+
_create_dir_if_not_exists(static_assets_dir)
|
|
60
|
+
|
|
61
|
+
def _copy_dir(src: Path | None, dest: Path, directory_name: str) -> None:
|
|
62
|
+
if src and src.exists():
|
|
63
|
+
if verbose:
|
|
64
|
+
console.print(f"Copying {directory_name} from {src} to {dest}")
|
|
65
|
+
shutil.copytree(src, dest, dirs_exist_ok=True)
|
|
66
|
+
else:
|
|
67
|
+
console.print(f"Project doesn't have a '{src}' directory.")
|
|
68
|
+
|
|
69
|
+
_copy_dir(configs.static_content_config.assets_dir, static_assets_dir, "assets")
|
|
70
|
+
_copy_dir(configs.static_content_config.styles_dir, static_css_dir, "styles")
|
|
71
|
+
_copy_dir(configs.static_content_config.js_scripts_dir, static_js_dir, "js_scripts")
|
|
72
|
+
_copy_dir(
|
|
73
|
+
configs.static_content_config.public_dir,
|
|
74
|
+
configs.build_config.output_dir,
|
|
75
|
+
"public",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if verbose:
|
|
79
|
+
console.print("Building Index HTML...")
|
|
80
|
+
|
|
81
|
+
builder(
|
|
82
|
+
console=console,
|
|
83
|
+
output_html_file_name="index.html",
|
|
84
|
+
output_dir=configs.build_config.output_dir,
|
|
85
|
+
prefix=prefix,
|
|
86
|
+
file_target=configs.build_config.index_page_file_target,
|
|
87
|
+
function_target=configs.build_config.index_page_function_target,
|
|
88
|
+
static_assets_dir=static_assets_dir,
|
|
89
|
+
static_css_dir=static_css_dir,
|
|
90
|
+
static_js_dir=static_js_dir,
|
|
91
|
+
verbose=verbose,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if configs.build_config.pages_file_targets:
|
|
95
|
+
if verbose:
|
|
96
|
+
console.print("Building Pages...")
|
|
97
|
+
|
|
98
|
+
for file_target, function_target, page_name in zip(
|
|
99
|
+
configs.build_config.pages_file_targets,
|
|
100
|
+
configs.build_config.pages_function_targets,
|
|
101
|
+
configs.build_config.pages_names,
|
|
102
|
+
strict=False,
|
|
103
|
+
):
|
|
104
|
+
builder(
|
|
105
|
+
console=console,
|
|
106
|
+
output_html_file_name=f"{page_name}.html",
|
|
107
|
+
output_dir=configs.build_config.output_dir,
|
|
108
|
+
prefix=prefix,
|
|
109
|
+
file_target=file_target,
|
|
110
|
+
function_target=function_target,
|
|
111
|
+
static_assets_dir=static_assets_dir,
|
|
112
|
+
static_css_dir=static_css_dir,
|
|
113
|
+
static_js_dir=static_js_dir,
|
|
114
|
+
verbose=verbose,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
console.print("\n[bold green]Build successful![/bold green]")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@main.command()
|
|
121
|
+
def run() -> None:
|
|
122
|
+
console = Console()
|
|
123
|
+
interrupt_event = threading.Event()
|
|
124
|
+
|
|
125
|
+
if not configs.build_config.output_dir.exists():
|
|
126
|
+
console.print("[bold red]Build directory not found.[/bold red]")
|
|
127
|
+
console.print("Please run 'pytempl-cli build' first.")
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
server = tcp_server(
|
|
132
|
+
console=console,
|
|
133
|
+
host=configs.run_config.host,
|
|
134
|
+
port=configs.run_config.port,
|
|
135
|
+
directory_path=configs.build_config.output_dir,
|
|
136
|
+
)
|
|
137
|
+
except OSError:
|
|
138
|
+
console.print(
|
|
139
|
+
f"[bold red]Error: Could not bind to address http://{configs.run_config.host if configs.run_config.host is not None else '127.0.0.1'}:{configs.run_config.port}.[/bold red]"
|
|
140
|
+
)
|
|
141
|
+
console.print(f"Is port {configs.run_config.port} already in use?")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
def shutdown_handler(signum, _frame):
|
|
145
|
+
"""Gracefully shut down the server."""
|
|
146
|
+
signal_name = signal.Signals(signum).name
|
|
147
|
+
console.print(
|
|
148
|
+
f"\n[bold yellow]Received {signal_name}. Shutting down server...[/bold yellow]"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
server.shutdown()
|
|
152
|
+
interrupt_event.set()
|
|
153
|
+
|
|
154
|
+
# original_sigint = signal.getsignal(signal.SIGINT)
|
|
155
|
+
# original_sigterm = signal.getsignal(signal.SIGTERM)
|
|
156
|
+
|
|
157
|
+
signal.signal(signal.SIGINT, shutdown_handler)
|
|
158
|
+
signal.signal(signal.SIGTERM, shutdown_handler)
|
|
159
|
+
|
|
160
|
+
with Live(
|
|
161
|
+
Spinner("dots", text=" Starting server..."), console=console, transient=True
|
|
162
|
+
):
|
|
163
|
+
server_thread = threading.Thread(target=server.serve_forever)
|
|
164
|
+
server_thread.daemon = True
|
|
165
|
+
server_thread.start()
|
|
166
|
+
|
|
167
|
+
info_table = Table.grid(padding=(0, 1))
|
|
168
|
+
info_table.add_column()
|
|
169
|
+
info_table.add_column()
|
|
170
|
+
|
|
171
|
+
info_table.add_row("✓", "[bold green]Aether App[/bold green] is running!")
|
|
172
|
+
info_table.add_row(
|
|
173
|
+
" ",
|
|
174
|
+
f"[cyan]Local:[/cyan] http://{configs.run_config.host if configs.run_config.host is not None else '127.0.0.1'}:{configs.run_config.port}",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
local_ip = get_local_ip()
|
|
178
|
+
if local_ip:
|
|
179
|
+
info_table.add_row(
|
|
180
|
+
" ",
|
|
181
|
+
f"[cyan]Network:[/cyan] http://{local_ip}:{configs.run_config.port}",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
console.print(info_table)
|
|
185
|
+
console.print("\nPress CTRL+C to stop the server.\n")
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
interrupt_event.wait()
|
|
189
|
+
finally:
|
|
190
|
+
# --- Cleanup: Restore original signal handlers ---
|
|
191
|
+
console.print("[bold red]Server has been shut down.[/bold red]")
|
|
192
|
+
signal.signal(signal.SIGINT, signal.getsignal(signal.SIGINT))
|
|
193
|
+
signal.signal(signal.SIGTERM, signal.getsignal(signal.SIGTERM))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == "__main__":
|
|
197
|
+
main()
|
aether_cli/configs.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Self
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, model_validator
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import tomllib # Python 3.11+
|
|
8
|
+
except ImportError:
|
|
9
|
+
import tomli as tomllib # type: ignore[import]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BuildConfig(BaseModel):
|
|
13
|
+
index_page_file_target: Path
|
|
14
|
+
index_page_function_target: str
|
|
15
|
+
|
|
16
|
+
output_dir: Path
|
|
17
|
+
|
|
18
|
+
pages_file_targets: list[Path]
|
|
19
|
+
pages_function_targets: list[str]
|
|
20
|
+
pages_names: list[str]
|
|
21
|
+
|
|
22
|
+
@model_validator(mode="after")
|
|
23
|
+
def validate_attrs(self) -> Self:
|
|
24
|
+
if len(self.pages_file_targets) != len(self.pages_function_targets):
|
|
25
|
+
raise ValueError(
|
|
26
|
+
"Each 'pages_file_target' must have a corresponding 'pages_function_target'."
|
|
27
|
+
)
|
|
28
|
+
if len(self.pages_file_targets) != len(self.pages_names):
|
|
29
|
+
raise ValueError(
|
|
30
|
+
"Each 'pages_file_target' must have a corresponding 'pages_name'."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RunConfig(BaseModel):
|
|
37
|
+
host: str | None
|
|
38
|
+
port: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class StaticContentConfig(BaseModel):
|
|
42
|
+
assets_dir: Path | None
|
|
43
|
+
js_scripts_dir: Path | None
|
|
44
|
+
public_dir: Path | None
|
|
45
|
+
styles_dir: Path | None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Config(BaseModel):
|
|
49
|
+
build_config: BuildConfig
|
|
50
|
+
run_config: RunConfig
|
|
51
|
+
static_content_config: StaticContentConfig
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_configs() -> Config:
|
|
55
|
+
pyproject_file = Path("pyproject.toml")
|
|
56
|
+
if not pyproject_file.exists():
|
|
57
|
+
raise FileNotFoundError("Unable to locate configuration file.")
|
|
58
|
+
|
|
59
|
+
with open(pyproject_file, "rb") as file:
|
|
60
|
+
raw_aether_config: dict = tomllib.load(file).get("tool", {}).get("aether", {})
|
|
61
|
+
|
|
62
|
+
raw_aether_build_config: dict = raw_aether_config.get("build", {})
|
|
63
|
+
raw_aether_run_config: dict = raw_aether_config.get("run", {})
|
|
64
|
+
raw_aether_static_content_config: dict = raw_aether_config.get(
|
|
65
|
+
"static_content", {}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Decompose raw 'build' configurations.
|
|
69
|
+
# TODO: Add fallbacks/defaults if the lengths of file_targets and function_targets are different
|
|
70
|
+
parsed_build_config = BuildConfig(
|
|
71
|
+
index_page_file_target=raw_aether_build_config.get("index_page", {}).get(
|
|
72
|
+
"file_target", "main.py"
|
|
73
|
+
),
|
|
74
|
+
index_page_function_target=raw_aether_build_config.get("index_page", {}).get(
|
|
75
|
+
"function_target", "main"
|
|
76
|
+
),
|
|
77
|
+
output_dir=raw_aether_build_config.get("output_dir", "build/"),
|
|
78
|
+
pages_file_targets=raw_aether_build_config.get("pages", {}).get(
|
|
79
|
+
"file_targets", []
|
|
80
|
+
),
|
|
81
|
+
pages_function_targets=raw_aether_build_config.get("pages", {}).get(
|
|
82
|
+
"function_targets", []
|
|
83
|
+
),
|
|
84
|
+
pages_names=raw_aether_build_config.get("pages", {}).get("names", []),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Decompose raw 'run' configurations.
|
|
88
|
+
parsed_run_config = RunConfig(
|
|
89
|
+
host=raw_aether_run_config.get("host", None),
|
|
90
|
+
port=raw_aether_run_config.get("port", 8080),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Decompose raw 'static content' configurations.
|
|
94
|
+
parsed_static_content_config = StaticContentConfig(
|
|
95
|
+
assets_dir=raw_aether_static_content_config.get("assets_dir"),
|
|
96
|
+
js_scripts_dir=raw_aether_static_content_config.get("js_scripts_dir"),
|
|
97
|
+
public_dir=raw_aether_static_content_config.get("public_dir"),
|
|
98
|
+
styles_dir=raw_aether_static_content_config.get("styles_dir"),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return Config(
|
|
102
|
+
build_config=parsed_build_config,
|
|
103
|
+
run_config=parsed_run_config,
|
|
104
|
+
static_content_config=parsed_static_content_config,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
configs = load_configs()
|
aether_cli/run_server.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import http.server
|
|
2
|
+
import socketserver
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def request_handler(console: Console, directory_path: str):
|
|
10
|
+
class RichHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|
11
|
+
def __init__(self, *args, **kwargs):
|
|
12
|
+
super().__init__(*args, directory=directory_path, **kwargs)
|
|
13
|
+
|
|
14
|
+
def log_message(self, format, *args):
|
|
15
|
+
status_code = args[1]
|
|
16
|
+
request_line = args[0]
|
|
17
|
+
|
|
18
|
+
if status_code.startswith("2"):
|
|
19
|
+
status_color = "green"
|
|
20
|
+
elif status_code.startswith("3"):
|
|
21
|
+
status_color = "yellow"
|
|
22
|
+
else:
|
|
23
|
+
status_color = "red"
|
|
24
|
+
|
|
25
|
+
timestamp = f"[[dim]{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}[/dim]]"
|
|
26
|
+
|
|
27
|
+
console.print(
|
|
28
|
+
f"{timestamp} INFO:\t"
|
|
29
|
+
f'{self.address_string()} - "{request_line}" '
|
|
30
|
+
f"[{status_color}]{status_code}[/{status_color}]"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return RichHTTPRequestHandler
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def tcp_server(
|
|
37
|
+
console: Console, host: str | None, port: int, directory_path: Path
|
|
38
|
+
) -> socketserver.TCPServer:
|
|
39
|
+
handler_factory = request_handler(console, str(directory_path))
|
|
40
|
+
|
|
41
|
+
if host is None:
|
|
42
|
+
host = ""
|
|
43
|
+
|
|
44
|
+
return socketserver.TCPServer((host, port), handler_factory)
|
aether_cli/utils.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import importlib.util
|
|
3
|
+
import socket
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ImportFromStringError(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_build_function_instance(file_target: str, function_target: str) -> Any:
|
|
14
|
+
working_dir = Path.cwd()
|
|
15
|
+
working_file = Path(file_target)
|
|
16
|
+
|
|
17
|
+
if not working_file.exists():
|
|
18
|
+
raise FileNotFoundError(f"File '{file_target}' not found.")
|
|
19
|
+
|
|
20
|
+
module_path = working_dir / file_target
|
|
21
|
+
module_name = Path(file_target).stem
|
|
22
|
+
|
|
23
|
+
sys.path.append(str(working_dir))
|
|
24
|
+
|
|
25
|
+
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
|
26
|
+
if spec and spec.loader:
|
|
27
|
+
module = importlib.util.module_from_spec(spec)
|
|
28
|
+
spec.loader.exec_module(module)
|
|
29
|
+
else:
|
|
30
|
+
raise ImportFromStringError(
|
|
31
|
+
f"Could not import module from path '{module_path}'"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
instance = getattr(module, function_target)
|
|
36
|
+
except AttributeError:
|
|
37
|
+
message = f'Attribute "{function_target}" not found in module "{module_name}".'
|
|
38
|
+
raise ImportFromStringError(message)
|
|
39
|
+
|
|
40
|
+
return instance
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_local_ip():
|
|
44
|
+
"""Tries to find the local network IP address of the machine."""
|
|
45
|
+
try:
|
|
46
|
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
|
47
|
+
s.connect(("8.8.8.8", 80))
|
|
48
|
+
return s.getsockname()[0]
|
|
49
|
+
except Exception:
|
|
50
|
+
return None
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyaether-cli
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: A CLI to build and run dev server for Aether apps.
|
|
5
|
+
Project-URL: homepage, https://github.com/pyaether/aether-cli
|
|
6
|
+
Project-URL: repository, https://github.com/pyaether/aether-cli
|
|
7
|
+
Project-URL: documentation, https://github.com/pyaether/aether-cli
|
|
8
|
+
Author-email: Saurabh Ghanekar <ghanekarsaurabh8@gmail.com>
|
|
9
|
+
License-Expression: BSD-2-Clause
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: beautifulsoup4>=4.13
|
|
14
|
+
Requires-Dist: click>=8.1
|
|
15
|
+
Requires-Dist: pyaether
|
|
16
|
+
Requires-Dist: pydantic>=2.10
|
|
17
|
+
Requires-Dist: rich>=13.9
|
|
18
|
+
Requires-Dist: typing-extensions>=4
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Aether CLI
|
|
22
|
+
|
|
23
|
+
Build and run your Aether apps as well as manage your Altar UI components from the command line.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
aether_cli/__init__.py,sha256=4GZKi13lDTD25YBkGakhZyEQZWTER_OWQMNPoH_UM2c,22
|
|
2
|
+
aether_cli/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
|
|
3
|
+
aether_cli/build_process.py,sha256=4Dl-rM0O7RN2QzL0FHhBTxENSuv0yts-Q58sNkZWRQc,3874
|
|
4
|
+
aether_cli/cli.py,sha256=H06Yu21EgkK-nHYfORC-LtLZXUTA2DVGdm5wDG9Pxuw,6377
|
|
5
|
+
aether_cli/configs.py,sha256=5wSRoDEfvR-HxCfQoLB0-FhQJVnHVPf9vI0Y_6iEwUc,3506
|
|
6
|
+
aether_cli/run_server.py,sha256=19700YKa6WPancZLIkvgpp4-dhECHtc4IEiwIRjXMto,1346
|
|
7
|
+
aether_cli/utils.py,sha256=xKUvgSxp8oNddaTC7mlra_LDBqyUrOCiW7nFKiarJ-Y,1393
|
|
8
|
+
pyaether_cli-0.0.3.dist-info/METADATA,sha256=Q-LOLSEw2H5Ll2gJxQeCykcxioqe6kKpev-UvyPYSmM,850
|
|
9
|
+
pyaether_cli-0.0.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
+
pyaether_cli-0.0.3.dist-info/entry_points.txt,sha256=zbcg3c2Ry_eTiyEbTMGMAGnl_MpbS68oZ1q-Jm6rp-M,47
|
|
11
|
+
pyaether_cli-0.0.3.dist-info/RECORD,,
|