Initial 2
This commit is contained in:
58
CLAUDE.md
58
CLAUDE.md
@ -11,9 +11,51 @@ Application connects to postgres database, inspects targeted table and creates:
|
|||||||
- repository
|
- repository
|
||||||
- manager
|
- manager
|
||||||
- factory
|
- factory
|
||||||
|
- mapper
|
||||||
|
|
||||||
|
## Generated files
|
||||||
|
|
||||||
Generated source files are targeted for Python 3.13 using SQLAlchemy.
|
Generated source files are targeted for Python 3.13 using SQLAlchemy.
|
||||||
|
|
||||||
|
All example files import from televend_core.databases.*, our a specific in-house framework.
|
||||||
|
Pluralization should be not just handling the *s* suffix, but using proper english.
|
||||||
|
|
||||||
|
### Foreign key relationships
|
||||||
|
Create imports following naming rules. For example, for author_info table, create import as `from televend_core.databases.televend_repositories.author_info.model import AuthorInfo`
|
||||||
|
|
||||||
|
Strip `_id` suffix from field name and use proper entity name.
|
||||||
|
|
||||||
|
### PostgreSQL enum types
|
||||||
|
|
||||||
|
Query PostgreSQL system catalogs to get enum values and place it in `enum.py` output file. Also, use those values in factory, as in example.
|
||||||
|
|
||||||
|
### Reverse relationships
|
||||||
|
|
||||||
|
Skip reverse relationships entirely. They will be added manually later when files are generated.
|
||||||
|
|
||||||
|
### Filter field selection
|
||||||
|
|
||||||
|
Available filter operators are listed in `./example/filters.py`. Generate filters using following rules:
|
||||||
|
- All boolean fields: create filter with EQ operator
|
||||||
|
- All ID fields (primary key and foreign keys): create filters with both EQ and IN operators. For IN operator, use plural field name (e.g., `ids: list[int]` for `id` field, `machine_ids: list[int]` for `machine_id` field)
|
||||||
|
- All text fields (varchar, text): create filter with EQ operator only
|
||||||
|
|
||||||
|
### Mapper file
|
||||||
|
|
||||||
|
Generate mapper file snippet without import statements. Map all foreign keys as relationships. User will manually remove relationships that are not important later.
|
||||||
|
|
||||||
|
### Load options
|
||||||
|
|
||||||
|
Load options should include all relationships defined in the mapper file.
|
||||||
|
|
||||||
|
### Factory special cases
|
||||||
|
|
||||||
|
Ignore factory special cases (e.g., using `.user_ptr_id` instead of `.id`). Use standard `.id` for all foreign key references.
|
||||||
|
|
||||||
|
### Missing fields in model
|
||||||
|
|
||||||
|
Include all fields from database table in the generated model, even if missing in example.
|
||||||
|
|
||||||
## Input parameters
|
## Input parameters
|
||||||
|
|
||||||
Application have following command line parameters:
|
Application have following command line parameters:
|
||||||
@ -27,15 +69,29 @@ Application have following command line parameters:
|
|||||||
- optional output directory
|
- optional output directory
|
||||||
- optional entity name
|
- optional entity name
|
||||||
|
|
||||||
When application starts, it will offer users to enter missing parameters from command line, using defaults as listed above. Save entered values in `~/.config/entity-maker` and use it for the next time.
|
When application starts, it will offer users to enter missing parameters from command line, using defaults as listed above. Save entered values in `~/.config/entity-maker.toml` and use it for the next time.
|
||||||
|
|
||||||
|
### CLI prompts behavior
|
||||||
|
|
||||||
|
- Display current value from config file (if exists)
|
||||||
|
- Allow pressing Enter to accept default or stored value
|
||||||
|
- Validate inputs:
|
||||||
|
- Port must be numeric
|
||||||
|
- Database name is required (no default)
|
||||||
|
- Table name is required (no default)
|
||||||
|
- Output directory must exist or be creatable
|
||||||
|
|
||||||
Generated files are placed in specificied output directory, in subdirectory named after table name, but in sigular. Check naming section for details.
|
Generated files are placed in specificied output directory, in subdirectory named after table name, but in sigular. Check naming section for details.
|
||||||
|
|
||||||
|
Optional entity name should override the automatically generated entity name.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Example sql table structure is located in `./example/cashbag_conforms.sql`.
|
Example sql table structure is located in `./example/cashbag_conforms.sql`.
|
||||||
Example output is located in `./example/cashbag_conform`.
|
Example output is located in `./example/cashbag_conform`.
|
||||||
|
|
||||||
|
Note: In the example SQL, the `route_id` field is deprecated and not used in generated files. This is intentional and should not be used as reference.
|
||||||
|
|
||||||
## Naming example
|
## Naming example
|
||||||
|
|
||||||
Table name: `cashbag_conforms`.
|
Table name: `cashbag_conforms`.
|
||||||
|
|||||||
@ -1,6 +1,16 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from televend_core.databases.base_filter import BaseFilter
|
from televend_core.databases.base_filter import BaseFilter
|
||||||
from televend_core.databases.common.filters.filters import EQ, IN, filterfield
|
from televend_core.databases.common.filters.filters import (
|
||||||
|
EQ,
|
||||||
|
GT,
|
||||||
|
IN,
|
||||||
|
IS_NOT_NULL,
|
||||||
|
LT,
|
||||||
|
filterfield,
|
||||||
|
)
|
||||||
from televend_core.databases.televend_repositories.cashbag_conform.model import CashBagConform
|
from televend_core.databases.televend_repositories.cashbag_conform.model import CashBagConform
|
||||||
|
from televend_core.databases.televend_repositories.machine.model import Machine
|
||||||
|
|
||||||
|
|
||||||
class CashBagConformFilter(BaseFilter):
|
class CashBagConformFilter(BaseFilter):
|
||||||
@ -9,6 +19,18 @@ class CashBagConformFilter(BaseFilter):
|
|||||||
alive: bool | None = filterfield(operator=EQ, default=True)
|
alive: bool | None = filterfield(operator=EQ, default=True)
|
||||||
ids: list[int] | None = filterfield(field="id", operator=IN)
|
ids: list[int] | None = filterfield(field="id", operator=IN)
|
||||||
machine_ids: list[int] | None = filterfield(field="machine_id", operator=IN)
|
machine_ids: list[int] | None = filterfield(field="machine_id", operator=IN)
|
||||||
cashflow_collections_ids: list[int] | None = filterfield(
|
company_id: int | None = filterfield(
|
||||||
field="cashflow_collections_id", operator=IN
|
field="owner_id",
|
||||||
|
operator=EQ,
|
||||||
|
joins=[Machine],
|
||||||
|
filter_on_model_cls=Machine,
|
||||||
)
|
)
|
||||||
|
cashflow_collections_ids: list[int] | None = filterfield(
|
||||||
|
field="cashflow_collection_id", operator=IN
|
||||||
|
)
|
||||||
|
is_assigned_to_collection: bool | None = filterfield(
|
||||||
|
field="cashflow_collection_id", operator=IS_NOT_NULL
|
||||||
|
)
|
||||||
|
is_counted: bool | None = filterfield(field="count_timestamp", operator=IS_NOT_NULL)
|
||||||
|
count_timestamp_gt: datetime | None = filterfield(field="count_timestamp", operator=GT)
|
||||||
|
count_timestamp_lt: datetime | None = filterfield(field="count_timestamp", operator=LT)
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
|
from sqlalchemy.orm import Query
|
||||||
|
|
||||||
from televend_core.databases.base_load_options import LoadOptions
|
from televend_core.databases.base_load_options import LoadOptions
|
||||||
from televend_core.databases.common.load_options import joinload
|
from televend_core.databases.common.load_options import joinload
|
||||||
from televend_core.databases.televend_repositories.cashbag_conform.model import CashBagConform
|
from televend_core.databases.televend_repositories.cashbag_conform.model import CashBagConform
|
||||||
|
from televend_core.databases.televend_repositories.custom_user.model import CustomUser
|
||||||
|
|
||||||
|
|
||||||
class CashBagConformLoadOptions(LoadOptions):
|
class CashBagConformLoadOptions(LoadOptions):
|
||||||
@ -10,3 +13,16 @@ class CashBagConformLoadOptions(LoadOptions):
|
|||||||
load_machine: bool = joinload(relations=["machine"])
|
load_machine: bool = joinload(relations=["machine"])
|
||||||
load_cashbag: bool = joinload(relations=["cashbag"])
|
load_cashbag: bool = joinload(relations=["cashbag"])
|
||||||
load_denominations: bool = joinload(relations=["denominations"])
|
load_denominations: bool = joinload(relations=["denominations"])
|
||||||
|
|
||||||
|
load_count_user_auth_user: bool = False
|
||||||
|
|
||||||
|
def _apply_custom_joins(self, query: Query) -> Query:
|
||||||
|
if self.load_count_user_auth_user:
|
||||||
|
query = self._add_joins_to_options(
|
||||||
|
query=query,
|
||||||
|
fields=[
|
||||||
|
CashBagConform.count_user,
|
||||||
|
CustomUser.auth_user,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return query
|
||||||
|
|||||||
26
example/cashbag_conform/mapper.py
Normal file
26
example/cashbag_conform/mapper.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
mapper_registry.map_imperatively(
|
||||||
|
class_=CashBagConform,
|
||||||
|
local_table=CASHBAG_CONFORM_TABLE,
|
||||||
|
properties={
|
||||||
|
"cashflow_collection": relationship(
|
||||||
|
CashFlowCollection, lazy=relationship_loading_strategy.value
|
||||||
|
),
|
||||||
|
"cashbag": relationship(CashBag, lazy=relationship_loading_strategy.value),
|
||||||
|
"machine": relationship(Machine, lazy=relationship_loading_strategy.value),
|
||||||
|
"count_user": relationship(
|
||||||
|
CustomUser,
|
||||||
|
lazy=relationship_loading_strategy.value,
|
||||||
|
foreign_keys=CASHBAG_CONFORM_TABLE.columns.count_user_id,
|
||||||
|
),
|
||||||
|
"collect_user": relationship(
|
||||||
|
CustomUser,
|
||||||
|
lazy=relationship_loading_strategy.value,
|
||||||
|
foreign_keys=CASHBAG_CONFORM_TABLE.columns.collect_user_id,
|
||||||
|
),
|
||||||
|
"denominations": relationship(
|
||||||
|
CashBagConformDenomination,
|
||||||
|
back_populates="cashbag_conform",
|
||||||
|
lazy=relationship_loading_strategy.value,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
122
example/filters.py
Normal file
122
example/filters.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
class FieldOperator(ABC):
|
||||||
|
model_cls: Type[Base]
|
||||||
|
|
||||||
|
def __init__(self, model_cls: Type[Base]) -> None:
|
||||||
|
self.model_cls = model_cls
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def apply_query(self, field: str, value: Any, query: Query) -> Query: ...
|
||||||
|
|
||||||
|
def to_query(self, field: str, value: Any, query: Query) -> Query:
|
||||||
|
return self.apply_query(field, value, query)
|
||||||
|
|
||||||
|
|
||||||
|
class EQ(FieldOperator):
|
||||||
|
"""ColumnOperators.__eq__() (Python “==” operator)"""
|
||||||
|
|
||||||
|
def apply_query(self, field: str, value: Any, query: Query) -> Query:
|
||||||
|
return query.where(getattr(self.model_cls, field) == value)
|
||||||
|
|
||||||
|
|
||||||
|
class NOT_EQ(FieldOperator):
|
||||||
|
"""ColumnOperators.__ne__() (Python “!=” operator)"""
|
||||||
|
|
||||||
|
def apply_query(self, field: str, value: Any, query: Query) -> Query:
|
||||||
|
return query.where(getattr(self.model_cls, field) != value)
|
||||||
|
|
||||||
|
|
||||||
|
class GT(FieldOperator):
|
||||||
|
"""ColumnOperators.__gt__() (Python “>” operator)"""
|
||||||
|
|
||||||
|
def apply_query(self, field: str, value: Comparable, query: Query) -> Query:
|
||||||
|
return query.where(getattr(self.model_cls, field) > value)
|
||||||
|
|
||||||
|
|
||||||
|
class GE(FieldOperator):
|
||||||
|
"""ColumnOperators.__ge__() (Python “>=” operator)"""
|
||||||
|
|
||||||
|
def apply_query(self, field: str, value: Comparable, query: Query) -> Query:
|
||||||
|
return query.where(getattr(self.model_cls, field) >= value)
|
||||||
|
|
||||||
|
|
||||||
|
class LT(FieldOperator):
|
||||||
|
"""ColumnOperators.__lt__() (Python “<” operator)"""
|
||||||
|
|
||||||
|
def apply_query(self, field: str, value: Comparable, query: Query) -> Query:
|
||||||
|
return query.where(getattr(self.model_cls, field) < value)
|
||||||
|
|
||||||
|
|
||||||
|
class LE(FieldOperator):
|
||||||
|
"""ColumnOperators.__le__() (Python “<=” operator)"""
|
||||||
|
|
||||||
|
def apply_query(self, field: str, value: Comparable, query: Query) -> Query:
|
||||||
|
return query.where(getattr(self.model_cls, field) <= value)
|
||||||
|
|
||||||
|
|
||||||
|
class IN(FieldOperator):
|
||||||
|
"""IN is available most typically by passing a list of values to the ColumnOperators.in_() method"""
|
||||||
|
|
||||||
|
def apply_query(self, field: str, value: Iterable[Any], query: Query) -> Query:
|
||||||
|
return query.where(getattr(self.model_cls, field).in_(value))
|
||||||
|
|
||||||
|
|
||||||
|
class NOT_IN(FieldOperator):
|
||||||
|
"""NOT IN is available via the ColumnOperators.not_in() operator"""
|
||||||
|
|
||||||
|
def apply_query(self, field: str, value: Iterable[Any], query: Query) -> Query:
|
||||||
|
return query.where(~getattr(self.model_cls, field).in_(value))
|
||||||
|
|
||||||
|
|
||||||
|
class LIKE(FieldOperator):
|
||||||
|
"""ColumnOperators.like(). Expects string value."""
|
||||||
|
|
||||||
|
def apply_query(self, field: str, value: str, query: Query) -> Query:
|
||||||
|
return query.where(getattr(self.model_cls, field).like(f"%{value}%"))
|
||||||
|
|
||||||
|
|
||||||
|
class ILIKE(FieldOperator):
|
||||||
|
"""ColumnOperators.ilike(). Expects string value."""
|
||||||
|
|
||||||
|
def apply_query(self, field: str, value: str, query: Query) -> Query:
|
||||||
|
return query.where(getattr(self.model_cls, field).ilike(f"%{value}%"))
|
||||||
|
|
||||||
|
|
||||||
|
class IS_NULL(FieldOperator):
|
||||||
|
"""Test value for NULL / NOT NULL"""
|
||||||
|
|
||||||
|
def apply_query(self, field: str, value: bool, query: Query) -> Query:
|
||||||
|
if value:
|
||||||
|
return query.where(getattr(self.model_cls, field).is_(None))
|
||||||
|
else:
|
||||||
|
return query.where(getattr(self.model_cls, field).is_not(None))
|
||||||
|
|
||||||
|
|
||||||
|
class IS_NOT_NULL(FieldOperator):
|
||||||
|
"""Test value for NULL / NOT NULL"""
|
||||||
|
|
||||||
|
def apply_query(self, field: str, value: bool, query: Query) -> Query:
|
||||||
|
if value:
|
||||||
|
return query.where(getattr(self.model_cls, field).is_not(None))
|
||||||
|
else:
|
||||||
|
return query.where(getattr(self.model_cls, field).is_(None))
|
||||||
|
|
||||||
|
|
||||||
|
class IEXACT(FieldOperator):
|
||||||
|
"""ColumnOperators.__eq__() over strings but comparison is case-insensitive. Expects string value."""
|
||||||
|
|
||||||
|
def apply_query(self, field: str, value: str, query: Query) -> Query:
|
||||||
|
return query.where(func.lower(getattr(self.model_cls, field)) == value.lower())
|
||||||
|
|
||||||
|
|
||||||
|
class REGEX(FieldOperator):
|
||||||
|
"""Case-sensitive regex operator. Expects string regex pattern."""
|
||||||
|
|
||||||
|
def apply_query(self, field: str, value: str, query: Query) -> Query:
|
||||||
|
return query.where(getattr(self.model_cls, field).op("~")(value))
|
||||||
|
|
||||||
|
|
||||||
|
class IREGEX(FieldOperator):
|
||||||
|
"""Case-insensitive regex operator. Expects string regex pattern."""
|
||||||
|
|
||||||
|
def apply_query(self, field: str, value: str, query: Query) -> Query:
|
||||||
|
return query.where(getattr(self.model_cls, field).op("~*")(value))
|
||||||
Reference in New Issue
Block a user