cuneus 0.2.1__py3-none-any.whl → 0.2.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.
cuneus/cli/__init__.py DELETED
@@ -1,394 +0,0 @@
1
- """
2
- CLI tools for application management.
3
-
4
- Reads configuration from pyproject.toml:
5
-
6
- [tool.qtip]
7
- app_name = "myapp"
8
- app_module = "myapp.main:application"
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import importlib
14
- import sys
15
- from pathlib import Path
16
-
17
- import click
18
-
19
- from cuneus.core.application import load_pyproject_config, DEFAULT_TOOL_NAME
20
- from cuneus.cli.console import (
21
- info,
22
- success,
23
- warning,
24
- error,
25
- fatal,
26
- dim,
27
- header,
28
- step,
29
- hint_for_error,
30
- key_value,
31
- confirm,
32
- table,
33
- )
34
-
35
-
36
- def get_config() -> dict:
37
- """Load configuration from pyproject.toml."""
38
- config = load_pyproject_config()
39
- if not config:
40
- hint_for_error("no_pyproject")
41
- sys.exit(1)
42
- return config
43
-
44
-
45
- def get_app():
46
- """Load the Application instance."""
47
- config = get_config()
48
- module_path = config.get("app_module", "app.main:app")
49
-
50
- try:
51
- module_name, attr_name = module_path.rsplit(":", 1)
52
- module = importlib.import_module(module_name)
53
- return getattr(module, attr_name)
54
- except ImportError as e:
55
- hint_for_error("import_error", e)
56
- dim(f" Tried to import: {module_path}")
57
- sys.exit(1)
58
- except AttributeError as e:
59
- hint_for_error("no_app_module", e)
60
- sys.exit(1)
61
-
62
-
63
- @click.group()
64
- @click.option(
65
- "--config", "-c", type=click.Path(exists=True), help="Path to pyproject.toml"
66
- )
67
- @click.option("--verbose", "-v", is_flag=True, help="Verbose output")
68
- @click.pass_context
69
- def cli(ctx: click.Context, config: str | None, verbose: bool) -> None:
70
- """Application management CLI."""
71
- ctx.ensure_object(dict)
72
- ctx.obj["verbose"] = verbose
73
- if config:
74
- ctx.obj["config_path"] = config
75
-
76
-
77
- @cli.command()
78
- @click.option("--host", default="0.0.0.0", help="Bind host")
79
- @click.option("--port", default=8000, type=int, help="Bind port")
80
- @click.option("--workers", default=1, type=int, help="Number of workers")
81
- @click.option("--reload", is_flag=True, help="Enable auto-reload")
82
- @click.pass_context
83
- def run(ctx: click.Context, host: str, port: int, workers: int, reload: bool) -> None:
84
- """Run the application server."""
85
- import uvicorn
86
-
87
- config = get_config()
88
- app_module = config.get("app_module", "app.main:app")
89
-
90
- # Convert Application reference to FastAPI app reference
91
- if ":application" in app_module:
92
- fastapi_module = app_module.replace(":application", ":app")
93
- else:
94
- fastapi_module = app_module
95
-
96
- info(f"Starting {config.get('app_name', 'app')}")
97
- dim(f" Module: {fastapi_module}")
98
- dim(f" Address: http://{host}:{port}")
99
-
100
- if reload:
101
- warning("Running in reload mode (not for production)")
102
-
103
- if workers > 1 and reload:
104
- warning("--reload is incompatible with multiple workers, using 1 worker")
105
- workers = 1
106
-
107
- click.echo()
108
-
109
- try:
110
- uvicorn.run(
111
- fastapi_module,
112
- host=host,
113
- port=port,
114
- workers=workers,
115
- reload=reload,
116
- log_level="info",
117
- )
118
- except OSError as e:
119
- if "address already in use" in str(e).lower():
120
- hint_for_error("port_in_use", e)
121
- else:
122
- raise
123
-
124
-
125
- @cli.group()
126
- def db() -> None:
127
- """Database management commands."""
128
- pass
129
-
130
-
131
- @db.command()
132
- @click.option("-m", "--message", required=True, help="Migration message")
133
- def migrate(message: str) -> None:
134
- """Generate a new migration."""
135
- from alembic import command
136
- from alembic.util.exc import CommandError
137
-
138
- info(f"Creating migration: {message}")
139
-
140
- try:
141
- with step("Generating migration"):
142
- alembic_cfg = _get_alembic_config()
143
- command.revision(alembic_cfg, message=message, autogenerate=True)
144
- success(f"Created migration: {message}")
145
- except CommandError as e:
146
- if "Target database is not up to date" in str(e):
147
- error("Database is not up to date")
148
- dim(" hint: Run 'db upgrade' first, then create the migration")
149
- else:
150
- error(f"Migration failed: {e}")
151
- sys.exit(1)
152
-
153
-
154
- @db.command()
155
- @click.option("--revision", default="head", help="Target revision")
156
- def upgrade(revision: str) -> None:
157
- """Upgrade database to a revision."""
158
- from alembic import command
159
- from sqlalchemy.exc import OperationalError
160
-
161
- info(f"Upgrading database to: {revision}")
162
-
163
- try:
164
- with step("Running migrations"):
165
- alembic_cfg = _get_alembic_config()
166
- command.upgrade(alembic_cfg, revision)
167
- success("Database upgraded successfully")
168
- except OperationalError as e:
169
- hint_for_error("database_connection", e)
170
- sys.exit(1)
171
-
172
-
173
- @db.command()
174
- @click.option("--revision", default="-1", help="Target revision")
175
- @click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
176
- def downgrade(revision: str, yes: bool) -> None:
177
- """Downgrade database to a revision."""
178
- from alembic import command
179
-
180
- if not yes:
181
- warning("This will downgrade your database and may cause data loss")
182
- if not confirm("Are you sure you want to continue?"):
183
- info("Aborted")
184
- return
185
-
186
- info(f"Downgrading database to: {revision}")
187
-
188
- with step("Running downgrade"):
189
- alembic_cfg = _get_alembic_config()
190
- command.downgrade(alembic_cfg, revision)
191
-
192
- success("Database downgraded successfully")
193
-
194
-
195
- @db.command()
196
- def history() -> None:
197
- """Show migration history."""
198
- from alembic import command
199
- from io import StringIO
200
-
201
- header("Migration History")
202
- alembic_cfg = _get_alembic_config()
203
- command.history(alembic_cfg, verbose=True)
204
-
205
-
206
- @db.command()
207
- def current() -> None:
208
- """Show current revision."""
209
- from alembic import command
210
-
211
- header("Current Database Revision")
212
- alembic_cfg = _get_alembic_config()
213
- command.current(alembic_cfg, verbose=True)
214
-
215
-
216
- @db.command()
217
- def heads() -> None:
218
- """Show migration heads (useful for detecting conflicts)."""
219
- from alembic import command
220
-
221
- header("Migration Heads")
222
- alembic_cfg = _get_alembic_config()
223
- command.heads(alembic_cfg, verbose=True)
224
-
225
-
226
- @db.command()
227
- def check() -> None:
228
- """Check if database is up to date."""
229
- from alembic.script import ScriptDirectory
230
- from alembic.runtime.migration import MigrationContext
231
- from sqlalchemy import create_engine
232
-
233
- alembic_cfg = _get_alembic_config()
234
-
235
- with step("Checking database status"):
236
- script = ScriptDirectory.from_config(alembic_cfg)
237
- head = script.get_current_head()
238
-
239
- db_url = alembic_cfg.get_main_option("sqlalchemy.url")
240
- if db_url is None:
241
- warning('unable to check "sqlalchemy.url" is missing')
242
- return
243
- engine = create_engine(db_url)
244
-
245
- with engine.connect() as conn:
246
- context = MigrationContext.configure(conn)
247
- current = context.get_current_revision()
248
-
249
- if current == head:
250
- success("Database is up to date")
251
- else:
252
- warning(f"Database is behind: {current} → {head}")
253
- dim(" Run 'db upgrade' to apply pending migrations")
254
-
255
-
256
- def _get_alembic_config():
257
- """Get Alembic config with database URL from settings."""
258
- from alembic.config import Config
259
-
260
- alembic_ini = Path("alembic.ini")
261
- if not alembic_ini.exists():
262
- fatal("alembic.ini not found", hint="Run 'alembic init alembic' to create it")
263
-
264
- alembic_cfg = Config("alembic.ini")
265
-
266
- app = get_app()
267
- if hasattr(app.settings, "database_url"):
268
- db_url = app.settings.database_url
269
- if db_url.startswith("postgresql+asyncpg://"):
270
- db_url = db_url.replace("postgresql+asyncpg://", "postgresql://")
271
- alembic_cfg.set_main_option("sqlalchemy.url", db_url)
272
- else:
273
- fatal(
274
- "No database_url in settings",
275
- hint="Add database_url to your Settings class",
276
- )
277
-
278
- return alembic_cfg
279
-
280
-
281
- @cli.command()
282
- def shell() -> None:
283
- """Start an interactive shell with app context."""
284
- import asyncio
285
- import code
286
-
287
- app = get_app()
288
-
289
- info(f"Starting {app.settings.app_name} shell")
290
-
291
- with step("Building application"):
292
- fastapi_app = app.build()
293
-
294
- with step("Running startup hooks"):
295
-
296
- async def setup():
297
- async with asyncio.timeout(30):
298
- ctx = fastapi_app.router.lifespan_context(fastapi_app)
299
- await ctx.__aenter__()
300
- return ctx
301
-
302
- ctx = asyncio.run(setup())
303
-
304
- success("Shell ready")
305
- click.echo()
306
-
307
- banner = f"""{click.style(app.settings.app_name, fg='cyan', bold=True)} Interactive Shell
308
-
309
- Available objects:
310
- {click.style('app', fg='green')} Application instance
311
- {click.style('fastapi_app', fg='green')} FastAPI instance
312
- {click.style('settings', fg='green')} Application settings
313
- {click.style('state', fg='green')} app.state (db_engine, redis, etc.)
314
- """
315
-
316
- local_vars = {
317
- "app": app,
318
- "fastapi_app": fastapi_app,
319
- "settings": app.settings,
320
- "state": fastapi_app.state,
321
- }
322
-
323
- code.interact(banner=banner, local=local_vars)
324
-
325
-
326
- @cli.command("config")
327
- def show_config() -> None:
328
- """Show current configuration."""
329
- config = get_config()
330
-
331
- header("Configuration")
332
- key_value(config)
333
-
334
- # Show where config was loaded from
335
- click.echo()
336
- dim(f" Source: pyproject.toml [tool.{DEFAULT_TOOL_NAME}]")
337
-
338
-
339
- @cli.command()
340
- def health() -> None:
341
- """Verify configuration and connectivity."""
342
- import asyncio
343
-
344
- header("Configuration Check")
345
-
346
- # Check config loads
347
- with step("Loading configuration"):
348
- config = get_config()
349
-
350
- # Check app loads
351
- with step("Loading application"):
352
- app = get_app()
353
-
354
- with step("Building FastAPI app"):
355
- fastapi_app = app.build()
356
-
357
- # Check connections
358
- header("Connectivity Check")
359
-
360
- async def check_connections():
361
- async with asyncio.timeout(10):
362
- ctx = fastapi_app.router.lifespan_context(fastapi_app)
363
- await ctx.__aenter__()
364
-
365
- # Check database
366
- if hasattr(fastapi_app.state, "db_engine"):
367
- with step("Database connection"):
368
- async with fastapi_app.state.db_engine.connect() as conn:
369
- await conn.execute("SELECT 1")
370
-
371
- # Check Redis
372
- if hasattr(fastapi_app.state, "redis") and fastapi_app.state.redis:
373
- with step("Redis connection"):
374
- await fastapi_app.state.redis.ping()
375
-
376
- await ctx.__aexit__(None, None, None)
377
-
378
- try:
379
- asyncio.run(check_connections())
380
- click.echo()
381
- success("All checks passed!")
382
- except Exception as e:
383
- click.echo()
384
- error(f"Check failed: {e}")
385
- sys.exit(1)
386
-
387
-
388
- def main() -> None:
389
- """Main entry point."""
390
- cli()
391
-
392
-
393
- if __name__ == "__main__":
394
- main()
cuneus/cli/console.py DELETED
@@ -1,208 +0,0 @@
1
- """
2
- Console output utilities for helpful CLI messaging.
3
- """
4
-
5
- from __future__ import annotations
6
-
7
- import sys
8
- from contextlib import contextmanager
9
- from typing import NoReturn
10
-
11
- import click
12
-
13
-
14
- # === Styled Output ===
15
-
16
-
17
- def info(message: str) -> None:
18
- """Print an info message."""
19
- click.echo(click.style("ℹ ", fg="blue") + message)
20
-
21
-
22
- def success(message: str) -> None:
23
- """Print a success message."""
24
- click.echo(click.style("✓ ", fg="green") + message)
25
-
26
-
27
- def warning(message: str) -> None:
28
- """Print a warning message."""
29
- click.echo(click.style("⚠ ", fg="yellow") + message, err=True)
30
-
31
-
32
- def error(message: str) -> None:
33
- """Print an error message."""
34
- click.echo(click.style("✗ ", fg="red") + message, err=True)
35
-
36
-
37
- def fatal(message: str, hint: str | None = None) -> NoReturn:
38
- """Print an error and exit."""
39
- error(message)
40
- if hint:
41
- click.echo(click.style(" hint: ", fg="cyan") + hint, err=True)
42
- sys.exit(1)
43
-
44
-
45
- def dim(message: str) -> None:
46
- """Print dimmed/secondary text."""
47
- click.echo(click.style(message, dim=True))
48
-
49
-
50
- def header(title: str) -> None:
51
- """Print a section header."""
52
- click.echo()
53
- click.echo(
54
- click.style(f"── {title} ", fg="cyan", bold=True)
55
- + click.style("─" * 40, dim=True)
56
- )
57
-
58
-
59
- # === Progress Indicators ===
60
-
61
-
62
- @contextmanager
63
- def spinner(message: str):
64
- """
65
- Simple spinner for long operations.
66
-
67
- Usage:
68
- with spinner("Connecting to database..."):
69
- await db.connect()
70
- """
71
- click.echo(click.style("◐ ", fg="blue") + message, nl=False)
72
- try:
73
- yield
74
- click.echo(click.style(" ✓", fg="green"))
75
- except Exception:
76
- click.echo(click.style(" ✗", fg="red"))
77
- raise
78
-
79
-
80
- @contextmanager
81
- def step(message: str):
82
- """
83
- Step indicator with pass/fail.
84
-
85
- Usage:
86
- with step("Running migrations"):
87
- run_migrations()
88
- """
89
- click.echo(click.style("→ ", fg="blue") + message + "... ", nl=False)
90
- try:
91
- yield
92
- click.echo(click.style("done", fg="green"))
93
- except Exception as e:
94
- click.echo(click.style("failed", fg="red"))
95
- raise
96
-
97
-
98
- # === Helpful Error Messages ===
99
-
100
- ERROR_HINTS = {
101
- "no_pyproject": (
102
- "Could not find pyproject.toml",
103
- "Make sure you're running from your project root, or use --config to specify the path",
104
- ),
105
- "no_app_module": (
106
- "Could not load application module",
107
- "Check that [tool.qtip].app_module points to your Application instance",
108
- ),
109
- "no_database_url": (
110
- "DATABASE_URL is not configured",
111
- "Set it in pyproject.toml, .env, or as an environment variable",
112
- ),
113
- "database_connection": (
114
- "Could not connect to database",
115
- "Check that your database is running and DATABASE_URL is correct",
116
- ),
117
- "redis_connection": (
118
- "Could not connect to Redis",
119
- "Check that Redis is running and REDIS_URL is correct",
120
- ),
121
- "migration_conflict": (
122
- "Migration conflict detected",
123
- "You may have multiple heads. Run 'db heads' to see them, then 'db merge' to resolve",
124
- ),
125
- "port_in_use": (
126
- "Port is already in use",
127
- "Either stop the other process or use --port to specify a different port",
128
- ),
129
- "import_error": (
130
- "Could not import module",
131
- "Check your PYTHONPATH and that all dependencies are installed",
132
- ),
133
- }
134
-
135
-
136
- def hint_for_error(error_key: str, exception: Exception | None = None) -> None:
137
- """Print a helpful error message with hint."""
138
- if error_key in ERROR_HINTS:
139
- msg, hint = ERROR_HINTS[error_key]
140
- error(msg)
141
- if exception:
142
- dim(f" {type(exception).__name__}: {exception}")
143
- click.echo(click.style(" hint: ", fg="cyan") + hint, err=True)
144
- elif exception:
145
- error(str(exception))
146
-
147
-
148
- # === Tables and Structured Output ===
149
-
150
-
151
- def table(headers: list[str], rows: list[list[str]]) -> None:
152
- """Print a simple table."""
153
- if not rows:
154
- dim(" (no data)")
155
- return
156
-
157
- # Calculate column widths
158
- widths = [len(h) for h in headers]
159
- for row in rows:
160
- for i, cell in enumerate(row):
161
- widths[i] = max(widths[i], len(str(cell)))
162
-
163
- # Print header
164
- header_row = " ".join(
165
- click.style(h.ljust(widths[i]), bold=True) for i, h in enumerate(headers)
166
- )
167
- click.echo(header_row)
168
- click.echo(click.style("─" * (sum(widths) + 2 * (len(headers) - 1)), dim=True))
169
-
170
- # Print rows
171
- for row in rows:
172
- click.echo(" ".join(str(cell).ljust(widths[i]) for i, cell in enumerate(row)))
173
-
174
-
175
- def key_value(data: dict[str, str], title: str | None = None) -> None:
176
- """Print key-value pairs."""
177
- if title:
178
- header(title)
179
-
180
- max_key_len = max(len(k) for k in data.keys()) if data else 0
181
-
182
- for key, value in data.items():
183
- key_str = click.style(key.ljust(max_key_len), fg="cyan")
184
- # Mask secrets
185
- if (
186
- "secret" in key.lower()
187
- or "password" in key.lower()
188
- or "token" in key.lower()
189
- ):
190
- value = "***"
191
- click.echo(f" {key_str} {value}")
192
-
193
-
194
- # === Confirmations ===
195
-
196
-
197
- def confirm(message: str, default: bool = False) -> bool:
198
- """Ask for confirmation."""
199
- return click.confirm(click.style("? ", fg="yellow") + message, default=default)
200
-
201
-
202
- def prompt(message: str, default: str | None = None, hide_input: bool = False) -> str:
203
- """Prompt for input."""
204
- return click.prompt(
205
- click.style("? ", fg="yellow") + message,
206
- default=default,
207
- hide_input=hide_input,
208
- )
File without changes
@@ -1,15 +0,0 @@
1
- cuneus/__init__.py,sha256=KB5M0Ll8iffKml3-DmI2_LdafkU8Jhw3BBJkH8MrbpI,1226
2
- cuneus/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- cuneus/cli/__init__.py,sha256=XlLdZYfjfQxkIi4QTe8NcinmkidX__kd4ryO-Fjwxig,10854
4
- cuneus/cli/console.py,sha256=e8ElKzkhL4FQQeVE-pjZx-kLgbzAKQrZJJpss3F4vr4,5620
5
- cuneus/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- cuneus/core/application.py,sha256=QSOSmo8-YwjbC6QVe_VkDRa0AdRStTZX9bSqcbf3MFw,6670
7
- cuneus/core/execptions.py,sha256=fM7-KbxYxzixehYd5pSDgWDYBW6IsawdJ6NwCxeIJPM,5695
8
- cuneus/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- cuneus/ext/health.py,sha256=YYKKfJ8X71hnqN8vCKydqUK7BmlvZrqeBs7L61r7zp8,3814
10
- cuneus/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- cuneus/middleware/logging.py,sha256=6KYakjrjzjfgdQ80xSsRtT3pX7NFAGkLUwceiEhDLfI,4780
12
- cuneus-0.2.1.dist-info/METADATA,sha256=c_ZSnSwnY8zjS-p9GQUTFQCYVPzsrboGizwRvISTAwE,6501
13
- cuneus-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
- cuneus-0.2.1.dist-info/entry_points.txt,sha256=tzPgom-_UkpP_uLKv3V_XyoIsKg84FBAc9ddjYl0W0Y,43
15
- cuneus-0.2.1.dist-info/RECORD,,
File without changes