Initial 2

This commit is contained in:
Eden Kirin
2025-10-31 14:03:41 +01:00
parent da97fada0e
commit f8edfc0fc1
6 changed files with 246 additions and 4 deletions

View File

@ -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`.

View File

@ -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)

View File

@ -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

View 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
View 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))