nostr-dev 1.0.0__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.
- nostr_dev/__init__.py +3 -0
- nostr_dev/cli.py +62 -0
- nostr_dev/commands/__init__.py +1 -0
- nostr_dev/commands/common.py +80 -0
- nostr_dev/commands/events.py +86 -0
- nostr_dev/commands/github.py +483 -0
- nostr_dev/commands/keys.py +152 -0
- nostr_dev/commands/publish.py +84 -0
- nostr_dev/commands/relay.py +184 -0
- nostr_dev/config.py +102 -0
- nostr_dev/db.py +53 -0
- nostr_dev/exceptions.py +47 -0
- nostr_dev/logging.py +83 -0
- nostr_dev/migrations/env.py +64 -0
- nostr_dev/migrations/script.py.mako +25 -0
- nostr_dev/migrations/versions/.gitkeep +1 -0
- nostr_dev/migrations/versions/41f8dd9a0e37_initial_schema.py +178 -0
- nostr_dev/models/__init__.py +15 -0
- nostr_dev/models/github.py +66 -0
- nostr_dev/models/keys.py +16 -0
- nostr_dev/models/notifications.py +18 -0
- nostr_dev/models/relays.py +18 -0
- nostr_dev/secrets.py +187 -0
- nostr_dev/services/__init__.py +1 -0
- nostr_dev/services/github.py +405 -0
- nostr_dev/services/mapper.py +102 -0
- nostr_dev/services/nostr.py +112 -0
- nostr_dev/services/relay.py +295 -0
- nostr_dev/services/scheduler.py +24 -0
- nostr_dev/utils/__init__.py +1 -0
- nostr_dev-1.0.0.dist-info/METADATA +254 -0
- nostr_dev-1.0.0.dist-info/RECORD +34 -0
- nostr_dev-1.0.0.dist-info/WHEEL +4 -0
- nostr_dev-1.0.0.dist-info/entry_points.txt +2 -0
nostr_dev/__init__.py
ADDED
nostr_dev/cli.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Command-line interface for nostr-dev."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from nostr_dev import __version__
|
|
6
|
+
from nostr_dev.commands.common import handle_command_errors, set_debug_enabled
|
|
7
|
+
from nostr_dev.commands.events import events
|
|
8
|
+
from nostr_dev.commands.github import github_app
|
|
9
|
+
from nostr_dev.commands.keys import import_command, keygen, whoami
|
|
10
|
+
from nostr_dev.commands.publish import publish
|
|
11
|
+
from nostr_dev.commands.relay import relay_app
|
|
12
|
+
from nostr_dev.logging import setup_logging
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
add_completion=False,
|
|
16
|
+
help="Developer tooling for Nostr workflows.",
|
|
17
|
+
no_args_is_help=True,
|
|
18
|
+
pretty_exceptions_show_locals=False,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def version_callback(value: bool) -> None:
|
|
23
|
+
"""Print the application version and exit."""
|
|
24
|
+
if value:
|
|
25
|
+
typer.echo(f"nostr-dev {__version__}")
|
|
26
|
+
raise typer.Exit
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.callback()
|
|
30
|
+
@handle_command_errors
|
|
31
|
+
def main(
|
|
32
|
+
version: bool = typer.Option(
|
|
33
|
+
False,
|
|
34
|
+
"--version",
|
|
35
|
+
callback=version_callback,
|
|
36
|
+
help="Show the application version and exit.",
|
|
37
|
+
is_eager=True,
|
|
38
|
+
),
|
|
39
|
+
verbose: bool = typer.Option(
|
|
40
|
+
False,
|
|
41
|
+
"--verbose",
|
|
42
|
+
"-v",
|
|
43
|
+
help="Show more detailed progress output.",
|
|
44
|
+
),
|
|
45
|
+
debug: bool = typer.Option(
|
|
46
|
+
False,
|
|
47
|
+
"--debug",
|
|
48
|
+
help="Show debug logs and full tracebacks for unexpected errors.",
|
|
49
|
+
),
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Developer tooling for Nostr workflows."""
|
|
52
|
+
set_debug_enabled(debug)
|
|
53
|
+
setup_logging(verbose=verbose, debug=debug)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
app.command("keygen")(keygen)
|
|
57
|
+
app.command("import")(import_command)
|
|
58
|
+
app.command("whoami")(whoami)
|
|
59
|
+
app.command("publish")(publish)
|
|
60
|
+
app.command("events")(events)
|
|
61
|
+
app.add_typer(relay_app, name="relay")
|
|
62
|
+
app.add_typer(github_app, name="github")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command modules."""
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Shared CLI command helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Iterator
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from functools import wraps
|
|
8
|
+
|
|
9
|
+
import structlog
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from sqlalchemy.exc import SQLAlchemyError
|
|
13
|
+
from sqlmodel import Session, SQLModel
|
|
14
|
+
|
|
15
|
+
from nostr_dev.config import load_config
|
|
16
|
+
from nostr_dev.db import create_db_engine, get_session
|
|
17
|
+
from nostr_dev.exceptions import ConfigError, NostrDevError
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
err_console = Console(stderr=True)
|
|
21
|
+
debug_enabled = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def set_debug_enabled(value: bool) -> None:
|
|
25
|
+
"""Set whether unexpected command exceptions should be re-raised."""
|
|
26
|
+
global debug_enabled
|
|
27
|
+
debug_enabled = value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def handle_command_errors[**P, R](command: Callable[P, R]) -> Callable[P, R]:
|
|
31
|
+
"""Print clean CLI errors for expected failures and short errors otherwise."""
|
|
32
|
+
|
|
33
|
+
@wraps(command)
|
|
34
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
35
|
+
try:
|
|
36
|
+
return command(*args, **kwargs)
|
|
37
|
+
except NostrDevError as exc:
|
|
38
|
+
err_console.print(str(exc), style="red")
|
|
39
|
+
raise typer.Exit(1) from exc
|
|
40
|
+
except typer.Exit:
|
|
41
|
+
raise
|
|
42
|
+
except Exception as exc:
|
|
43
|
+
structlog.get_logger(__name__).exception("unexpected_cli_error")
|
|
44
|
+
if debug_enabled:
|
|
45
|
+
raise
|
|
46
|
+
err_console.print(
|
|
47
|
+
f"Unexpected error: {exc}. Run with --debug for full traceback.",
|
|
48
|
+
style="red",
|
|
49
|
+
)
|
|
50
|
+
raise typer.Exit(1) from exc
|
|
51
|
+
|
|
52
|
+
return wrapper
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@contextmanager
|
|
56
|
+
def managed_session() -> Iterator[Session]:
|
|
57
|
+
"""Yield a command session after ensuring the local database exists."""
|
|
58
|
+
ensure_database()
|
|
59
|
+
try:
|
|
60
|
+
with get_session() as session:
|
|
61
|
+
yield session
|
|
62
|
+
except SQLAlchemyError as exc:
|
|
63
|
+
raise ConfigError("Database operation failed.") from exc
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def ensure_database() -> None:
|
|
67
|
+
"""Ensure database tables exist for command execution."""
|
|
68
|
+
try:
|
|
69
|
+
config = load_config()
|
|
70
|
+
engine = create_db_engine(config)
|
|
71
|
+
try:
|
|
72
|
+
SQLModel.metadata.create_all(engine)
|
|
73
|
+
finally:
|
|
74
|
+
engine.dispose()
|
|
75
|
+
except NostrDevError:
|
|
76
|
+
raise
|
|
77
|
+
except SQLAlchemyError as exc:
|
|
78
|
+
raise ConfigError("Database setup failed.") from exc
|
|
79
|
+
except OSError as exc:
|
|
80
|
+
raise ConfigError("Could not prepare nostr-dev data directory.") from exc
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Query Nostr events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from nostr_sdk import Event, Filter
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
from sqlmodel import Session, col, select
|
|
13
|
+
|
|
14
|
+
from nostr_dev.commands.common import handle_command_errors, managed_session
|
|
15
|
+
from nostr_dev.exceptions import ConfigError
|
|
16
|
+
from nostr_dev.models import Relay
|
|
17
|
+
from nostr_dev.services.nostr import NostrEventSummary, build_subscription_filter, summarize_event
|
|
18
|
+
from nostr_dev.services.relay import RelayPool
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
_managed_session = managed_session
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@handle_command_errors
|
|
25
|
+
def events(
|
|
26
|
+
author: str | None = typer.Option(None, "--author", help="Filter by author npub."),
|
|
27
|
+
limit: int = typer.Option(20, "--limit", min=1, help="Maximum number of events to show."),
|
|
28
|
+
kind: int | None = typer.Option(None, "--kind", min=0, help="Filter by event kind."),
|
|
29
|
+
timeout_seconds: float = typer.Option(
|
|
30
|
+
8.0,
|
|
31
|
+
"--timeout",
|
|
32
|
+
min=0.1,
|
|
33
|
+
help="Subscription timeout in seconds.",
|
|
34
|
+
),
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Query active relays for Nostr events."""
|
|
37
|
+
with _managed_session() as session:
|
|
38
|
+
relay_urls = _active_relay_urls(session)
|
|
39
|
+
if not relay_urls:
|
|
40
|
+
raise ConfigError("No active relays found. Run `nostr-dev relay add <url>`.")
|
|
41
|
+
|
|
42
|
+
subscription_filter = build_subscription_filter(author=author, kind=kind, limit=limit)
|
|
43
|
+
sdk_events = asyncio.run(_subscribe_events(subscription_filter, relay_urls, timeout_seconds))
|
|
44
|
+
summaries = [summarize_event(event) for event in sdk_events[:limit]]
|
|
45
|
+
_print_events(summaries)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def _subscribe_events(
|
|
49
|
+
subscription_filter: Filter,
|
|
50
|
+
relay_urls: list[str],
|
|
51
|
+
timeout_seconds: float,
|
|
52
|
+
) -> list[Event]:
|
|
53
|
+
pool = RelayPool(relay_urls)
|
|
54
|
+
return await pool.subscribe(subscription_filter, relay_urls, timeout_seconds)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _print_events(event_summaries: list[NostrEventSummary]) -> None:
|
|
58
|
+
table = Table(title="Nostr Events")
|
|
59
|
+
table.add_column("Author", style="cyan")
|
|
60
|
+
table.add_column("Kind")
|
|
61
|
+
table.add_column("Created")
|
|
62
|
+
table.add_column("Content")
|
|
63
|
+
for event in event_summaries:
|
|
64
|
+
table.add_row(
|
|
65
|
+
event.author,
|
|
66
|
+
str(event.kind),
|
|
67
|
+
_format_timestamp(event.created_at),
|
|
68
|
+
_preview(event.content),
|
|
69
|
+
)
|
|
70
|
+
console.print(table)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _preview(content: str, max_length: int = 80) -> str:
|
|
74
|
+
normalized = " ".join(content.split())
|
|
75
|
+
if len(normalized) <= max_length:
|
|
76
|
+
return normalized
|
|
77
|
+
return f"{normalized[: max_length - 1]}..."
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _format_timestamp(timestamp: int) -> str:
|
|
81
|
+
return datetime.fromtimestamp(timestamp, tz=UTC).isoformat(timespec="seconds")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _active_relay_urls(session: Session) -> list[str]:
|
|
85
|
+
statement = select(Relay).where(Relay.is_active).order_by(col(Relay.url).asc())
|
|
86
|
+
return [relay.url for relay in session.exec(statement).all()]
|