python-general-be-lib 0.1.0__tar.gz

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 (34) hide show
  1. python_general_be_lib-0.1.0/LICENSE +21 -0
  2. python_general_be_lib-0.1.0/PKG-INFO +33 -0
  3. python_general_be_lib-0.1.0/README.md +1 -0
  4. python_general_be_lib-0.1.0/general/__init__.py +0 -0
  5. python_general_be_lib-0.1.0/general/exception/__init__.py +0 -0
  6. python_general_be_lib-0.1.0/general/exception/access_exceptions.py +11 -0
  7. python_general_be_lib-0.1.0/general/exception/crud_exceptions.py +37 -0
  8. python_general_be_lib-0.1.0/general/exception/exception_interface.py +10 -0
  9. python_general_be_lib-0.1.0/general/interface/base/__init__.py +2 -0
  10. python_general_be_lib-0.1.0/general/interface/base/base_model.py +27 -0
  11. python_general_be_lib-0.1.0/general/interface/base/declarative_base.py +57 -0
  12. python_general_be_lib-0.1.0/general/interface/metadata/__init__.py +0 -0
  13. python_general_be_lib-0.1.0/general/interface/metadata/crud_metadata.py +179 -0
  14. python_general_be_lib-0.1.0/general/interface/metadata/geom_metadata.py +88 -0
  15. python_general_be_lib-0.1.0/general/interface/repository/__init__.py +4 -0
  16. python_general_be_lib-0.1.0/general/interface/repository/crud_repository.py +272 -0
  17. python_general_be_lib-0.1.0/general/interface/repository/geometry_repository.py +103 -0
  18. python_general_be_lib-0.1.0/general/interface/repository/handler/__init__.py +2 -0
  19. python_general_be_lib-0.1.0/general/interface/repository/handler/base_handler.py +40 -0
  20. python_general_be_lib-0.1.0/general/interface/repository/handler/ilike_handler.py +38 -0
  21. python_general_be_lib-0.1.0/general/interface/repository/handler/interval_handler.py +34 -0
  22. python_general_be_lib-0.1.0/general/interface/repository/many_to_many_repository.py +101 -0
  23. python_general_be_lib-0.1.0/general/interface/repository/view_repository.py +57 -0
  24. python_general_be_lib-0.1.0/general/log_config.ini +21 -0
  25. python_general_be_lib-0.1.0/general/logger.py +54 -0
  26. python_general_be_lib-0.1.0/general/paginator_dto.py +23 -0
  27. python_general_be_lib-0.1.0/pyproject.toml +54 -0
  28. python_general_be_lib-0.1.0/python_general_be_lib.egg-info/PKG-INFO +33 -0
  29. python_general_be_lib-0.1.0/python_general_be_lib.egg-info/SOURCES.txt +32 -0
  30. python_general_be_lib-0.1.0/python_general_be_lib.egg-info/dependency_links.txt +1 -0
  31. python_general_be_lib-0.1.0/python_general_be_lib.egg-info/requires.txt +10 -0
  32. python_general_be_lib-0.1.0/python_general_be_lib.egg-info/top_level.txt +1 -0
  33. python_general_be_lib-0.1.0/setup.cfg +4 -0
  34. python_general_be_lib-0.1.0/setup.py +15 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arpes S.r.l.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-general-be-lib
