From f8edfc0fc1cce73c5d4890faa6d6635aa9199fe9 Mon Sep 17 00:00:00 2001 From: Eden Kirin Date: Fri, 31 Oct 2025 14:03:41 +0100 Subject: [PATCH] Initial 2 --- CLAUDE.md | 58 ++++++++- example/cashbag_conform/filter.py | 28 +++- example/cashbag_conform/load_options.py | 16 +++ example/cashbag_conform/mapper.py | 26 ++++ ...hbag_confirms.sql => cashbag_conforms.sql} | 0 example/filters.py | 122 ++++++++++++++++++ 6 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 example/cashbag_conform/mapper.py rename example/{cashbag_confirms.sql => cashbag_conforms.sql} (100%) create mode 100644 example/filters.py diff --git a/CLAUDE.md b/CLAUDE.md index 5ebfe16..3793fdb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,9 +11,51 @@ Application connects to postgres database, inspects targeted table and creates: - repository - manager - factory +- mapper + +## Generated files 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 Application have following command line parameters: @@ -27,15 +69,29 @@ Application have following command line parameters: - optional output directory - 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. +Optional entity name should override the automatically generated entity name. + ## Examples Example sql table structure is located in `./example/cashbag_conforms.sql`. 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 Table name: `cashbag_conforms`. diff --git a/example/cashbag_conform/filter.py b/example/cashbag_conform/filter.py index 03431dd..f1bc667 100644 --- a/example/cashbag_conform/filter.py +++ b/example/cashbag_conform/filter.py @@ -1,6 +1,16 @@ +from datetime import datetime + 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.machine.model import Machine class CashBagConformFilter(BaseFilter): @@ -9,6 +19,18 @@ class CashBagConformFilter(BaseFilter): alive: bool | None = filterfield(operator=EQ, default=True) ids: list[int] | None = filterfield(field="id", operator=IN) machine_ids: list[int] | None = filterfield(field="machine_id", operator=IN) - cashflow_collections_ids: list[int] | None = filterfield( - field="cashflow_collections_id", operator=IN + company_id: int | None = filterfield( + 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) diff --git a/example/cashbag_conform/load_options.py b/example/cashbag_conform/load_options.py index 84043c1..ea86870 100644 --- a/example/cashbag_conform/load_options.py +++ b/example/cashbag_conform/load_options.py @@ -1,6 +1,9 @@ +from sqlalchemy.orm import Query + from televend_core.databases.base_load_options import LoadOptions 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.custom_user.model import CustomUser class CashBagConformLoadOptions(LoadOptions): @@ -10,3 +13,16 @@ class CashBagConformLoadOptions(LoadOptions): load_machine: bool = joinload(relations=["machine"]) load_cashbag: bool = joinload(relations=["cashbag"]) 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 diff --git a/example/cashbag_conform/mapper.py b/example/cashbag_conform/mapper.py new file mode 100644 index 0000000..e5c2800 --- /dev/null +++ b/example/cashbag_conform/mapper.py @@ -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, + ), + }, + ) diff --git a/example/cashbag_confirms.sql b/example/cashbag_conforms.sql similarity index 100% rename from example/cashbag_confirms.sql rename to example/cashbag_conforms.sql diff --git a/example/filters.py b/example/filters.py new file mode 100644 index 0000000..d80f371 --- /dev/null +++ b/example/filters.py @@ -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))