Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
007940e
refactor(python): fix signals logger name
Einswilli Jun 3, 2026
6372fef
refactor(python): fix transaction logger name
Einswilli Jun 3, 2026
61c5777
feat(python): implement logging from RYX_LOG_LEVEL env var
Einswilli Jun 3, 2026
8bd95ed
feat(python): add logging for bulk operations
Einswilli Jun 3, 2026
c4c3c18
feat(python): add logging for caching operations
Einswilli Jun 3, 2026
fb184e8
feat(python): wire CLI debug/verbose flags to logging
Einswilli Jun 3, 2026
3017d15
feat(python): add debug logs for CRUD operations
Einswilli Jun 3, 2026
15f7985
feat(python): log raw parameterized query executions
Einswilli Jun 3, 2026
8a24030
feat(python): log select_related execution
Einswilli Jun 3, 2026
8c600e9
feat(python): log global router configuration
Einswilli Jun 3, 2026
56a1379
feat(python): initialize Rust tracing from Python module
Einswilli Jun 3, 2026
7deb521
feat(query): add debug trace during query compilation
Einswilli Jun 3, 2026
3a7db91
feat(query): add debug trace during lookup registration
Einswilli Jun 3, 2026
a55960b
feat(rs): add init_tracing and call it on init
Einswilli Jun 3, 2026
6e35d39
chore: update dependencies and Lockfile for tracing
Einswilli Jun 3, 2026
eb9cddc
feat(logging): support silencing via NO_LOG/OFF/SILENT in RYX_LOG_LEVEL
Einswilli Jun 3, 2026
73c862e
feat(migrations): extract standalone helpers and add database alias s…
Einswilli Jun 3, 2026
a9709da
feat(cli): add --alias option to makemigrations command
Einswilli Jun 3, 2026
74e0e2e
feat(cli): support --no-interactive and configure migration directory…
Einswilli Jun 3, 2026
615d71d
feat(migrations): implement file-based migrations, database tracking,…
Einswilli Jun 3, 2026
06f5d85
feat(cli): add color and formatting utilities for CLI output
Einswilli Jun 3, 2026
5f19f05
feat(migrations): inject model references in operations and auto-impo…
Einswilli Jun 3, 2026
9794404
refactor(cli): use new style module in makemigrations output
Einswilli Jun 3, 2026
9281c54
refactor(cli): cleanup imports and apply styling in migrate command
Einswilli Jun 3, 2026
d1dd73e
feat(migrations): implement flat discovery, per-alias operation routi…
Einswilli Jun 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ryx-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ sqlx = { workspace = true }
tokio = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
tracing = { workspace = true }
34 changes: 32 additions & 2 deletions ryx-python/ryx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,26 @@

# Import the compiled Rust extension directly to avoid circular import
import ryx.ryx_core as _core
import logging as _logging
import os

# Configure Ryx logging from RYX_LOG_LEVEL env var
_ryx_logger = _logging.getLogger("ryx")
_ryx_log_level = os.environ.get("RYX_LOG_LEVEL", "").strip().upper()
if _ryx_log_level in ("NO_LOG", "OFF", "SILENT"):
_ryx_logger.disabled = True
_ryx_logger.propagate = False
_ryx_logger.addHandler(_logging.NullHandler())
elif _ryx_log_level:
_ryx_log_level_num = getattr(_logging, _ryx_log_level, _logging.INFO)
_ryx_log_handler = _logging.StreamHandler()
_ryx_log_handler.setFormatter(
_logging.Formatter("[ryx] %(levelname)s %(name)s: %(message)s")
)
_ryx_logger.addHandler(_ryx_log_handler)
_ryx_logger.setLevel(_ryx_log_level_num)
_ryx_logger.propagate = False