3
+ Version: 0.1.0
4
+ Summary: General purpose backend library — SQLAlchemy CRUD/Geometry repositories, FastAPI exceptions, Pydantic base models, logger utilities.
5
+ Author-email: Andrea Di Placido <a.diplacido@arpes.it>, "Arpes S.r.l." <it.admin@arpes.it>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Arpes-IS/python-general-be-lib
8
+ Project-URL: Repository, https://github.com/Arpes-IS/python-general-be-lib
9
+ Keywords: backend,sqlalchemy,fastapi,pydantic,repository,crud,geoalchemy2
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Topic :: Database
17
+ Classifier: Topic :: Internet :: WWW/HTTP
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: fastapi>=0.100.0
22
+ Requires-Dist: pydantic>=2.0.0
23
+ Requires-Dist: sqlalchemy>=2.0.0
24
+ Requires-Dist: geoalchemy2>=0.14.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7; extra == "dev"
27
+ Requires-Dist: pytest-cov; extra == "dev"
28
+ Requires-Dist: build; extra == "dev"
29
+ Requires-Dist: twine; extra == "dev"
30
+ Dynamic: license-file
31
+ Dynamic: requires-python
32
+
33
+ # python-general-be-lib
@@ -0,0 +1 @@
1
+ # python-general-be-lib
File without changes
@@ -0,0 +1,11 @@
1
+ from .exception_interface import ExceptionInterface
2
+
3
+
4
+ class ForbiddenException(ExceptionInterface):
5
+ status_code = 403
6
+ default_message = "Risorsa non accessibile da questo utente"
7
+
8
+
9
+ class UnauthorizedException(ExceptionInterface):
10
+ status_code = 401
11
+ default_message = "Non autorizzato"
@@ -0,0 +1,37 @@
1
+ from .exception_interface import ExceptionInterface
2
+
3
+
4
+ class BadRequestException(ExceptionInterface):
5
+ status_code = 400
6
+ default_message = "Richiesta non corretta"
7
+
8
+
9
+ class EntityNotFoundException(ExceptionInterface):
10
+ status_code = 404
11
+ default_message = "Dato non trovato"
12
+
13
+
14
+ class ServiceUnavailableException(ExceptionInterface):
15
+ status_code = 503
16
+ default_message = "Servizio momentaneamente non disponibile, riprovare più tardi"
17
+
18
+
19
+ class UnprocessableEntityException(ExceptionInterface):
20
+ status_code = 422
21
+ default_message = "Entità non processabile"
22
+
23
+
24
+ class HasNoAttributeException(ExceptionInterface):
25
+ status_code = 400
26
+ default_message = "Richiesta non corretta"
27
+
28
+ def __init__(self, attr: str = ""):
29
+ if attr:
30
+ super().__init__(custom_message=f"Attributo {attr} non presente", )
31
+ else:
32
+ super().__init__(custom_message="Alcuni attributi non sono presenti per questa entità", )
33
+
34
+
35
+ class InternalServerException(ExceptionInterface):
36
+ status_code = 500
37
+ default_message = "Impossibile gestire la richiesta al momento"
@@ -0,0 +1,10 @@
1
+ from fastapi import HTTPException
2
+
3
+
4
+ class ExceptionInterface(HTTPException):
5
+ status_code: int
6
+ default_message: str
7
+
8
+ def __init__(self, custom_message: str = None):
9
+ detail = f"{self.default_message} - {custom_message}" if custom_message else self.default_message
10
+ super().__init__(status_code=self.status_code, detail=detail)
@@ -0,0 +1,2 @@
1
+ from .base_model import *
2
+ from .declarative_base import *
@@ -0,0 +1,27 @@
1
+ __all__ = ["ExtBaseModel", "CamelExtBaseModel"]
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+ from pydantic.alias_generators import to_camel
5
+
6
+
7
+ class ConfigModel(BaseModel):
8
+ model_config = ConfigDict(from_attributes=True, populate_by_name=True, arbitrary_types_allowed=True, validate_default=True)
9
+
10
+
11
+ class CamelConfigModel(ConfigModel):
12
+ model_config = ConfigDict(alias_generator=to_camel)
13
+
14
+ @classmethod
15
+ def get_attr_by_alias(cls, alias: str) -> str:
16
+ for name, field in cls.model_fields.items():
17
+ if field.alias == alias:
18
+ return name
19
+ return alias
20
+
21
+
22
+ class ExtBaseModel(ConfigModel):
23
+ pass
24
+
25
+
26
+ class CamelExtBaseModel(CamelConfigModel):
27
+ pass
@@ -0,0 +1,57 @@
1
+ __all__ = ["Base", "retrieve_mapper_entity", "retrieve_mapper_from_table"]
2
+
3
+ from typing import Optional, Any
4
+
5
+ from geoalchemy2 import Geometry, WKBElement
6
+ from pydantic import create_model
7
+ from sqlalchemy.inspection import inspect
8
+ from sqlalchemy.orm import Mapper, as_declarative
9
+
10
+
11
+ @as_declarative()
12
+ class Base:
13
+
14
+ @classmethod
15
+ def to_pydantic(cls, model_name: str = None, full_optional: bool = False, default_values: dict[str, Any] = None, **create_model_kwargs):
16
+ if not default_values:
17
+ default_values = dict()
18
+ mapper: Mapper = inspect(cls)
19
+ model_name = f"{cls.__name__}Model" if not model_name else model_name
20
+ fields = dict()
21
+ field_values = {col.name: None if full_optional else ... for col in mapper.columns}
22
+ field_values.update(default_values)
23
+ for col in mapper.columns:
24
+ field_name = col.name
25
+ if isinstance(col.type, Geometry):
26
+ field_type = Optional[WKBElement] if col.nullable or full_optional else WKBElement
27
+ else:
28
+ field_type = Optional[col.type.python_type] if col.nullable or full_optional else col.type.python_type
29
+ fields[field_name] = (field_type, field_values[field_name])
30
+ return create_model(model_name, **create_model_kwargs, **fields)
31
+
32
+ @classmethod
33
+ def columns(cls):
34
+ return cls.__table__.c
35
+
36
+ @classmethod
37
+ def columns_names(cls):
38
+ return [col.name for col in cls.__table__.c]
39
+
40
+ @classmethod
41
+ def primary_key(cls):
42
+ return cls.__table__.primary_key.columns.values()
43
+
44
+ def to_dict(self):
45
+ return {key: getattr(self, key) for key in self.__class__.columns_names()}
46
+
47
+
48
+ def retrieve_mapper_entity(entity_name: str):
49
+ for mapper in Base.__subclasses__():
50
+ if mapper.__name__ == entity_name:
51
+ return mapper
52
+
53
+
54
+ def retrieve_mapper_from_table(table_name: str):
55
+ for mapper in Base.__subclasses__():
56
+ if mapper.__tablename__ == table_name:
57
+ return mapper
@@ -0,0 +1,179 @@
1
+ __all__ = ["CrudMetadata"]
2
+
3
+ import logging
4
+ from typing import Optional, Any, Type, Iterable, Sequence
5
+
6
+ from pydantic import BaseModel
7
+ from sqlalchemy import MetaData, Engine, Column, Table, inspect, Connection, select, RowMapping, CursorResult, not_
8
+
9
+ from ...exception.crud_exceptions import HasNoAttributeException
10
+
11
+
12
+ class CrudMetadata:
13
+ __slots__ = "schema", "metadata", "engine", "engine_inspection"
14
+
15
+ @property
16
+ def open_connection(self):
17
+ return self.engine.connect()
18
+
19
+ def __init__(self, engine: Engine, schema_name: str = None, reflect: bool = True):
20
+ self.schema = schema_name
21
+ self.metadata = MetaData(schema_name) if schema_name else MetaData()
22
+ self.engine = engine
23
+ self.engine_inspection = inspect(self.engine)
24
+ if reflect:
25
+ self.metadata.reflect(bind=self.engine, schema=schema_name)
26
+
27
+ # ---------------------------
28
+
29
+ def _handle_exception(self, connection: Connection, e: Exception, to_raise: bool = True):
30
+ connection.close()
31
+ logging.error(e)
32
+ if to_raise:
33
+ raise e
34
+
35
+ def commit(self, connection: Connection):
36
+ try:
37
+ connection.commit()
38
+ except Exception as e:
39
+ self._handle_exception(connection, e)
40
+
41
+ def has_columns(self, *columns: Iterable[str], table: Table):
42
+ return all([column in [c.key for c in table.c] for column in columns])
43
+
44
+ def _select(self, table: Table, columns: list[str] = None):
45
+ try:
46
+ stmt = table.select() if not columns else select(*[table.c[column] for column in columns])
47
+ except:
48
+ raise HasNoAttributeException()
49
+ return stmt
50
+
51
+ def _return(self, connection: Connection, result: CursorResult | RowMapping | Sequence[RowMapping] | Sequence[dict] = None, keep_open: bool = False, commit: bool = True,
52
+ parsing_model: Type[BaseModel] | BaseModel = None):
53
+ if keep_open:
54
+ return result
55
+ else:
56
+ if commit:
57
+ self.commit(connection=connection)
58
+ result = result if parsing_model is None else [parsing_model(**r) for r in result]
59
+ connection.close()
60
+ return result
61
+
62
+ def create_table(self, name: str, columns: Iterable[Column], **kwargs):
63
+ table = Table(name=name, metadata=self.metadata, *columns, **kwargs)
64
+ table.create(self.engine)
65
+
66
+ def drop_table(self, name: str):
67
+ table = self.get_table(name=name)
68
+ table.drop(self.engine)
69
+ self.metadata.remove(table=table)
70
+
71
+ def find(self, from_table: str, connection: Connection = None, where: dict[str, Any] = None, columns: list[str] = None, limit: int = 0, parsing_model: Type[BaseModel] = None, **kwhere):
72
+ keep_open = False if connection is None else True
73
+ connection = self.open_connection if connection is None else connection
74
+ table = self.get_table(name=from_table)
75
+ result = self._find(table=table, connection=connection, where=where, columns=columns, limit=limit, **kwhere)
76
+ return self._return(connection=connection, result=result, keep_open=keep_open, commit=False, parsing_model=parsing_model)
77
+
78
+ def _find(self, table: Table, connection: Connection, where: dict[str, Any] = None, columns: list[str] = None, limit: int = 0, **kwhere):
79
+ if where is None:
80
+ where = kwhere if kwhere else {}
81
+ stmt = self._select(table=table, columns=columns)
82
+ stmt = stmt.where(*[self._eq_where(table.c[column], condition) for column, condition in where.items()])
83
+ stmt = self._add_limit_offset_condition(stmt, limit)
84
+ return self.execute(connection=connection, stmt=stmt, mapping=True)
85
+
86
+ def insert(self, from_table: str, connection: Connection = None, models: list[Type[BaseModel] | BaseModel] = None, values: list[dict[str, Any]] = None, returning: bool = True,
87
+ parsing_model: Type[BaseModel] | BaseModel = None):
88
+ keep_open = False if connection is None else True
89
+ connection = self.open_connection if connection is None else connection
90
+ table = self.get_table(name=from_table)
91
+ values_condition = values if values else []
92
+ values_condition.extend([model.model_dump(exclude_none=True) for model in models]) if models else values_condition
93
+ result = self._insert(table=table, connection=connection, values=values_condition, returning=returning)
94
+ return self._return(connection=connection, result=result, keep_open=keep_open, commit=True, parsing_model=parsing_model)
95
+
96
+ def _insert(self, table: Table, connection: Connection, values: list[dict[str, Any]] = None, returning: bool = True):
97
+ if values:
98
+ stmt = table.insert().values(values)
99
+ stmt = stmt if not returning else stmt.returning()
100
+ return self.execute(connection=connection, stmt=stmt, mapping=True)
101
+ else:
102
+ return []
103
+
104
+ def update(self, from_table: str, connection: Connection = None, where: dict[str, Any] = None, returning: bool = True, parsing_model: Type[BaseModel] | BaseModel = None, **kwargs):
105
+ keep_open = False if connection is None else True
106
+ connection = self.open_connection if connection is None else connection
107
+ table = self.get_table(name=from_table)
108
+ result = self._update(table=table, connection=connection, where=where, returning=returning, **kwargs)
109
+ return self._return(connection=connection, result=result, keep_open=keep_open, commit=True, parsing_model=parsing_model)
110
+
111
+ def _update(self, table: Table, connection: Connection, where: dict[str, Any] = None, returning: bool = True, **kwargs):
112
+ if where is None:
113
+ where = {}
114
+ stmt = table.update()
115
+ stmt = stmt.where(*[self._eq_where(table.c[column], condition) for column, condition in where.items()])
116
+ stmt = stmt.values(**kwargs)
117
+ stmt = stmt if not returning else stmt.returning()
118
+ return self.execute(connection=connection, stmt=stmt, mapping=True)
119
+
120
+ def delete(self, from_table: str, connection: Connection = None, where: dict[str, Any] = None, **kwhere):
121
+ keep_open = False if connection is None else True
122
+ connection = self.open_connection if connection is None else connection
123
+ table = self.get_table(name=from_table)
124
+ num_rows = self._delete(table=table, connection=connection, where=where, **kwhere)
125
+ if not num_rows or keep_open:
126
+ return num_rows
127
+ else:
128
+ self.commit(connection)
129
+ connection.close()
130
+ return num_rows
131
+
132
+ def _delete(self, table: Table, connection: Connection, where: dict[str, Any] = None, **kwhere):
133
+ if where is None:
134
+ where = kwhere if kwhere else {}
135
+ stmt = table.delete()
136
+ stmt = stmt.where(*[self._eq_where(table.c[column], condition) for column, condition in where.items()])
137
+ return self.execute(connection=connection, stmt=stmt).rowcount
138
+
139
+ # --------------
140
+
141
+ def execute(self, connection: Connection, stmt, mapping: bool = False):
142
+ try:
143
+ result = connection.execute(stmt)
144
+ except Exception as e:
145
+ self._handle_exception(connection, e)
146
+ else:
147
+ if mapping:
148
+ result = result.mappings().all()
149
+ return result
150
+
151
+ def _get_table(self, name: str):
152
+ if self.schema is not None:
153
+ table_name = name if name.startswith(f"{self.schema}.") else f"{self.schema}.{name}"
154
+ else:
155
+ table_name = name
156
+ return self.metadata.tables.get(table_name)
157
+
158
+ def get_table(self, name: str) -> Optional[Table]:
159
+ table = self._get_table(name=name)
160
+ if table is None:
161
+ table = Table(name, self.metadata, schema=self.schema, autoload_with=self.engine)
162
+ return table
163
+
164
+ def has_table(self, name: str):
165
+ return self.engine_inspection.has_table(name, self.schema)
166
+
167
+ def _add_limit_offset_condition(self, stmt, limit: int, page: int = None):
168
+ if limit and limit > 0:
169
+ stmt = stmt.limit(limit)
170
+ if page and page > 0:
171
+ offset = (page - 1) * limit
172
+ stmt = stmt.offset(offset)
173
+ return stmt
174
+
175
+ def _eq_where(self, col: Column, condition, neq: bool = False):
176
+ eq_where = col.in_(condition) if isinstance(condition, list) else col == condition if str(col.type) != "ARRAY" else condition == col.any_()
177
+ if neq:
178
+ not_(eq_where)
179
+ return eq_where
@@ -0,0 +1,88 @@
1
+ __all__ = ["GeomMetadata"]
2
+
3
+ from typing import Any, Type
4
+
5
+ from geoalchemy2 import WKBElement
6
+ from pydantic import BaseModel
7
+ from sqlalchemy import Connection, Table, Column
8
+
9
+ from .crud_metadata import CrudMetadata
10
+
11
+
12
+ class GeomMetadata(CrudMetadata):
13
+
14
+ def intersects(self, from_table: str, geom: WKBElement, polygon_column_name: str, polygon_column_srid: int, connection: Connection = None, tolerance: float = 0.0, columns: list[str] = None,
15
+ where: dict[str, Any] = None, not_where: dict[str, Any] = None, limit: int = 0, geom_srid: int = 4326, parsing_model: Type[BaseModel] | BaseModel = None, intersection: bool = False,
16
+ intersection_area: bool = False):
17
+
18
+ keep_open = False if connection is None else True
19
+ connection = self.open_connection if connection is None else connection
20
+ table = self.get_table(name=from_table)
21
+ polygon_column = table.c[polygon_column_name]
22
+ result = self._intersects(table=table, connection=connection, polygon_column=polygon_column, polygon_column_srid=polygon_column_srid, geom=geom, tolerance=tolerance, where=where, not_where=not_where,
23
+ columns=columns, limit=limit, geom_srid=geom_srid, intersection=intersection, intersection_area=intersection_area)
24
+ return self._return(connection=connection, result=result, keep_open=keep_open, commit=False, parsing_model=parsing_model)
25
+
26
+ def _intersects(self, table: Table, geom: WKBElement, polygon_column: Column, polygon_column_srid: int, connection: Connection = None, tolerance: float = 0.0, columns: list[str] = None, where: dict[str, Any] = None,
27
+ not_where: dict[str, Any] = None, limit: int = 0, geom_srid: int = 4326, intersection: bool = False, intersection_area: bool = False):
28
+ if where is None:
29
+ where = {}
30
+ if not_where is None:
31
+ not_where = {}
32
+
33
+ stmt = self._select(table, columns)
34
+
35
+ intersecting_geom = self._geom_condition(geom=geom, geom_srid=geom_srid, polygon_column_srid=polygon_column_srid, buffer=tolerance)
36
+
37
+ if intersection:
38
+ stmt = stmt.add_columns(polygon_column.ST_Intersection(intersecting_geom).label("intersection"))
39
+ if intersection_area:
40
+ stmt = stmt.add_columns(polygon_column.ST_Intersection(intersecting_geom).ST_Transform(3857).ST_Area().label("intersection_area"))
41
+
42
+ eq_where = [self._eq_where(table.c[column], condition) for column, condition in where.items()]
43
+ eq_where.extend([self._eq_where(table.c[column], condition, True) for column, condition in not_where.items()])
44
+
45
+ geom_condition = polygon_column.ST_Intersects(intersecting_geom)
46
+
47
+ eq_where.append(geom_condition)
48
+
49
+ stmt = stmt.where(*eq_where)
50
+
51
+ stmt = self._add_limit_offset_condition(stmt, limit)
52
+ return self.execute(connection=connection, stmt=stmt, mapping=True)
53
+
54
+ def contains(self, from_table: str, geom: WKBElement, polygon_column_name: str, polygon_column_srid: int, connection: Connection = None, tolerance: float = 0.0, columns: list[str] = None,
55
+ where: dict[str, Any] = None, limit: int = 0, contained: bool = False, geom_srid: int = 4326, parsing_model: Type[BaseModel] | BaseModel = None):
56
+
57
+ keep_open = False if connection is None else True
58
+ connection = self.open_connection if connection is None else connection
59
+ table = self.get_table(name=from_table)
60
+ polygon_column = table.c[polygon_column_name]
61
+ result = self._contains(table=table, connection=connection, polygon_column=polygon_column, polygon_column_srid=polygon_column_srid, geom=geom, tolerance=tolerance, where=where, contained=contained,
62
+ columns=columns, limit=limit, geom_srid=geom_srid)
63
+ return self._return(connection=connection, result=result, keep_open=keep_open, commit=False, parsing_model=parsing_model)
64
+
65
+ def _contains(self, table: Table, geom: WKBElement, polygon_column: Column, polygon_column_srid: int, connection: Connection = None, tolerance: float = 0.0, columns: list[str] = None, where: dict[str, Any] = None,
66
+ limit: int = 0, contained: bool = False, geom_srid: int = 4326):
67
+ if columns is None:
68
+ columns = []
69
+ if where is None:
70
+ where = {}
71
+
72
+ stmt = self._select(table, columns)
73
+ eq_where = [self._eq_where(table.c[column], condition) for column, condition in where.items()]
74
+
75
+ geom_condition = self._geom_condition(geom=geom, geom_srid=geom_srid, polygon_column_srid=polygon_column_srid, buffer=tolerance)
76
+ geom_condition = polygon_column.ST_Contains(geom_condition) if not contained else geom_condition.ST_Contains(polygon_column)
77
+
78
+ eq_where.append(geom_condition)
79
+
80
+ stmt = stmt.where(*eq_where)
81
+ stmt = self._add_limit_offset_condition(stmt, limit)
82
+ return self.execute(connection=connection, stmt=stmt, mapping=True)
83
+
84
+ def _geom_condition(self, geom: WKBElement, polygon_column_srid: int = 4326, geom_srid: int = 4326, buffer: float = 0):
85
+ geom_condition = geom.ST_Transform(polygon_column_srid) if geom_srid != polygon_column_srid else geom
86
+ geom_condition = geom_condition.ST_Buffer(buffer) if buffer else geom_condition
87
+
88
+ return geom_condition
@@ -0,0 +1,4 @@
1
+ from .crud_repository import *
2
+ from .geometry_repository import *
3
+ from .many_to_many_repository import *
4
+ from .view_repository import *