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.
Files changed (97) hide show
  1. rpr/__init__.py +1 -0
  2. rpr/agent/__init__.py +29 -0
  3. rpr/agent/approval.py +19 -0
  4. rpr/agent/bootstrap.py +60 -0
  5. rpr/agent/client.py +115 -0
  6. rpr/agent/control.py +16 -0
  7. rpr/agent/mock.py +158 -0
  8. rpr/agent/runtime.py +291 -0
  9. rpr/agent/session.py +91 -0
  10. rpr/agent/tools/__init__.py +10 -0
  11. rpr/agent/tools/base.py +62 -0
  12. rpr/agent/tools/mutating.py +47 -0
  13. rpr/agent/tools/readonly.py +147 -0
  14. rpr/agent/tools/registry.py +30 -0
  15. rpr/application/__init__.py +1 -0
  16. rpr/application/catalog.py +427 -0
  17. rpr/application/chat_service.py +413 -0
  18. rpr/application/checks.py +49 -0
  19. rpr/application/cli_adapter.py +36 -0
  20. rpr/application/completer.py +163 -0
  21. rpr/application/conversation_service.py +92 -0
  22. rpr/application/prompt_service.py +85 -0
  23. rpr/application/selector.py +240 -0
  24. rpr/application/shell.py +543 -0
  25. rpr/checks/__init__.py +0 -0
  26. rpr/checks/base.py +13 -0
  27. rpr/checks/instructions.py +89 -0
  28. rpr/checks/packages.py +619 -0
  29. rpr/checks/workspace.py +170 -0
  30. rpr/cli.py +78 -0
  31. rpr/commands/__init__.py +0 -0
  32. rpr/commands/add.py +166 -0
  33. rpr/commands/chat.py +48 -0
  34. rpr/commands/check.py +53 -0
  35. rpr/commands/generate/__init__.py +0 -0
  36. rpr/commands/generate/api.py +228 -0
  37. rpr/commands/generate/domain.py +383 -0
  38. rpr/commands/generate/engine.py +148 -0
  39. rpr/commands/generate/storybook.py +442 -0
  40. rpr/commands/generate/ui.py +414 -0
  41. rpr/commands/init.py +822 -0
  42. rpr/commands/map.py +113 -0
  43. rpr/commands/settings.py +102 -0
  44. rpr/commands/sync.py +97 -0
  45. rpr/context.py +203 -0
  46. rpr/generators/__init__.py +0 -0
  47. rpr/generators/base.py +110 -0
  48. rpr/generators/claude.py +33 -0
  49. rpr/generators/copilot.py +36 -0
  50. rpr/generators/cursor.py +38 -0
  51. rpr/generators/gemini.py +33 -0
  52. rpr/map/__init__.py +0 -0
  53. rpr/map/architecture.py +495 -0
  54. rpr/map/chains.py +317 -0
  55. rpr/map/classifier.py +170 -0
  56. rpr/map/coverage.py +200 -0
  57. rpr/map/dependencies.py +243 -0
  58. rpr/map/extractor.py +223 -0
  59. rpr/map/graph.py +318 -0
  60. rpr/map/output.py +1030 -0
  61. rpr/map/responsibility.py +345 -0
  62. rpr/map/topology.py +327 -0
  63. rpr/map/walker.py +151 -0
  64. rpr/scaffolds/domain/base_entity.md +30 -0
  65. rpr/scaffolds/domain/base_repo.md +48 -0
  66. rpr/scaffolds/domain/container.md +76 -0
  67. rpr/scaffolds/domain/settings.md +57 -0
  68. rpr/scaffolds/instructions/all.instructions.md +50 -0
  69. rpr/scaffolds/instructions/api.instructions.md +42 -0
  70. rpr/scaffolds/instructions/domain.instructions.md +93 -0
  71. rpr/scaffolds/instructions/frontend.instructions.md +97 -0
  72. rpr/scaffolds/instructions/rust-engine.instructions.md +40 -0
  73. rpr/scaffolds/instructions/setup-guide.instructions.md +86 -0
  74. rpr/scaffolds/instructions/tooling-setup.instructions.md +97 -0
  75. rpr/scaffolds/instructions/tooling.instructions.md +42 -0
  76. rpr/scaffolds/js_special_files/fetch.service.md +222 -0
  77. rpr/scaffolds/js_special_files/sticky-navigation.md +164 -0
  78. rpr/scaffolds/special_files/domain_container.md +76 -0
  79. rpr/scaffolds/special_files/domain_settings.md +57 -0
  80. rpr/scaffolds/special_files/dto_util.md +62 -0
  81. rpr/scaffolds/special_files/encrypted_column.md +98 -0
  82. rpr/scaffolds/special_files/mapper_util.md +159 -0
  83. rpr/scaffolds/special_files/partial_update.md +61 -0
  84. rpr/templates/__init__.py +0 -0
  85. rpr/templates/registry.py +81 -0
  86. rpr/ui/__init__.py +0 -0
  87. rpr/ui/console.py +32 -0
  88. rpr/ui/markdown.py +59 -0
  89. rpr/ui/prompt_session.py +430 -0
  90. rpr/ui/renderers.py +167 -0
  91. rpr/ui/theme.py +286 -0
  92. rpr/workspace.py +131 -0
  93. rpr_cli-0.1.1.dist-info/METADATA +201 -0
  94. rpr_cli-0.1.1.dist-info/RECORD +97 -0
  95. rpr_cli-0.1.1.dist-info/WHEEL +4 -0
  96. rpr_cli-0.1.1.dist-info/entry_points.txt +2 -0
  97. 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)