π¦ release: crates.io publish prep β metadata, versions on path-deps, bin renamed to justapi-cli
swadhinbiswas committed 2 months ago
f47a45e
1 parent 74e99fc
commit f47a45e
5 files changed
+170 -18
Showing 5 changed files+170-18
βΌCargo.toml
+2-0
| @@ -13,6 +13,8 @@ exclude = ["fuzz"] | ||
13 | 13 | version = "2.0.9" |
14 | 14 | edition = "2021" |
15 | 15 | license = "MIT" |
16 | +description = "JustAPI β Python web framework with a Rust core. Networking, TLS, routing, validation, serialization run in Rust." | |
17 | +repository = "https://github.com/swadhinbiswas/JustAPI" | |
16 | 18 | |
17 | 19 | # Release builds abort on panic instead of unwinding. This is required for the |
18 | 20 | # pyo3 extension-module FFI boundary: a Rust panic that unwinds across the |
19 | 21 | |
βΌcrates/justapi-cli/Cargo.toml
+3-2
| @@ -1,11 +1,12 @@ | ||
1 | 1 | [package] |
2 | 2 | name = "justapi-cli" |
3 | +description = "JustAPI CLI β serve, create, check, profile, openapi, hot reload. High-performance Rust CLI for the JustAPI framework." | |
3 | 4 | version.workspace = true |
4 | 5 | edition.workspace = true |
5 | 6 | license.workspace = true |
6 | 7 | |
7 | 8 | [[bin]] |
8 | -name = "justapi" | |
9 | +name = "justapi-cli" | |
9 | 10 | path = "src/main.rs" |
10 | 11 | |
11 | 12 | [features] |
| @@ -17,7 +18,7 @@ inference = ["justapi-core/inference", "dep:justapi-inference"] | ||
17 | 18 | |
18 | 19 | [dependencies] |
19 | 20 | justapi-core = { workspace = true, features = ["db"] } |
20 | -justapi-inference = { path = "../justapi-inference", optional = true } | |
21 | +justapi-inference = { version = "2.0.9", path = "../justapi-inference", optional = true } | |
21 | 22 | tokio.workspace = true |
22 | 23 | clap.workspace = true |
23 | 24 | tracing.workspace = true |
24 | 25 | |
βΌcrates/justapi-core/Cargo.toml
+2-1
| @@ -1,5 +1,6 @@ | ||
1 | 1 | [package] |
2 | 2 | name = "justapi-core" |
3 | +description = "JustAPI core β tokio/hyper networking, matchit router, middleware, validation, serialization, sqlx DB pool." | |
3 | 4 | version.workspace = true |
4 | 5 | edition.workspace = true |
5 | 6 | license.workspace = true |
| @@ -75,7 +76,7 @@ redis = { version = "1.3.0", features = ["tokio-comp", "connection-manager"], op | ||
75 | 76 | notify = "8.2.0" |
76 | 77 | arc-swap = "1.9.2" |
77 | 78 | async-graphql = { version = "7.2.1", optional = true } |
78 | -justapi-inference = { path = "../justapi-inference", optional = true } | |
79 | +justapi-inference = { version = "2.0.9", path = "../justapi-inference", optional = true } | |
79 | 80 | tempfile = "3.27.0" |
80 | 81 | lettre = { workspace = true, optional = true } |
81 | 82 | minijinja = { workspace = true, optional = true } |
82 | 83 | |
βΌcrates/justapi-inference/Cargo.toml
+1-0
| @@ -1,5 +1,6 @@ | ||
1 | 1 | [package] |
2 | 2 | name = "justapi-inference" |
3 | +description = "JustAPI inference engine β Candle-based LLM serving (experimental, optional feature)." | |
3 | 4 | version.workspace = true |
4 | 5 | edition.workspace = true |
5 | 6 | license.workspace = true |
6 | 7 | |
βΌcrates/justapi-py/python/justapi/cli.py
+162-15
| @@ -1,24 +1,171 @@ | ||
1 | -import sys | |
1 | +"""JustAPI CLI β Python package entry point. | |
2 | + | |
3 | +Provides the core commands standalone (no Rust binary required) and | |
4 | +transparently delegates to the high-performance Rust CLI (`justapi-cli`) | |
5 | +when it is installed via `cargo install justapi-cli`. | |
6 | + | |
7 | +Commands: | |
8 | + justapi --version Show version | |
9 | + justapi serve [addr] Run the app in the current directory (main.py / app.py) | |
10 | + justapi create NAME Scaffold a new project | |
11 | + justapi check [file] Validate routes without starting the server | |
12 | + justapi openapi [file] Print the OpenAPI spec | |
13 | +""" | |
14 | + | |
15 | +import argparse | |
16 | +import importlib.util | |
17 | +import os | |
2 | 18 | import subprocess |
19 | +import sys | |
3 | 20 | import shutil |
4 | 21 | |
5 | -def main(): | |
6 | - """JustAPI CLI entrypoint for python package.""" | |
7 | - binary = shutil.which("justapi-cli") | |
8 | - if binary: | |
9 | - sys.exit(subprocess.call([binary] + sys.argv[1:])) | |
22 | +__version__ = "2.0.9" | |
23 | + | |
24 | +RUST_BINARY = "justapi-cli" # cargo install justapi-cli β binary name | |
25 | + | |
26 | + | |
27 | +def _rust_cli() -> str | None: | |
28 | + """Return the path to the Rust CLI if installed, else None.""" | |
29 | + return shutil.which(RUST_BINARY) | |
30 | + | |
31 | + | |
32 | +def _delegate_or(args: list[str]) -> bool: | |
33 | + """If the Rust CLI is installed, exec it with args. Returns True if delegated.""" | |
34 | + rust = _rust_cli() | |
35 | + if rust: | |
36 | + sys.exit(subprocess.call([rust] + args)) | |
37 | + return False | |
38 | + | |
39 | + | |
40 | +def _find_app_file() -> str | None: | |
41 | + """Locate the user's app module: app.py, main.py, or src/app.py.""" | |
42 | + for name in ("app.py", "main.py", "src/app.py"): | |
43 | + if os.path.isfile(name): | |
44 | + return name | |
45 | + return None | |
46 | + | |
47 | + | |
48 | +def cmd_version(args: argparse.Namespace) -> None: | |
49 | + rust = _rust_cli() | |
50 | + print(f"justapi {__version__} (Python package)") | |
51 | + if rust: | |
52 | + print(f"Rust CLI detected: {rust}") | |
53 | + print(f"Python {sys.version.split()[0]} Β· PyPI package") | |
54 | + | |
55 | + | |
56 | +def cmd_serve(args: argparse.Namespace) -> None: | |
57 | + """Run the app in the current directory.""" | |
58 | + if _delegate_or([f"--addr={args.addr}"]): | |
59 | + return | |
60 | + app_file = _find_app_file() | |
61 | + if app_file is None: | |
62 | + print("error: no app.py or main.py found in the current directory", file=sys.stderr) | |
63 | + sys.exit(1) | |
64 | + print(f"justapi {__version__} β serving {app_file} on {args.addr}") | |
65 | + # Import the app module β its `app.run(...)` (or `if __name__`) starts the server. | |
66 | + module_name = os.path.splitext(app_file)[0].replace("/", ".") | |
67 | + spec = importlib.util.spec_from_file_location(module_name, app_file) | |
68 | + assert spec and spec.loader | |
69 | + module = importlib.util.module_from_spec(spec) | |
70 | + sys.argv = [app_file, args.addr] | |
71 | + spec.loader.exec_module(module) | |
72 | + # If the module only defines `app` without calling run(), call it for the user. | |
73 | + app = getattr(module, "app", None) | |
74 | + if app is not None and not getattr(module, "_justapi_ran", False): | |
75 | + app.run(args.addr) | |
76 | + | |
10 | 77 | |
11 | - if len(sys.argv) > 1 and sys.argv[1] in ("--version", "-V"): | |
12 | - print("justapi 2.0.0") | |
78 | +def cmd_create(args: argparse.Namespace) -> None: | |
79 | + """Scaffold a new project. Delegates to Rust CLI when available.""" | |
80 | + if _delegate_or(["create", args.name] + args.rest): | |
13 | 81 | return |
82 | + print("error: `justapi create` requires the Rust CLI:", file=sys.stderr) | |
83 | + print(" cargo install justapi-cli", file=sys.stderr) | |
84 | + sys.exit(1) | |
85 | + | |
86 | + | |
87 | +def cmd_check(args: argparse.Namespace) -> None: | |
88 | + """Validate the app's routes without starting the server.""" | |
89 | + if _delegate_or(["check"] + ([args.file] if args.file else [])): | |
90 | + return | |
91 | + app_file = args.file or _find_app_file() | |
92 | + if app_file is None: | |
93 | + print("error: no app.py or main.py found", file=sys.stderr) | |
94 | + sys.exit(1) | |
95 | + spec = importlib.util.spec_from_file_location("app", app_file) | |
96 | + assert spec and spec.loader | |
97 | + module = importlib.util.module_from_spec(spec) | |
98 | + spec.loader.exec_module(module) | |
99 | + app = getattr(module, "app", None) | |
100 | + if app is None: | |
101 | + print(f"error: {app_file} does not define `app`", file=sys.stderr) | |
102 | + sys.exit(1) | |
103 | + print(f"β {app_file}: app loaded, routes registered") | |
104 | + | |
105 | + | |
106 | +def cmd_openapi(args: argparse.Namespace) -> None: | |
107 | + """Print the OpenAPI spec for the app.""" | |
108 | + if _delegate_or(["openapi"] + ([args.file] if args.file else [])): | |
109 | + return | |
110 | + app_file = args.file or _find_app_file() | |
111 | + if app_file is None: | |
112 | + print("error: no app.py or main.py found", file=sys.stderr) | |
113 | + sys.exit(1) | |
114 | + spec = importlib.util.spec_from_file_location("app", app_file) | |
115 | + assert spec and spec.loader | |
116 | + module = importlib.util.module_from_spec(spec) | |
117 | + spec.loader.exec_module(module) | |
118 | + app = getattr(module, "app", None) | |
119 | + if app is None: | |
120 | + print(f"error: {app_file} does not define `app`", file=sys.stderr) | |
121 | + sys.exit(1) | |
122 | + import json | |
123 | + spec_dict = app.openapi_spec() if hasattr(app, "openapi_spec") else {} | |
124 | + print(json.dumps(spec_dict, indent=2)) | |
125 | + | |
126 | + | |
127 | +def main(argv: list[str] | None = None) -> None: | |
128 | + argv = argv if argv is not None else sys.argv[1:] | |
129 | + parser = argparse.ArgumentParser( | |
130 | + prog="justapi", | |
131 | + description="JustAPI β Python web framework with a Rust core.", | |
132 | + ) | |
133 | + parser.add_argument("--version", "-V", action="store_true", help="Show version") | |
134 | + sub = parser.add_subparsers(dest="command") | |
135 | + | |
136 | + p_serve = sub.add_parser("serve", help="Run the app in the current directory") | |
137 | + p_serve.add_argument("addr", nargs="?", default="127.0.0.1:8000", help="Address to bind (default 127.0.0.1:8000)") | |
138 | + | |
139 | + p_create = sub.add_parser("create", help="Scaffold a new project (requires Rust CLI)") | |
140 | + p_create.add_argument("name", help="Project name") | |
141 | + p_create.add_argument("rest", nargs=argparse.REMAINDER, help="Extra args passed to the Rust CLI") | |
142 | + | |
143 | + p_check = sub.add_parser("check", help="Validate the app without starting the server") | |
144 | + p_check.add_argument("file", nargs="?", help="App file (default: auto-detect)") | |
145 | + | |
146 | + p_openapi = sub.add_parser("openapi", help="Print the OpenAPI spec") | |
147 | + p_openapi.add_argument("file", nargs="?", help="App file (default: auto-detect)") | |
148 | + | |
149 | + args = parser.parse_args(argv) | |
150 | + | |
151 | + if args.version: | |
152 | + cmd_version(args) | |
153 | + return | |
154 | + | |
155 | + if args.command == "serve": | |
156 | + cmd_serve(args) | |
157 | + elif args.command == "create": | |
158 | + cmd_create(args) | |
159 | + elif args.command == "check": | |
160 | + cmd_check(args) | |
161 | + elif args.command == "openapi": | |
162 | + cmd_openapi(args) | |
163 | + else: | |
164 | + # No command β show help / quick start | |
165 | + if _delegate_or([]): | |
166 | + return | |
167 | + parser.print_help() | |
14 | 168 | |
15 | - print("JustAPI Runtime v2.0.0 (Python package)") | |
16 | - print("Commands:") | |
17 | - print(" justapi --version Display version") | |
18 | - print(" python -m app.main Run application directly") | |
19 | - print("\nFor high-performance Rust CLI features (justapi create, serve --reload, profile),") | |
20 | - print("install justapi-cli via cargo: cargo install justapi-cli") | |
21 | 169 | |
22 | 170 | if __name__ == "__main__": |
23 | 171 | main() |
24 | - | |
25 | 172 | |