# ORM core
from ryx.models import Constraint, Index, Model
Expand Down Expand Up @@ -123,8 +141,12 @@ async def setup(

# For old versions wrap the url with a dict
if isinstance(urls, str):
urls = {'default': urls}
urls = {'default': urls}

_ryx_logger.info(
"Initializing pools: %s (max_conn=%d, min_conn=%d)",
list(urls.keys()), max_connections, min_connections,
)
await _core.setup(
urls,
max_connections = max_connections,
Expand All @@ -133,10 +155,12 @@ async def setup(
idle_timeout = idle_timeout,
max_lifetime = max_lifetime,
)
_ryx_logger.info("Pools initialized: %s", list(urls.keys()))


def register_lookup(name: str, sql_template: str) -> None:
"""Register a custom lookup operator (process-global)."""
_ryx_logger.debug("Register custom lookup: %s = %s", name, sql_template)
_core.register_lookup(name, sql_template)


Expand Down Expand Up @@ -345,7 +369,10 @@ def _discover_config_file():

def _auto_setup():
global _AUTO_INIT_DONE
if _AUTO_INIT_DONE or not _should_auto_init():
if _AUTO_INIT_DONE:
return
if not _should_auto_init():
_ryx_logger.debug("Auto-init disabled via RYX_AUTO_INITIALIZE")
return

urls = _discover_urls_from_env()
Expand All @@ -356,8 +383,11 @@ def _auto_setup():
pool_cfg = cfg.get("pool", {}) or {}

if not urls:
_ryx_logger.debug("No URLs found — auto-init skipped")
return

_ryx_logger.info("Auto-initializing with URLs: %s", list(urls.keys()))

try:
import asyncio

Expand Down
18 changes: 18 additions & 0 deletions ryx-python/ryx/bulk.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from __future__ import annotations

import logging
from typing import List, Sequence, Type, TYPE_CHECKING, Optional

if TYPE_CHECKING:
Expand All @@ -33,6 +34,8 @@
from ryx import ryx_core as _core
from ryx.router import get_router

logger = logging.getLogger("ryx.bulk")


def _resolve_alias(model: "Model") -> Optional[str]:
"""Resolve DB alias using Router → Meta.database → default(None)."""
Expand Down Expand Up @@ -96,6 +99,11 @@ async def bulk_create(
"""
from ryx.models import _apply_auto_timestamps

logger.info(
"bulk_create %s: %d rows (batch_size=%d, validate=%s, ignore_conflicts=%s)",
model.__name__, len(instances), batch_size, validate, ignore_conflicts,
)

if not instances:
return list(instances)

Expand Down Expand Up @@ -271,6 +279,11 @@ async def bulk_update(
Signals:
Does NOT fire pre_save / post_save signals (for performance).
"""
logger.info(
"bulk_update %s: %d rows, fields=%s (batch_size=%d)",
model.__name__, len(instances), fields, batch_size,
)

if not instances or not fields:
return 0

Expand Down Expand Up @@ -352,6 +365,11 @@ async def bulk_delete(
Signals:
Does NOT fire pre_delete / post_delete signals.
"""
logger.info(
"bulk_delete %s: %d rows (batch_size=%d)",
model.__name__, len(instances), batch_size,
)

pk_field = model._meta.pk_field
if not pk_field:
raise ValueError(f"{model.__name__} has no primary key")
Expand Down
12 changes: 12 additions & 0 deletions ryx-python/ryx/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@
import asyncio
import hashlib
import json
import logging
import time
from abc import ABC, abstractmethod
from collections import OrderedDict
from typing import Any, Optional

logger = logging.getLogger("ryx.cache")


####
## ABSTRACT CACHE BACKEND
Expand Down Expand Up @@ -175,6 +178,10 @@ def configure_cache(
global _cache_backend, _auto_invalidate
_cache_backend = backend
_auto_invalidate = auto_invalidate
logger.info(
"Cache configured: backend=%s, auto_invalidate=%s",
type(backend).__name__, auto_invalidate,
)

if auto_invalidate:
_register_invalidation_signals()
Expand Down Expand Up @@ -215,6 +222,7 @@ async def invalidate(key: str) -> None:
"""
if _cache_backend:
await _cache_backend.delete(key)
logger.debug("Cache invalidate: %s", key)


async def invalidate_model(model: type) -> None:
Expand All @@ -231,12 +239,14 @@ async def invalidate_model(model: type) -> None:
keys = await _cache_backend.keys(prefix)
if keys:
await _cache_backend.delete_many(keys)
logger.debug("Cache invalidate model %s: %d keys", model.__name__, len(keys))


async def invalidate_all() -> None:
"""Clear the entire cache."""
if _cache_backend:
await _cache_backend.clear()
logger.debug("Cache invalidate all")


####
Expand Down Expand Up @@ -269,9 +279,11 @@ async def _execute(self) -> list:
# Try cache first
cached = await backend.get(key)
if cached is not None:
logger.debug("Cache HIT: %s", key)
return cached

# Cache miss → hit DB
logger.debug("Cache MISS: %s", key)
result = await super()._execute() # type: ignore[misc]

# Serialise model instances to plain dicts for caching
Expand Down
24 changes: 17 additions & 7 deletions ryx-python/ryx/cli/commands/makemigrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import sys

from ryx.cli.commands.base import Command
from ryx.cli.style import PREFIX, OK, WARN, green, red, yellow


class MakeMigrationsCommand(Command):
Expand All @@ -28,6 +29,11 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
metavar="DIR",
help="Migrations directory (default: migrations)",
)
parser.add_argument(
"--alias",
metavar="ALIAS",
help="Database alias to generate migrations for (creates subdirectory)",
)
parser.add_argument(
"--name", metavar="NAME", help="Override migration name slug"
)
Expand All @@ -44,30 +50,34 @@ async def execute(self, args: argparse.Namespace) -> int:
cfg = getattr(args, "resolved_config", None) or resolve_config(args)
models = self._load_models(args.models or cfg.models)
if not models:
print("[ryx] No models found. Pass --models myapp.models or set [models].files in ryx.toml")
print(f"{PREFIX} {WARN} No models found. Pass {yellow('--models myapp.models')} or set [models].files in ryx.toml")
return 1

from ryx.migrations.autodetect import Autodetector

detector = Autodetector(models=models, migrations_dir=args.dir)
detector = Autodetector(
models=models,
migrations_dir=args.dir,
alias=getattr(args, "alias", None),
)
operations = detector.detect()

if not operations:
print("[ryx] No changes detected.")
print(f"{PREFIX} No changes detected.")
if args.check:
return 0
return 0

if args.check:
print(f"[ryx] {len(operations)} change(s) detected:")
print(f"{PREFIX} {red(str(len(operations)))} change(s) detected:")
for op in operations:
print(f" - {op.describe()}")
print(f" {op.describe()}")
return 1

path = detector.write_migration(operations)
print(f"[ryx] Created migration: {path}")
print(f"{PREFIX} {OK} Created {green(str(path.name))}")
for op in operations:
print(f" - {op.describe()}")
print(f" {op.describe()}")

return 0

Expand Down
36 changes: 18 additions & 18 deletions ryx-python/ryx/cli/commands/migrate.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
from __future__ import annotations

import argparse
import asyncio
import sys
from pathlib import Path
from typing import List, Optional
from typing import Optional

from ryx.cli.commands.base import Command
from ryx.cli.config import get_config, Config
from ryx.cli.style import PREFIX, OK, FAIL, cyan, green


class MigrateCommand(Command):
Expand Down Expand Up @@ -38,6 +37,11 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
metavar="ALIAS",
help="Run migrations for a specific database alias",
)
parser.add_argument(
"--no-interactive",
action="store_true",
help="Fail if no migration files found (for CI)",
)

async def execute(self, args: argparse.Namespace) -> int:
cfg = getattr(args, "resolved_config", None)
Expand All @@ -52,7 +56,7 @@ async def execute(self, args: argparse.Namespace) -> int:

# Masking the first URL for the log
first_url = list(urls.values())[0] if isinstance(urls, dict) else urls
print(f"[ryx] Connecting to {self._mask_url(first_url)} ...")
print(f"{PREFIX} Connecting to {cyan(self._mask_url(first_url))} ...")

import ryx

Expand All @@ -66,22 +70,17 @@ async def execute(self, args: argparse.Namespace) -> int:
models,
dry_run=getattr(args, "dry_run", False),
alias_filter=getattr(args, "database", None) or (cfg.db_alias if cfg else None),
migrations_dir=getattr(args, "dir", "migrations"),
no_interactive=getattr(args, "no_interactive", False),
)

if getattr(args, "plan", False):
# For plan, we just want to see what would happen
# In a real implementation, this would be a separate runner method
print("[ryx] --plan is active. Running in dry-run mode...")
# We could force dry_run = True here

changes = await runner.migrate()

if changes:
print(
f"[ryx] Applied {len(changes)} change(s) across configured databases."
)
count = green(str(len(changes)))
print(f"{PREFIX} {OK} Applied {count} change(s)")
else:
print("[ryx] No pending migrations.")
print(f"{PREFIX} No pending migrations")

return 0

Expand Down Expand Up @@ -129,11 +128,12 @@ def _mask_url(self, url: str) -> str:
return re.sub(r"(:)[^:@/]+(@)", r"\1***\2", url)

def _print_missing_url(self) -> None:
from ryx.cli.style import FAIL, yellow, cyan
print(
"[ryx] No database URL found.\n"
" Set RYX_DATABASE_URL environment variable, or\n"
" pass --url postgres://user:pass@host/db, or\n"
" create ryx_settings.py with DATABASE_URL = '...'"
f"{PREFIX} {FAIL} No database URL found.\n"
f" Set {cyan('RYX_DATABASE_URL')} environment variable, or\n"
f" pass {yellow('--url postgres://user:pass@host/db')}, or\n"
f" create {cyan('ryx_settings.py')} with {cyan('DATABASE_URL')} = '...'"
)


Expand Down
12 changes: 12 additions & 0 deletions ryx-python/ryx/cli/config.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
from __future__ import annotations

import logging
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, Optional, List

from ryx.cli.config_loader import get_loader, load_config

_ryx_logger = logging.getLogger("ryx")
_logger = logging.getLogger("ryx.cli")


@dataclass
class Config:
Expand Down Expand Up @@ -54,6 +58,14 @@ def from_args(cls, args) -> "Config":
config.debug = getattr(args, "debug", False)
config.verbose = getattr(args, "verbose", False)

# Wire CLI flags to logging levels
if config.debug:
_ryx_logger.setLevel(logging.DEBUG)
_logger.debug("Debug mode enabled via CLI --debug")
elif config.verbose:
_ryx_logger.setLevel(logging.INFO)
_logger.info("Verbose mode enabled via CLI --verbose")

# Load config file if specified
config_file = getattr(args, "config_file", None)
if config_file:
Expand Down
Loading
Loading