rpr-cli 0.1.1__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.
- rpr/__init__.py +1 -0
- rpr/agent/__init__.py +29 -0
- rpr/agent/approval.py +19 -0
- rpr/agent/bootstrap.py +60 -0
- rpr/agent/client.py +115 -0
- rpr/agent/control.py +16 -0
- rpr/agent/mock.py +158 -0
- rpr/agent/runtime.py +291 -0
- rpr/agent/session.py +91 -0
- rpr/agent/tools/__init__.py +10 -0
- rpr/agent/tools/base.py +62 -0
- rpr/agent/tools/mutating.py +47 -0
- rpr/agent/tools/readonly.py +147 -0
- rpr/agent/tools/registry.py +30 -0
- rpr/application/__init__.py +1 -0
- rpr/application/catalog.py +427 -0
- rpr/application/chat_service.py +413 -0
- rpr/application/checks.py +49 -0
- rpr/application/cli_adapter.py +36 -0
- rpr/application/completer.py +163 -0
- rpr/application/conversation_service.py +92 -0
- rpr/application/prompt_service.py +85 -0
- rpr/application/selector.py +240 -0
- rpr/application/shell.py +543 -0
- rpr/checks/__init__.py +0 -0
- rpr/checks/base.py +13 -0
- rpr/checks/instructions.py +89 -0
- rpr/checks/packages.py +619 -0
- rpr/checks/workspace.py +170 -0
- rpr/cli.py +78 -0
- rpr/commands/__init__.py +0 -0
- rpr/commands/add.py +166 -0
- rpr/commands/chat.py +48 -0
- rpr/commands/check.py +53 -0
- rpr/commands/generate/__init__.py +0 -0
- rpr/commands/generate/api.py +228 -0
- rpr/commands/generate/domain.py +383 -0
- rpr/commands/generate/engine.py +148 -0
- rpr/commands/generate/storybook.py +442 -0
- rpr/commands/generate/ui.py +414 -0
- rpr/commands/init.py +822 -0
- rpr/commands/map.py +113 -0
- rpr/commands/settings.py +102 -0
- rpr/commands/sync.py +97 -0
- rpr/context.py +203 -0
- rpr/generators/__init__.py +0 -0
- rpr/generators/base.py +110 -0
- rpr/generators/claude.py +33 -0
- rpr/generators/copilot.py +36 -0
- rpr/generators/cursor.py +38 -0
- rpr/generators/gemini.py +33 -0
- rpr/map/__init__.py +0 -0
- rpr/map/architecture.py +495 -0
- rpr/map/chains.py +317 -0
- rpr/map/classifier.py +170 -0
- rpr/map/coverage.py +200 -0
- rpr/map/dependencies.py +243 -0
- rpr/map/extractor.py +223 -0
- rpr/map/graph.py +318 -0
- rpr/map/output.py +1030 -0
- rpr/map/responsibility.py +345 -0
- rpr/map/topology.py +327 -0
- rpr/map/walker.py +151 -0
- rpr/scaffolds/domain/base_entity.md +30 -0
- rpr/scaffolds/domain/base_repo.md +48 -0
- rpr/scaffolds/domain/container.md +76 -0
- rpr/scaffolds/domain/settings.md +57 -0
- rpr/scaffolds/instructions/all.instructions.md +50 -0
- rpr/scaffolds/instructions/api.instructions.md +42 -0
- rpr/scaffolds/instructions/domain.instructions.md +93 -0
- rpr/scaffolds/instructions/frontend.instructions.md +97 -0
- rpr/scaffolds/instructions/rust-engine.instructions.md +40 -0
- rpr/scaffolds/instructions/setup-guide.instructions.md +86 -0
- rpr/scaffolds/instructions/tooling-setup.instructions.md +97 -0
- rpr/scaffolds/instructions/tooling.instructions.md +42 -0
- rpr/scaffolds/js_special_files/fetch.service.md +222 -0
- rpr/scaffolds/js_special_files/sticky-navigation.md +164 -0
- rpr/scaffolds/special_files/domain_container.md +76 -0
- rpr/scaffolds/special_files/domain_settings.md +57 -0
- rpr/scaffolds/special_files/dto_util.md +62 -0
- rpr/scaffolds/special_files/encrypted_column.md +98 -0
- rpr/scaffolds/special_files/mapper_util.md +159 -0
- rpr/scaffolds/special_files/partial_update.md +61 -0
- rpr/templates/__init__.py +0 -0
- rpr/templates/registry.py +81 -0
- rpr/ui/__init__.py +0 -0
- rpr/ui/console.py +32 -0
- rpr/ui/markdown.py +59 -0
- rpr/ui/prompt_session.py +430 -0
- rpr/ui/renderers.py +167 -0
- rpr/ui/theme.py +286 -0
- rpr/workspace.py +131 -0
- rpr_cli-0.1.1.dist-info/METADATA +201 -0
- rpr_cli-0.1.1.dist-info/RECORD +97 -0
- rpr_cli-0.1.1.dist-info/WHEEL +4 -0
- rpr_cli-0.1.1.dist-info/entry_points.txt +2 -0
- rpr_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
## Domain Container Template
|
|
2
|
+
|
|
3
|
+
Target path: `packages/python/domain/src/{name_underscore}_domain/config/container.py`
|
|
4
|
+
|
|
5
|
+
Use this template when scaffolding the domain DI container. It wires the core infrastructure providers — database engine and HTTP client — as singletons driven by `Settings`.
|
|
6
|
+
|
|
7
|
+
The first Python block is the generated file used by `rpr generate domain` and `rpr add template domain_container`.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from dependency_injector import containers, providers
|
|
14
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
15
|
+
|
|
16
|
+
from your_app_domain.config.settings import Settings
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Container(containers.DeclarativeContainer):
|
|
20
|
+
wiring_config = containers.WiringConfiguration(modules=[])
|
|
21
|
+
|
|
22
|
+
config = providers.Singleton(Settings)
|
|
23
|
+
|
|
24
|
+
db_engine = providers.Singleton(
|
|
25
|
+
lambda settings: create_async_engine(
|
|
26
|
+
settings.database_url,
|
|
27
|
+
echo=settings.db_echo,
|
|
28
|
+
pool_pre_ping=True,
|
|
29
|
+
pool_recycle=3600,
|
|
30
|
+
),
|
|
31
|
+
settings=config,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
http_client = providers.Singleton(
|
|
35
|
+
httpx.AsyncClient,
|
|
36
|
+
timeout=config.provided.http_timeout,
|
|
37
|
+
limits=providers.Factory(
|
|
38
|
+
httpx.Limits,
|
|
39
|
+
max_connections=config.provided.httpx_max_connections,
|
|
40
|
+
max_keepalive_connections=config.provided.http_max_keepalive,
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# To add lifecycle methods (startup/shutdown hooks), override __new__:
|
|
45
|
+
#
|
|
46
|
+
# def __new__(cls):
|
|
47
|
+
# instance = super().__new__(cls)
|
|
48
|
+
# return add_lifecycle_methods(instance)
|
|
49
|
+
#
|
|
50
|
+
# Implement add_lifecycle_methods() in this module to attach
|
|
51
|
+
# startup and shutdown handlers to the container instance.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## What This Template Provides
|
|
55
|
+
|
|
56
|
+
- `config`: a singleton `Settings` instance that reads from `.env`.
|
|
57
|
+
- `db_engine`: a singleton async SQLAlchemy engine built from `settings.database_url`, with connection-pool tuning defaults (`pool_pre_ping`, `pool_recycle`).
|
|
58
|
+
- `http_client`: a singleton `httpx.AsyncClient` with timeout and connection limits sourced from `Settings`.
|
|
59
|
+
|
|
60
|
+
## Lifecycle Extension Pattern
|
|
61
|
+
|
|
62
|
+
If the project needs startup or shutdown hooks (e.g., acquiring a connection pool, registering signal handlers), override `__new__` on `Container`:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
def __new__(cls):
|
|
66
|
+
instance = super().__new__(cls)
|
|
67
|
+
return add_lifecycle_methods(instance)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Define `add_lifecycle_methods(instance)` in the same module to attach `on_startup` and `on_shutdown` handlers without modifying the provider declarations.
|
|
71
|
+
|
|
72
|
+
## Usage Notes
|
|
73
|
+
|
|
74
|
+
- Register new services and repositories as providers directly on `Container`; do not subclass it.
|
|
75
|
+
- Inject the container into FastAPI (or the framework of your choice) via its `wire()` method in the app factory.
|
|
76
|
+
- Keep transport-specific clients (e.g., third-party API clients) as additional `providers.Singleton` entries, bound to `config` for their credentials.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
## Domain Settings Template
|
|
2
|
+
|
|
3
|
+
Target path: `packages/python/domain/src/{name_underscore}_domain/config/settings.py`
|
|
4
|
+
|
|
5
|
+
Use this template when scaffolding the domain configuration layer. It provides a Pydantic `BaseSettings` class that loads from `.env` and defines all core infrastructure settings needed by the DI container.
|
|
6
|
+
|
|
7
|
+
The first Python block is the generated file used by `rpr generate domain` and `rpr add template domain_settings`.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Settings(BaseSettings):
|
|
17
|
+
model_config = SettingsConfigDict(
|
|
18
|
+
env_file=".env",
|
|
19
|
+
env_file_encoding="utf-8",
|
|
20
|
+
case_sensitive=False,
|
|
21
|
+
extra="ignore",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
db_host: str = Field(default="localhost", description="Database host")
|
|
25
|
+
db_port: int = Field(default=5432, description="Database port")
|
|
26
|
+
db_name: str = Field(default="postgres", description="Database name")
|
|
27
|
+
db_user: str = Field(default="postgres", description="Database user")
|
|
28
|
+
db_password: str = Field(default="", description="Database password")
|
|
29
|
+
db_sslmode: str = Field(default="prefer", description="Database SSL mode")
|
|
30
|
+
db_echo: bool = Field(default=False, description="Enable SQL query logging")
|
|
31
|
+
db_encrypt_key: bytes | None = Field(default=None, description="Fernet encryption key")
|
|
32
|
+
|
|
33
|
+
http_timeout: float = Field(default=30.0, description="HTTP client timeout in seconds")
|
|
34
|
+
httpx_max_connections: int = Field(default=100, description="Maximum HTTP connections")
|
|
35
|
+
http_max_keepalive: int = Field(default=20, description="Maximum keepalive HTTP connections")
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def database_url(self) -> str:
|
|
39
|
+
return (
|
|
40
|
+
f"postgresql+asyncpg://{self.db_user}:{self.db_password}"
|
|
41
|
+
f"@{self.db_host}:{self.db_port}/{self.db_name}"
|
|
42
|
+
f"?sslmode={self.db_sslmode}"
|
|
43
|
+
)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## What This Template Provides
|
|
47
|
+
|
|
48
|
+
- Reads all settings from `.env` with `case_sensitive=False` and `extra="ignore"` so unknown keys never raise errors.
|
|
49
|
+
- Defines the full set of database fields used to build `database_url` in the `@property`.
|
|
50
|
+
- Defines `http_timeout`, `httpx_max_connections`, and `http_max_keepalive` so the container's `http_client` provider can bind them without extra lookups.
|
|
51
|
+
- `db_encrypt_key` is `bytes | None` to pair with the `encrypted_column` utility when needed.
|
|
52
|
+
|
|
53
|
+
## Usage Notes
|
|
54
|
+
|
|
55
|
+
- Add corresponding keys to `.env.example` for each field (see `special_files.md` ENV section).
|
|
56
|
+
- Extend `Settings` in the domain package to add app-specific fields; do not modify this base set.
|
|
57
|
+
- `database_url` is a computed property — it is not read from `.env` directly.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
## DTO Helper Template
|
|
2
|
+
|
|
3
|
+
Target path: `/packages/python/domain/src/{name_underscore}_domain/utils/dto_util.py`
|
|
4
|
+
|
|
5
|
+
Use this helper when the same DTO mapper should support both a single entity and a list of entities without requiring separate functions.
|
|
6
|
+
|
|
7
|
+
The first Python block is the generated file used by `rpr add template dto_util`.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from functools import wraps
|
|
11
|
+
from typing import Any, Callable, overload
|
|
12
|
+
|
|
13
|
+
class DtoHelper[ModelType, R]:
|
|
14
|
+
"""A smart DTO helper that can convert a single entity or a list of entities to DTO(s) without requiring separate functions."""
|
|
15
|
+
def __init__(self, to_dto_func: Callable[[ModelType], R]):
|
|
16
|
+
self.to_dto_func = to_dto_func
|
|
17
|
+
wraps(to_dto_func)(self)
|
|
18
|
+
|
|
19
|
+
@overload
|
|
20
|
+
def __call__(self, model: ModelType, *args, **kwargs) -> R: ...
|
|
21
|
+
|
|
22
|
+
@overload
|
|
23
|
+
def __call__(self, model: list[ModelType], *args, **kwargs) -> list[R]: ...
|
|
24
|
+
|
|
25
|
+
def __call__(self, model: Any, *args, **kwargs) -> Any:
|
|
26
|
+
if isinstance(model, list):
|
|
27
|
+
return [
|
|
28
|
+
self.to_dto_func(*m, *args, **kwargs)
|
|
29
|
+
if isinstance(m, tuple)
|
|
30
|
+
else self.to_dto_func(m, *args, **kwargs)
|
|
31
|
+
for m in model
|
|
32
|
+
]
|
|
33
|
+
return self.to_dto_func(model, *args, **kwargs)
|
|
34
|
+
|
|
35
|
+
def dto_helper[ModelType, R](to_dto_func: Callable[[ModelType], R]) -> DtoHelper[ModelType, R]:
|
|
36
|
+
return DtoHelper(to_dto_func)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Why This Exists
|
|
40
|
+
|
|
41
|
+
- It removes repeated `[to_dto(item) for item in items]` boilerplate.
|
|
42
|
+
- It keeps the mapper call site the same whether the caller has one entity or many.
|
|
43
|
+
- It pairs naturally with repository and service code that already works with ORM models.
|
|
44
|
+
|
|
45
|
+
### Usage
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from your_app_domain.utils.dto_util import dto_helper
|
|
49
|
+
from your_app_domain.models.entities import User
|
|
50
|
+
from your_app_domain.models.dtos import UserDTO
|
|
51
|
+
|
|
52
|
+
# Define a function that converts a model to a DTO
|
|
53
|
+
@dto_helper
|
|
54
|
+
def user_to_dto(user: User) -> UserDTO:
|
|
55
|
+
return UserDTO(id=user.id, name=user.name)
|
|
56
|
+
|
|
57
|
+
# Convert a single user
|
|
58
|
+
dto = user_to_dto(user)
|
|
59
|
+
|
|
60
|
+
# Convert a list of users
|
|
61
|
+
dtos = user_to_dto([user1, user2, user3])
|
|
62
|
+
```
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
## Encrypted Column Template
|
|
2
|
+
|
|
3
|
+
Target path: `/packages/python/domain/src/{name_underscore}_domain/utils/encrypted_column.py`
|
|
4
|
+
|
|
5
|
+
Use this template when a SQLModel field must be stored encrypted at rest without changing the rest of the model or repository code.
|
|
6
|
+
|
|
7
|
+
Dependency: `uv add cryptography`
|
|
8
|
+
|
|
9
|
+
The first Python block is the generated file used by `rpr add template encrypted_column`.
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from cryptography.fernet import Fernet
|
|
16
|
+
from sqlalchemy import Column
|
|
17
|
+
from sqlalchemy.types import TypeDecorator
|
|
18
|
+
from sqlmodel import Field, SQLModel
|
|
19
|
+
from sqlmodel.sql.sqltypes import AutoString
|
|
20
|
+
|
|
21
|
+
def _get_fernet_key() -> bytes:
|
|
22
|
+
"""
|
|
23
|
+
Return the Fernet encryption key.
|
|
24
|
+
|
|
25
|
+
Replace this with your project's settings / secret manager lookup.
|
|
26
|
+
return b"your-fernet-key-here"
|
|
27
|
+
"""
|
|
28
|
+
# Important this is sample or temp code, the key should not come from env but from a secure secret manager, like GCP, Doppler, AWS Secrets Manager, etc.
|
|
29
|
+
import os
|
|
30
|
+
key = os.environ.get("FERNET_KEY")
|
|
31
|
+
if key is None:
|
|
32
|
+
raise RuntimeError("FERNET_KEY environment variable is not set")
|
|
33
|
+
return key.encode()
|
|
34
|
+
|
|
35
|
+
class EncryptedColumn(TypeDecorator):
|
|
36
|
+
"""
|
|
37
|
+
SQLAlchemy column type that transparently encrypts and decrypts values using Fernet symmetric encryption.
|
|
38
|
+
|
|
39
|
+
Stores the encrypted value as a string in the database.
|
|
40
|
+
Supports any JSON serializable Python value (str, dict, list, int, etc.).
|
|
41
|
+
|
|
42
|
+
Usage in a SQLModel:
|
|
43
|
+
|
|
44
|
+
class MyModel(SQLModel, table=True):
|
|
45
|
+
api_token: str | None = Field(
|
|
46
|
+
default=None,
|
|
47
|
+
sa_column=Column(EncryptedColumn())
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
impl = AutoString(length=1000)
|
|
52
|
+
cache_ok = True
|
|
53
|
+
|
|
54
|
+
def __init__(self, *args, **kwargs):
|
|
55
|
+
super().__init__(*args, **kwargs)
|
|
56
|
+
self._fernet = Fernet(_get_fernet_key())
|
|
57
|
+
|
|
58
|
+
def process_bind_param(self, value: Any, dialect) -> str | None:
|
|
59
|
+
"""
|
|
60
|
+
Encrypt the value before storing it in the database.
|
|
61
|
+
"""
|
|
62
|
+
if value is None:
|
|
63
|
+
return None
|
|
64
|
+
try:
|
|
65
|
+
json_value = json.dumps(value)
|
|
66
|
+
encrypted = self._fernet.encrypt(json_value.encode("utf-8"))
|
|
67
|
+
return encrypted.decode("utf-8")
|
|
68
|
+
except Exception as e:
|
|
69
|
+
raise ValueError(f"Error encrypting value: {e}")
|
|
70
|
+
|
|
71
|
+
def process_result_value(self, value: str | None, dialect) -> Any:
|
|
72
|
+
"""
|
|
73
|
+
Decrypt the value when loading it from the database.
|
|
74
|
+
"""
|
|
75
|
+
if value is None:
|
|
76
|
+
return None
|
|
77
|
+
try:
|
|
78
|
+
decrypted_value = self._fernet.decrypt(value.encode("utf-8"))
|
|
79
|
+
return json.loads(decrypted_value.decode("utf-8"))
|
|
80
|
+
except Exception as e:
|
|
81
|
+
# log error
|
|
82
|
+
# Return None if decryption fails, we don't want the app to crash unless the consumer needs it to crash
|
|
83
|
+
return None
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Security Notes
|
|
87
|
+
|
|
88
|
+
- Replace `_get_fernet_key()` with the project's real secret-loading path before using this in production.
|
|
89
|
+
- Prefer a secrets manager over environment variables for long-lived deployments.
|
|
90
|
+
- Decide whether decryption failures should return `None`, be logged, or raise loudly based on the sensitivity of the consuming workflow.
|
|
91
|
+
|
|
92
|
+
## Key Generation
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from cryptography.fernet import Fernet
|
|
96
|
+
key = Fernet.generate_key().decode()
|
|
97
|
+
# Store in secrets manager
|
|
98
|
+
```
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
## Mapper Utility Template
|
|
2
|
+
|
|
3
|
+
Target path: `/packages/python/domain/src/{name_underscore}_domain/utils/mapper_util.py`
|
|
4
|
+
|
|
5
|
+
Use this template when repository methods should support both raw ORM access and deferred DTO conversion through `.to_dto()`.
|
|
6
|
+
|
|
7
|
+
The first Python block is the generated file used by `rpr add template mapper_util`.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
import inspect
|
|
12
|
+
from functools import wraps
|
|
13
|
+
from typing import (
|
|
14
|
+
TYPE_CHECKING,
|
|
15
|
+
Any,
|
|
16
|
+
Awaitable,
|
|
17
|
+
Callable,
|
|
18
|
+
Concatenate,
|
|
19
|
+
cast,
|
|
20
|
+
overload
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from your_app_domain.repos import BaseRepo # Another special file
|
|
25
|
+
|
|
26
|
+
class Mapper[RepoResult, DtoResult]:
|
|
27
|
+
"""
|
|
28
|
+
Wraps a repository coroutine and provides deferred DTO conversion.
|
|
29
|
+
- Await the mapper directly to get the raw ORM result.
|
|
30
|
+
- Call `.to_dto()` on the mapper to get the converted DTO result.
|
|
31
|
+
"""
|
|
32
|
+
def __init__(self, coro: Awaitable[RepoResult], dto_func: Callable[[RepoResult], DtoResult]):
|
|
33
|
+
self._coro = coro
|
|
34
|
+
self._dto_func = dto_func
|
|
35
|
+
|
|
36
|
+
def __await__(self):
|
|
37
|
+
return self._coro.__await__()
|
|
38
|
+
|
|
39
|
+
async def to_dto(self) -> DtoResult | None:
|
|
40
|
+
obj = await self._coro
|
|
41
|
+
if obj is None:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
if isinstance(obj, tuple):
|
|
45
|
+
actual_func = inspect.unwrap(self._dto_func)
|
|
46
|
+
sig = inspect.signature(actual_func)
|
|
47
|
+
params = list(sig.parameters.values())
|
|
48
|
+
if len(params) == 1:
|
|
49
|
+
return self._dto_func(obj)
|
|
50
|
+
else:
|
|
51
|
+
return self._dto_func(*obj)
|
|
52
|
+
|
|
53
|
+
return self._dto_func(obj)
|
|
54
|
+
|
|
55
|
+
@overload
|
|
56
|
+
def mapper[Repo, RepoResult, **P](
|
|
57
|
+
method: Callable[Concatenate[Repo, P], Awaitable[RepoResult]],
|
|
58
|
+
) -> Callable[Concatenate[Repo, P], Mapper[RepoResult, Any]]: ...
|
|
59
|
+
|
|
60
|
+
@overload
|
|
61
|
+
def mapper[Repo, RepoResult, DtoResult, **P](
|
|
62
|
+
*,
|
|
63
|
+
dto_func: Callable[[RepoResult], DtoResult],
|
|
64
|
+
) -> Callable[
|
|
65
|
+
[Callable[Concatenate[Repo, P], Awaitable[RepoResult]]],
|
|
66
|
+
Callable[Concatenate[Repo, P], Mapper[RepoResult, DtoResult]],
|
|
67
|
+
]: ...
|
|
68
|
+
|
|
69
|
+
def mapper(
|
|
70
|
+
method: Callable[..., Awaitable[Any]] | None = None,
|
|
71
|
+
*,
|
|
72
|
+
dto_func: Callable[[Any], Any] | None = None,
|
|
73
|
+
):
|
|
74
|
+
"""
|
|
75
|
+
Decorator for repository methods that returns a Mapper instance.
|
|
76
|
+
|
|
77
|
+
The Mapper can then be:
|
|
78
|
+
- awaited directly -> returns the raw ORM result
|
|
79
|
+
- call .to_dto() -> returns the converted DTO result
|
|
80
|
+
|
|
81
|
+
use @mapper alone to use the repository's default dto_func
|
|
82
|
+
use @mapper(dto_func=your_custom_dto_func) to override the default dto_func for that specific method.
|
|
83
|
+
|
|
84
|
+
Examples:
|
|
85
|
+
@mapper
|
|
86
|
+
async def get_by_id(self, id: str) -> TEntity | None:
|
|
87
|
+
|
|
88
|
+
@mapper(dto_func=custom_dto_func)
|
|
89
|
+
async def get_by_id_custom(self, id: str) -> dict[str, TEntity] | None:
|
|
90
|
+
"""
|
|
91
|
+
def decorate_with_default[Repo, RepoResult, **P](
|
|
92
|
+
func: Callable[Concatenate[Repo, P], Awaitable[RepoResult]],
|
|
93
|
+
) -> Callable[Concatenate[Repo, P], Mapper[RepoResult, Any]]:
|
|
94
|
+
@wraps(func)
|
|
95
|
+
def wrapper(
|
|
96
|
+
self: BaseRepo, *args: P.args, **kwargs: P.kwargs
|
|
97
|
+
) -> Mapper[RepoResult, Any]:
|
|
98
|
+
return Mapper(func(self, *args, **kwargs), self.dto_func)
|
|
99
|
+
return wrapper
|
|
100
|
+
|
|
101
|
+
if method is not None:
|
|
102
|
+
return decorate_with_default(method)
|
|
103
|
+
|
|
104
|
+
def decorator[Repo, RepoResult, DtoResult, **P](
|
|
105
|
+
func: Callable[Concatenate[Repo, P], Awaitable[RepoResult]],
|
|
106
|
+
) -> Callable[Concatenate[Repo, P], Mapper[RepoResult, DtoResult]]:
|
|
107
|
+
if dto_func is None:
|
|
108
|
+
raise ValueError("dto_func must be provided when using @mapper(...)")
|
|
109
|
+
typed_dto_func = cast(Callable[[RepoResult], DtoResult], dto_func)
|
|
110
|
+
|
|
111
|
+
@wraps(func)
|
|
112
|
+
def wrapper(
|
|
113
|
+
self: BaseRepo, *args: P.args, **kwargs: P.kwargs
|
|
114
|
+
) -> Mapper[RepoResult, DtoResult]:
|
|
115
|
+
return Mapper(func(self, *args, **kwargs), typed_dto_func)
|
|
116
|
+
|
|
117
|
+
return wrapper
|
|
118
|
+
|
|
119
|
+
return decorator
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Usage
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from your_app_domain.repos import BaseRepo
|
|
126
|
+
from your_app_domain.utils import mapper
|
|
127
|
+
from your_app_domain.models.entities import SomeEntity
|
|
128
|
+
from your_app_domain.models.dtos import some_entity_to_dto, custom_dto_func
|
|
129
|
+
|
|
130
|
+
class MyRepo(BaseRepo[SomeEntity]):
|
|
131
|
+
def __init__(self, engine):
|
|
132
|
+
super().__init__(SomeEntity, engine, some_entity_to_dto) # Pass the default DTO conversion function
|
|
133
|
+
|
|
134
|
+
@mapper
|
|
135
|
+
async def get_by_id(self, id: str) -> SomeEntity | None:
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
@mapper(dto_func=custom_dto_func)
|
|
139
|
+
async def get_by_id_custom(self, id: str) -> dict[str, SomeEntity] | None:
|
|
140
|
+
...
|
|
141
|
+
|
|
142
|
+
# Usage
|
|
143
|
+
repo = MyRepo(engine)
|
|
144
|
+
result = await repo.get_by_id("some_id") # Returns raw ORM result
|
|
145
|
+
dto_result = await repo.get_by_id("some_id").to_dto() # Returns DTO result
|
|
146
|
+
custom_dto_result = await repo.get_by_id_custom("some_id").to_dto() # Returns custom DTO result
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## What This Template Solves
|
|
150
|
+
|
|
151
|
+
- Repository callers can choose between awaiting the raw ORM result and calling `.to_dto()` when they need mapped DTOs.
|
|
152
|
+
- The repository keeps a default mapper while still allowing per-method overrides.
|
|
153
|
+
- Tuple results are supported for methods that return composite values into a mapper function.
|
|
154
|
+
|
|
155
|
+
## Usage Notes
|
|
156
|
+
|
|
157
|
+
- Use `@mapper` on repository methods that should expose `.to_dto()`.
|
|
158
|
+
- Use `@mapper(dto_func=...)` only when a method needs a different mapping strategy than the repository default.
|
|
159
|
+
- Pair this with `dto_util` when the DTO mapper itself should support both single values and lists.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
## Partial Update Utility Template
|
|
2
|
+
|
|
3
|
+
Target path: `/packages/python/domain/src/{name_underscore}_domain/utils/partial_update.py`
|
|
4
|
+
|
|
5
|
+
Use this helper when a PATCH-style DTO should update only the fields that were explicitly provided by the caller.
|
|
6
|
+
|
|
7
|
+
The first Python block is the generated file used by `rpr add template partial_update`.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
from typing import TypeVar
|
|
12
|
+
from my_app_domain.models.entities import BaseEntity
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
EntityT = TypeVar("EntityT", bound="BaseEntity")
|
|
16
|
+
EXCLUDED_FIELDS = frozenset({'id', 'created_at', 'updated_at', 'created_by', 'updated_by'})
|
|
17
|
+
|
|
18
|
+
def apply_partial_update(entity: EntityT, partial_dto: BaseModel, *, field_mapping: dict[str, str] | None = None) -> EntityT:
|
|
19
|
+
"""
|
|
20
|
+
Apply explicitly set fields from a partial update DTO onto the current Entity.
|
|
21
|
+
Iterates over model_fields_set (only fields explicitly included in the request payload),
|
|
22
|
+
excluding id, created_at, and updated_at, and sets the corresponding attribute on current_entity.
|
|
23
|
+
Fields absent from the payload are left unchanged.
|
|
24
|
+
|
|
25
|
+
When field names differ between the DTO and the Entity, provide a field_mapping dict mapping DTO field name => entity attribute name.
|
|
26
|
+
entity: The ORM entity to update.
|
|
27
|
+
partial_dto: The Pydantic model containing the fields to update.
|
|
28
|
+
field_mapping: Optional dictionary mapping DTO field names to entity attribute names.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The same entity instance, mutated in place.
|
|
32
|
+
"""
|
|
33
|
+
field_mapping = field_mapping or {}
|
|
34
|
+
|
|
35
|
+
for field_name in partial_dto.model_fields_set:
|
|
36
|
+
if field_name in EXCLUDED_FIELDS:
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
value = getattr(partial_dto, field_name)
|
|
40
|
+
target_attr = field_mapping.get(field_name, field_name)
|
|
41
|
+
setattr(entity, target_attr, value)
|
|
42
|
+
return entity
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Usage:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
# PATCH endpoint handler
|
|
49
|
+
async def update_user(id: str, body: UpdateUserDto) -> UserDto:
|
|
50
|
+
user = await repo.get_user_by_id(id)
|
|
51
|
+
user = apply_partial_update(user, body)
|
|
52
|
+
|
|
53
|
+
#with field names mismatch
|
|
54
|
+
apply_partial_update(entity, dto, field_mapping={"displayName": "name"})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Notes
|
|
58
|
+
|
|
59
|
+
- The helper mutates the existing entity in place and returns that same instance for convenience.
|
|
60
|
+
- `model_fields_set` ensures omitted fields are left untouched.
|
|
61
|
+
- Use `field_mapping` when DTO field names and entity attribute names differ.
|
|
File without changes
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class TemplateEntry:
|
|
8
|
+
id: str
|
|
9
|
+
description: str
|
|
10
|
+
source_filename: str
|
|
11
|
+
source_subdir: str
|
|
12
|
+
target_path: str
|
|
13
|
+
language: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
TEMPLATES: dict[str, TemplateEntry] = {
|
|
17
|
+
"mapper_util": TemplateEntry(
|
|
18
|
+
id="mapper_util",
|
|
19
|
+
description="Mapper decorator for repo methods with DTO conversion",
|
|
20
|
+
source_filename="mapper_util.md",
|
|
21
|
+
source_subdir="special_files",
|
|
22
|
+
target_path="packages/python/domain/src/{name_underscore}_domain/utils/mapper_util.py",
|
|
23
|
+
language="python",
|
|
24
|
+
),
|
|
25
|
+
"dto_util": TemplateEntry(
|
|
26
|
+
id="dto_util",
|
|
27
|
+
description="DtoHelper for single/list entity-to-DTO conversion",
|
|
28
|
+
source_filename="dto_util.md",
|
|
29
|
+
source_subdir="special_files",
|
|
30
|
+
target_path="packages/python/domain/src/{name_underscore}_domain/utils/dto_util.py",
|
|
31
|
+
language="python",
|
|
32
|
+
),
|
|
33
|
+
"partial_update": TemplateEntry(
|
|
34
|
+
id="partial_update",
|
|
35
|
+
description="apply_partial_update utility for PATCH endpoints",
|
|
36
|
+
source_filename="partial_update.md",
|
|
37
|
+
source_subdir="special_files",
|
|
38
|
+
target_path="packages/python/domain/src/{name_underscore}_domain/utils/partial_update.py",
|
|
39
|
+
language="python",
|
|
40
|
+
),
|
|
41
|
+
"encrypted_column": TemplateEntry(
|
|
42
|
+
id="encrypted_column",
|
|
43
|
+
description="SQLAlchemy EncryptedColumn TypeDecorator using Fernet",
|
|
44
|
+
source_filename="encrypted_column.md",
|
|
45
|
+
source_subdir="special_files",
|
|
46
|
+
target_path="packages/python/domain/src/{name_underscore}_domain/utils/encrypted_column.py",
|
|
47
|
+
language="python",
|
|
48
|
+
),
|
|
49
|
+
"domain_settings": TemplateEntry(
|
|
50
|
+
id="domain_settings",
|
|
51
|
+
description="Pydantic BaseSettings with .env loading and DB/HTTP fields",
|
|
52
|
+
source_filename="domain_settings.md",
|
|
53
|
+
source_subdir="special_files",
|
|
54
|
+
target_path="packages/python/domain/src/{name_underscore}_domain/config/settings.py",
|
|
55
|
+
language="python",
|
|
56
|
+
),
|
|
57
|
+
"domain_container": TemplateEntry(
|
|
58
|
+
id="domain_container",
|
|
59
|
+
description="Dependency Injector Container with db_engine and http_client providers",
|
|
60
|
+
source_filename="domain_container.md",
|
|
61
|
+
source_subdir="special_files",
|
|
62
|
+
target_path="packages/python/domain/src/{name_underscore}_domain/config/container.py",
|
|
63
|
+
language="python",
|
|
64
|
+
),
|
|
65
|
+
"sticky_navigation": TemplateEntry(
|
|
66
|
+
id="sticky_navigation",
|
|
67
|
+
description="useStateNavigation React hook for URL-based state",
|
|
68
|
+
source_filename="sticky-navigation.md",
|
|
69
|
+
source_subdir="js_special_files",
|
|
70
|
+
target_path="packages/node/{name}-ui/src/hooks/useStateNavigation.ts",
|
|
71
|
+
language="typescript",
|
|
72
|
+
),
|
|
73
|
+
"fetch_service": TemplateEntry(
|
|
74
|
+
id="fetch_service",
|
|
75
|
+
description="Centralized API service with auth, error handling, fetch",
|
|
76
|
+
source_filename="fetch.service.md",
|
|
77
|
+
source_subdir="js_special_files",
|
|
78
|
+
target_path="packages/node/{name}-ui/src/services/base/api.service.ts",
|
|
79
|
+
language="typescript",
|
|
80
|
+
),
|
|
81
|
+
}
|
rpr/ui/__init__.py
ADDED
|
File without changes
|
rpr/ui/console.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rpr.ui.theme import theme_manager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_console() -> Console:
|
|
10
|
+
return Console(theme=theme_manager.get_theme(), color_system="auto")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Shared console instance
|
|
14
|
+
# Note: In a real app, you might want to re-initialize this if the theme changes
|
|
15
|
+
console = get_console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def refresh_console():
|
|
19
|
+
"""Refresh the shared console instance with the current theme."""
|
|
20
|
+
global console
|
|
21
|
+
console = get_console()
|
|
22
|
+
|
|
23
|
+
for module_name in (
|
|
24
|
+
"rpr.ui.markdown",
|
|
25
|
+
"rpr.ui.renderers",
|
|
26
|
+
"rpr.context",
|
|
27
|
+
"rpr.commands.chat",
|
|
28
|
+
"rpr.commands.settings",
|
|
29
|
+
):
|
|
30
|
+
module = sys.modules.get(module_name)
|
|
31
|
+
if module is not None:
|
|
32
|
+
setattr(module, "console", console)
|