module-typica 0.2.2__tar.gz → 0.2.4__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.
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: module-typica
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Standard Pydantic usages & utilities
5
5
  Author: Oktapian
6
- Author-email: oktapian@jkt1.ebdesk.com
6
+ Author-email: oktapian1998@gmail.com
7
7
  Requires-Python: >=3.10,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.10
@@ -12,15 +12,15 @@ Classifier: Programming Language :: Python :: 3.12
12
12
  Requires-Dist: deprecated (>=1.2.14,<2.0.0)
13
13
  Requires-Dist: pydantic (>=2.9.2,<3.0.0)
14
14
  Requires-Dist: pydantic-settings (>=2.6.1,<3.0.0)
15
- Requires-Dist: tomli (>=2.1.0,<3.0.0)
16
15
  Description-Content-Type: text/markdown
17
16
 
18
17
  # Description
19
- [![python: v^3.10](https://img.shields.io/badge/python-v^3.10-333A73.svg?logo=python&style=for-the-badge&logoColor=ffffff)](https://www.python.org/downloads/release/python-3100/)
20
- [![Pydantic: v^2.3.0](https://img.shields.io/badge/pydantic-v^2.3.0-e92063.svg?logo=pydantic&style=for-the-badge&logoColor=ffffff)](https://pydantic.dev)
21
-
22
18
  This is a standardized model library that uses the pydantic library, which is often used in work, especially in database connection, parameter, or response in service, and other utilities. This repository provides a standardized model that can be used in various scenarios. It aims to simplify the process of defining and validating models in Python applications.
23
19
 
20
+ ## Usages
21
+ there are several use cases that can be implemented using this library, such as some of the examples below
22
+
23
+
24
24
 
25
25
  ## Contributors
26
26
 
@@ -1,9 +1,10 @@
1
1
  # Description
2
- [![python: v^3.10](https://img.shields.io/badge/python-v^3.10-333A73.svg?logo=python&style=for-the-badge&logoColor=ffffff)](https://www.python.org/downloads/release/python-3100/)
3
- [![Pydantic: v^2.3.0](https://img.shields.io/badge/pydantic-v^2.3.0-e92063.svg?logo=pydantic&style=for-the-badge&logoColor=ffffff)](https://pydantic.dev)
4
-
5
2
  This is a standardized model library that uses the pydantic library, which is often used in work, especially in database connection, parameter, or response in service, and other utilities. This repository provides a standardized model that can be used in various scenarios. It aims to simplify the process of defining and validating models in Python applications.
6
3
 
4
+ ## Usages
5
+ there are several use cases that can be implemented using this library, such as some of the examples below
6
+
7
+
7
8
 
8
9
  ## Contributors
9
10
 
@@ -1,8 +1,8 @@
1
1
  [tool.poetry]
2
2
  name = "module-typica"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "Standard Pydantic usages & utilities"
5
- authors = ["Oktapian <oktapian@jkt1.ebdesk.com>"]
5
+ authors = ["Oktapian <oktapian1998@gmail.com>"]
6
6
  readme = "Readme.md"
7
7
  packages = [{include = "typica"}]
8
8
 
@@ -14,11 +14,28 @@ venv = ".venv"
14
14
  python = "^3.10"
15
15
  pydantic = "^2.9.2"
16
16
  pydantic-settings = "^2.6.1"
17
- tomli = "^2.1.0"
18
17
  deprecated = "^1.2.14"
19
18
 
20
19
  [tool.poetry.group.dev.dependencies]
21
20
  faker = "^25.3.0"
21
+ pytest = "^8.3.3"
22
+
23
+
24
+ [tool.poetry.group.postgresql.dependencies]
25
+ psycopg2-binary = "^2.9.10"
26
+ sqlalchemy = "^2.0.36"
27
+
28
+
29
+ [tool.poetry.group.mongo.dependencies]
30
+ pymongo = "^4.10.1"
31
+
32
+
33
+ [tool.poetry.group.clickhouse.dependencies]
34
+ clickhouse-connect = "^0.8.7"
35
+
36
+
37
+ [tool.poetry.group.redis.dependencies]
38
+ redis = "^5.2.0"
22
39
 
23
40
  [build-system]
24
41
  requires = ["poetry-core"]
@@ -1,4 +1,5 @@
1
1
  from .base import *
2
2
  from .connection import *
3
3
  from .utils import *
4
- from .response import *
4
+ from .response import *
5
+ from .schema import *
@@ -4,11 +4,15 @@ from typing import Optional
4
4
  from pydantic import BaseModel, Field, model_validator
5
5
 
6
6
 
7
-
8
7
  class EndpointMeta(BaseModel):
9
8
  host: Optional[str] = Field("localhost", description="Connection host")
10
9
  port: Optional[str | int] = Field(8000, description="Connection port")
11
10
 
11
+ @property
12
+ def port_int(self) -> int | None:
13
+ if isinstance(self.port, str):
14
+ return int(self.port)
15
+ return self.port
12
16
 
13
17
 
14
18
  class AuthMeta(BaseModel):
@@ -23,7 +27,6 @@ class URIConnectionMeta(BaseModel):
23
27
  class DBConnectionMeta(EndpointMeta, AuthMeta, URIConnectionMeta):
24
28
  database: Optional[str] = Field(None, description="Database name")
25
29
 
26
-
27
30
  def uri_string(self, base: str = "http", with_db: bool = True) -> str:
28
31
  """
29
32
  Return a URI string for the database connection.
@@ -39,9 +42,26 @@ class DBConnectionMeta(EndpointMeta, AuthMeta, URIConnectionMeta):
39
42
  return f"{base}://{meta}/{self.database if with_db else ''}"
40
43
  return ""
41
44
 
42
-
43
45
  @model_validator(mode="after")
44
46
  def extract_uri(self):
47
+ """
48
+ Extracts and parses the URI to populate the connection metadata fields.
49
+
50
+ This method processes the `uri` attribute to extract authentication and
51
+ connection details such as username, password, host, port, and database.
52
+ It modifies the respective attributes of the instance based on the parsed
53
+ URI components.
54
+
55
+ Steps involved:
56
+ - Strips the scheme from the URI.
57
+ - Splits the URI into metadata and additional query parameters.
58
+ - Extracts database name from query parameters if present.
59
+ - Parses authentication info and host details from the metadata.
60
+ - Converts the port to an integer.
61
+
62
+ Returns:
63
+ The instance with populated connection metadata fields.
64
+ """
45
65
  if self.uri:
46
66
  uri = re.sub(r"\w+:(//|/)", "", self.uri)
47
67
  metadata, others = (
@@ -63,8 +83,11 @@ class DBConnectionMeta(EndpointMeta, AuthMeta, URIConnectionMeta):
63
83
  self.port = int(self.port)
64
84
  return self
65
85
 
86
+
66
87
  class ClusterConnectionMeta(AuthMeta, URIConnectionMeta):
67
- cluster_uri: Optional[list[EndpointMeta]] = Field([], description="List of clusters endpoint")
88
+ cluster_uri: Optional[list[EndpointMeta]] = Field(
89
+ [], description="List of clusters endpoint"
90
+ )
68
91
  database: Optional[str] = Field(None, description="Database name")
69
92
 
70
93
  def uri_string(self, base: str = "http", with_db: bool = True) -> str:
@@ -81,38 +104,42 @@ class ClusterConnectionMeta(AuthMeta, URIConnectionMeta):
81
104
  return f"{base}://{self.username}:{self.password}@{meta}/{self.database if with_db else ''}"
82
105
  return f"{base}://{meta}/{self.database if with_db else ''}"
83
106
  return ""
84
-
85
107
 
86
108
  @model_validator(mode="after")
87
109
  def extract_uri(self):
110
+ """
111
+ Extract URI from connection string and fill in the respective fields.
112
+
113
+ If the connection string is in the format of mongodb://user:password@host:port/database,
114
+ the respective fields will be filled in. If the connection string is in the format of
115
+ mongodb://host:port,host:port/database, the hosts will be split into a list of
116
+ EndpointMeta objects.
117
+
118
+ :return: The modified ClusterConnectionMeta object.
119
+ :rtype: ClusterConnectionMeta
120
+ """
88
121
  if self.uri:
89
122
  uri = re.sub(r"\w+:(//|/)", "", self.uri)
90
- metadata, others = (
123
+ clean_meta, others = (
91
124
  re.split(r"\/\?|\/", uri) if re.search(r"\/\?|\/", uri) else [uri, None]
92
125
  )
126
+ cluster_uri = []
93
127
  if others and "&" in others:
94
128
  for other in others.split("&"):
95
129
  if "=" in other and re.search(r"authSource", other):
96
130
  self.database = other.split("=")[-1]
97
131
  elif "=" not in other:
98
132
  self.database = other
99
- if "@" in metadata:
100
- if "," in metadata:
101
- metadata, raw_clusters = re.split(r"\@", metadata)
102
- self.username, self.password = re.split(r"\:", metadata)
103
- cluster_uri = []
104
- for cluster in raw_clusters.split(","):
105
- hostData = re.split(r"\:", cluster)
106
- cluster_uri.append(EndpointMeta(host=hostData[0], port=int(hostData[1])))
107
- self.cluster_uri = cluster_uri
108
- else:
109
- self.username, self.password, self.host, self.port = re.split(
110
- r"\@|\:", metadata
111
- )
112
- else:
113
- self.host, self.port = re.split(r"\:", metadata)
114
- if self.port:
115
- self.port = int(self.port)
133
+ if "@" in clean_meta:
134
+ auth_meta, clean_meta = re.split(r"\@", clean_meta)
135
+ self.username, self.password = re.split(r"\:", auth_meta)
136
+
137
+ for cluster in clean_meta.split(","):
138
+ hostData = re.split(r"\:", cluster)
139
+ cluster_uri.append(
140
+ EndpointMeta(host=hostData[0], port=int(hostData[1]))
141
+ )
142
+ self.cluster_uri = cluster_uri
116
143
  return self
117
144
 
118
145
 
@@ -122,7 +149,6 @@ class S3ConnectionMeta(EndpointMeta):
122
149
  bucket: str = Field(..., description="S3 bucket name")
123
150
  base_path: Optional[str] = Field("/", description="S3 base path")
124
151
 
125
-
126
152
  @property
127
153
  def json_meta(self) -> dict:
128
154
  """
@@ -134,4 +160,12 @@ class S3ConnectionMeta(EndpointMeta):
134
160
  "endpoint_url": f"http://{self.host}:{self.port}",
135
161
  "key": self.access_key,
136
162
  "secret": self.secret_key,
137
- }
163
+ }
164
+
165
+
166
+ class RedisConnectionMeta(EndpointMeta, AuthMeta):
167
+ database: int = Field(..., description="Database name")
168
+
169
+
170
+ class RMQConnectionMeta(EndpointMeta, AuthMeta):
171
+ vhost: Optional[str] = Field(None, description="Virtual host")
@@ -1,7 +1,6 @@
1
-
2
1
  from typing import Optional
3
2
 
4
- from pydantic import BaseModel, Field
3
+ from pydantic import BaseModel, RootModel, Field
5
4
 
6
5
  from .utils import MedallionTypes, LocationLevel
7
6
  from .connection import DBConnectionMeta, S3ConnectionMeta, ClusterConnectionMeta
@@ -16,6 +15,30 @@ class SchemaMeta(BaseModel):
16
15
  field_hide: Optional[bool] = Field(False, description="Hide field")
17
16
 
18
17
 
18
+ class SchemaRawMeta(SchemaMeta):
19
+ regional_field: Optional[str] = Field(None, description="Regional field")
20
+ none_percentage: Optional[float] = Field(
21
+ 0.0, description="Percentage of null / none values"
22
+ )
23
+ unique_value: Optional[list[str]] = Field(
24
+ None, description="Unique values in field"
25
+ )
26
+ describe_field: Optional[str] = Field(None, description="Describe field")
27
+
28
+
29
+ class Schemas(RootModel):
30
+ root: list[SchemaMeta]
31
+
32
+ def __iter__(self): # type: ignore
33
+ return self.root.__iter__()
34
+
35
+ def __getitem__(self, item):
36
+ return self.root[item]
37
+
38
+ def __len__(self):
39
+ return self.root.__len__()
40
+
41
+
19
42
  class SimplifieMetadata(BaseModel):
20
43
  id: str = Field(..., description="Identifier for the metadata")
21
44
 
@@ -23,7 +46,7 @@ class SimplifieMetadata(BaseModel):
23
46
  title: str = Field(..., description="Title, name, or label for the metadata")
24
47
  source: str = Field(..., description="Source of the metadata, e.g. www.example.com")
25
48
  country: str = Field(..., description="Country of origin")
26
- year: str = Field(..., description="Year of the metadata")
49
+ year: str | int = Field(..., description="Year of the metadata")
27
50
  range_data: str = Field(..., description="Range of data, eg. 2021-2022")
28
51
  description: Optional[str] = Field(None)
29
52
 
@@ -32,7 +55,7 @@ class SimplifieMetadata(BaseModel):
32
55
  sub_category: Optional[str] = Field(None)
33
56
 
34
57
  # ? Schemas
35
- schemas: list[SchemaMeta] = Field(..., description="Description of all fields in data")
58
+ schemas: Schemas = Field(..., description="Description of all fields in data")
36
59
 
37
60
  # ? Database
38
61
  database_access: DBConnectionMeta | ClusterConnectionMeta
@@ -65,10 +88,9 @@ class FullMetadata(SimplifieMetadata):
65
88
 
66
89
  # ? Data lake
67
90
  # * Use case: bronze
68
- lake_access: Optional[S3ConnectionMeta | DBConnectionMeta | ClusterConnectionMeta] = Field(
69
- None, description="Data lake access"
70
- )
91
+ lake_access: Optional[
92
+ S3ConnectionMeta | DBConnectionMeta | ClusterConnectionMeta
93
+ ] = Field(None, description="Data lake access")
71
94
  lake_meta_path: Optional[str] = Field(None, description="Data lake metadata path")
72
95
  lake_data_path: Optional[str] = Field(None, description="Data lake data path")
73
96
  lake_data_format: Optional[str] = Field(None, description="Data lake data format")
74
-
@@ -0,0 +1,67 @@
1
+ from clickhouse_connect import get_client
2
+ from clickhouse_connect.driver.client import Client
3
+
4
+ from typica.connection import DBConnectionMeta
5
+
6
+
7
+ class CHConnector:
8
+
9
+ _meta: DBConnectionMeta
10
+ _client: Client
11
+
12
+ def __init__(self, meta: DBConnectionMeta) -> None:
13
+ """
14
+ Initialize the ClickHouse connector with the given connection metadata.
15
+
16
+ :param meta: The metadata of the database connection.
17
+ :type meta: DBConnectionMeta
18
+ """
19
+ self._meta = meta
20
+
21
+ def __enter__(self) -> "CHConnector":
22
+ """
23
+ Connect to the ClickHouse server and return the connection object.
24
+
25
+ :return: The connection object.
26
+ :rtype: CHConnector
27
+ :raises ValueError: If the connection to the ClickHouse server fails.
28
+ """
29
+ self.connect()
30
+ return self
31
+
32
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
33
+ """
34
+ Close the connection to the ClickHouse server.
35
+
36
+ This method is called when the context manager exits its scope.
37
+ """
38
+ self.close()
39
+
40
+ def connect(self) -> None:
41
+ """
42
+ Establish a connection to the ClickHouse server.
43
+
44
+ :raises ValueError: If the connection to the ClickHouse server fails.
45
+ :raises Exception: If any other error occurs during the connection.
46
+ """
47
+ try:
48
+ self._client = get_client(
49
+ host=str(self._meta.host),
50
+ port=int(self._meta.port), # type: ignore
51
+ user=self._meta.username,
52
+ password=self._meta.password
53
+ or "", # cause get_client doesn't support empty password
54
+ database=str(self._meta.database),
55
+ )
56
+
57
+ except Exception as e:
58
+ raise e
59
+
60
+ def close(self) -> None:
61
+ """
62
+ Close the connection to the ClickHouse server.
63
+
64
+ This method is a no-op if the connection is already closed.
65
+ """
66
+ if self._client:
67
+ self._client.close()
@@ -0,0 +1,70 @@
1
+ from pymongo import MongoClient
2
+ from pymongo.database import Database
3
+ from pymongo.errors import NetworkTimeout, ExecutionTimeout
4
+
5
+ from typica.connection import DBConnectionMeta
6
+
7
+
8
+ class MongoConnector:
9
+
10
+ _meta: DBConnectionMeta
11
+ _client: MongoClient
12
+ _db: Database
13
+
14
+ def __init__(self, meta: DBConnectionMeta) -> None:
15
+ """
16
+ Initialize the Mongo connector with the given connection metadata.
17
+
18
+ :param meta: The metadata of the database connection.
19
+ :type meta: DBConnectionMeta
20
+ """
21
+ self._meta = meta
22
+ if not self._meta.uri:
23
+ self._meta.uri = self._meta.uri_string(base="mongodb", with_db=False)
24
+
25
+ def __enter__(self):
26
+ """
27
+ Connect to the MongoDB server and return the connection object.
28
+
29
+ :return: The connection object.
30
+ :rtype: MongoConnector
31
+ :raises ValueError: If the connection to the MongoDB server fails.
32
+ """
33
+ self.connect()
34
+ if self._client is None:
35
+ raise ValueError("Mongo not connected.")
36
+ return self
37
+
38
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
39
+ """
40
+ Close the connection to the MongoDB server.
41
+
42
+ This method is called when the context manager exits its scope.
43
+ """
44
+ self.close()
45
+
46
+ def connect(self, **kwargs) -> None:
47
+ """
48
+ Establish a connection to the MongoDB server.
49
+
50
+ :param kwargs: Additional keyword arguments for MongoClient.
51
+ :raises ValueError: If the connection to the MongoDB server fails.
52
+ :raises Exception: If any other error occurs during the connection.
53
+ """
54
+
55
+ try:
56
+ self._client = MongoClient(self._meta.uri, timeoutMS=20000, **kwargs)
57
+ self._db = self._client[str(self._meta.database)]
58
+ except (NetworkTimeout, ExecutionTimeout):
59
+ raise ValueError("Mongo connection timed out.")
60
+ except Exception as e:
61
+ raise e
62
+
63
+ def close(self) -> None:
64
+ """
65
+ Close the connection to the MongoDB server.
66
+
67
+ This method is a no-op if the connection is already closed.
68
+ """
69
+ if self._client:
70
+ self._client.close()
@@ -0,0 +1,76 @@
1
+ import psycopg2
2
+
3
+ from psycopg2.extensions import connection, cursor
4
+ from psycopg2.errors import (
5
+ IdleSessionTimeout,
6
+ IdleInTransactionSessionTimeout,
7
+ ConnectionFailure,
8
+ )
9
+
10
+ from typica.connection import DBConnectionMeta
11
+
12
+
13
+ class PostgreConnector:
14
+
15
+ _meta: DBConnectionMeta
16
+ _conn: connection
17
+ _cursor: cursor
18
+
19
+ def __init__(self, meta: DBConnectionMeta) -> None:
20
+ """
21
+ Initialize the Postgre connector with the given connection metadata.
22
+
23
+ :param meta: The metadata of the database connection.
24
+ :type meta: DBConnectionMeta
25
+ """
26
+ self._meta = meta
27
+
28
+ def __enter__(self) -> "PostgreConnector":
29
+ """
30
+ Connect to the PostgreSQL server and return the connection object.
31
+
32
+ :return: The connection object.
33
+ :rtype: PostgreConnector
34
+ :raises ValueError: If the connection to the PostgreSQL server fails.
35
+ """
36
+ self.connect()
37
+ return self
38
+
39
+ def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
40
+ self.close()
41
+
42
+ def connect(self, **kwargs) -> None:
43
+ """
44
+ Establish a connection to the PostgreSQL server.
45
+
46
+ :param kwargs: Additional keyword arguments for psycopg2.connect.
47
+ :raises ValueError: If the connection to the PostgreSQL server fails.
48
+ :raises Exception: If any other error occurs during the connection.
49
+ """
50
+ try:
51
+ self._conn = psycopg2.connect(
52
+ dbname=self._meta.database,
53
+ user=self._meta.username,
54
+ password=self._meta.password,
55
+ host=self._meta.host,
56
+ port=self._meta.port,
57
+ **kwargs
58
+ )
59
+ self._cursor = self._conn.cursor()
60
+ except ConnectionFailure:
61
+ raise ValueError("PostgreSQL connection failed.")
62
+ except (IdleInTransactionSessionTimeout, IdleSessionTimeout):
63
+ raise ValueError("Session timed out.")
64
+ except Exception as e:
65
+ raise e
66
+
67
+ def close(self) -> None:
68
+ """
69
+ Close the connection to the PostgreSQL server.
70
+
71
+ This method is a no-op if the connection is already closed.
72
+ """
73
+ if self._cursor:
74
+ self._cursor.close()
75
+ if self._conn:
76
+ self._conn.close()
@@ -0,0 +1,83 @@
1
+ from sqlalchemy import Engine, URL, Connection, create_engine, text, CursorResult
2
+ from sqlalchemy.exc import TimeoutError
3
+
4
+ from typica.connection import DBConnectionMeta
5
+
6
+
7
+ class PostgreConnector:
8
+
9
+ _meta: DBConnectionMeta
10
+ _engine: Engine
11
+ _conn: Connection
12
+
13
+ def __init__(self, meta: DBConnectionMeta) -> None:
14
+ """
15
+ Initialize the Postgre connector with the given connection metadata.
16
+
17
+ :param meta: The metadata of the database connection.
18
+ :type meta: DBConnectionMeta
19
+ """
20
+ self._meta = meta
21
+
22
+ def __enter__(self) -> Connection:
23
+ """
24
+ Connect to the PostgreSQL server and return the connection object.
25
+
26
+ :return: The connection object.
27
+ :rtype: Connection
28
+ :raises ValueError: If the connection to the PostgreSQL server fails.
29
+ """
30
+ self.connect()
31
+ return self._conn
32
+
33
+ def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
34
+ """
35
+ Close the connection to the PostgreSQL server.
36
+
37
+ This method is called when the context manager exits its scope.
38
+ """
39
+ self.close()
40
+
41
+ def execute_text(self, sql: str) -> CursorResult:
42
+ try:
43
+ res = self._conn.execute(text(sql))
44
+ return res
45
+ except Exception as e:
46
+ raise e
47
+
48
+ def connect(self) -> Connection:
49
+ """
50
+ Establish a connection to the PostgreSQL server.
51
+
52
+ :return: The connection object.
53
+ :rtype: Connection
54
+ :raises ValueError: If the connection to the PostgreSQL server fails.
55
+ :raises Exception: If any other error occurs during the connection.
56
+ """
57
+ try:
58
+ self._engine = create_engine(
59
+ URL.create(
60
+ drivername="postgresql",
61
+ username=self._meta.username,
62
+ password=self._meta.password,
63
+ host=self._meta.host,
64
+ port=int(self._meta.port), # type: ignore
65
+ database=self._meta.database,
66
+ )
67
+ )
68
+ self._conn = self._engine.connect()
69
+
70
+ return self._conn
71
+ except TimeoutError:
72
+ raise ValueError("PostgreSQL connection timed out.")
73
+ except Exception as e:
74
+ raise e
75
+
76
+ def close(self) -> None:
77
+ """
78
+ Close the connection to the PostgreSQL server.
79
+
80
+ This method is a no-op if the connection is already closed.
81
+ """
82
+ if self._conn:
83
+ self._conn.close()
@@ -0,0 +1,165 @@
1
+ from redis import Redis
2
+ from redis.asyncio import Redis as AsyncRedis
3
+ from redis.exceptions import TimeoutError
4
+
5
+ from typica.connection import RedisConnectionMeta
6
+
7
+
8
+ class RedisConnector:
9
+
10
+ _meta: RedisConnectionMeta
11
+ _client: Redis
12
+
13
+ def __init__(self, meta: RedisConnectionMeta) -> None:
14
+ """
15
+ Initialize the Redis connector with the given connection metadata.
16
+
17
+ :param meta: The metadata of the database connection.
18
+ :type meta: RedisConnectionMeta
19
+ """
20
+ self._meta = meta
21
+
22
+ def __enter__(self):
23
+ """
24
+ Connect to the Redis server and return the connection object.
25
+
26
+ :return: The connection object.
27
+ :rtype: RedisConnector
28
+ :raises ValueError: If the connection to the Redis server fails.
29
+ """
30
+ self.connect()
31
+ if self._client is None:
32
+ raise ValueError("Redis not connected.")
33
+ return self
34
+
35
+ def __call__(self, *args, **kwds) -> bool:
36
+ """
37
+ Check if the connection is already established.
38
+
39
+ :return: True if the connection is available, False otherwise.
40
+ :rtype: bool
41
+ """
42
+
43
+ if self._client is None:
44
+ raise ValueError("Redis not connected.")
45
+ return True
46
+
47
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
48
+ """
49
+ Close the connection to the Redis server.
50
+
51
+ This method is called when the context manager exits its scope.
52
+ """
53
+ self.close()
54
+
55
+ def connect(self, other_database: int | None = None) -> None:
56
+ """
57
+ Establish a connection to the Redis server.
58
+
59
+ :param other_database: The ID of an alternative database to connect to.
60
+ If not provided, the default database ID will be used.
61
+ :type other_database: int | None
62
+ :raises ValueError: If the connection to the Redis server fails.
63
+ """
64
+ try:
65
+
66
+ self._client = Redis(
67
+ host=str(self._meta.host),
68
+ port=int(self._meta.port), # type: ignore
69
+ username=self._meta.username,
70
+ password=self._meta.password,
71
+ db=other_database if other_database else self._meta.database,
72
+ )
73
+
74
+ except TimeoutError:
75
+ raise ValueError("Redis connection timed out.")
76
+ except Exception as e:
77
+ raise e
78
+
79
+ def close(self) -> None:
80
+ """
81
+ Close the connection to the Redis server.
82
+
83
+ This method is a no-op if the connection is already closed.
84
+ """
85
+ if self._client:
86
+ self._client.close()
87
+
88
+
89
+ class AsyncRedisConnector:
90
+
91
+ _meta: RedisConnectionMeta
92
+ _client: AsyncRedis
93
+
94
+ def __init__(self, meta: RedisConnectionMeta) -> None:
95
+ """
96
+ Initialize the Redis connector with the given connection metadata.
97
+
98
+ :param meta: The metadata of the database connection.
99
+ :type meta: RedisConnectionMeta
100
+ """
101
+ self._meta = meta
102
+
103
+ async def __enter__(self):
104
+ """
105
+ Connect to the Redis server and return the connection object.
106
+
107
+ :return: The connection object.
108
+ :rtype: RedisConnector
109
+ :raises ValueError: If the connection to the Redis server fails.
110
+ """
111
+ await self.connect()
112
+ if self._client is None:
113
+ raise ValueError("Redis not connected.")
114
+ return self
115
+
116
+ async def __call__(self, *args, **kwds) -> bool:
117
+ """
118
+ Check if the connection is already established.
119
+
120
+ :return: True if the connection is available, False otherwise.
121
+ :rtype: bool
122
+ """
123
+
124
+ if self._client is None:
125
+ raise ValueError("Redis not connected.")
126
+ return True
127
+
128
+ async def __exit__(self, exc_type, exc_val, exc_tb) -> None:
129
+ """
130
+ Close the connection to the Redis server.
131
+
132
+ This method is called when the context manager exits its scope.
133
+ """
134
+ await self.close()
135
+
136
+ async def connect(self, other_database: int | None = None) -> None:
137
+ """
138
+ Establish a connection to the Redis server.
139
+
140
+ :param other_database: The alternative database name to connect to.
141
+ :raises ValueError: If the connection to the Redis server fails.
142
+ :raises Exception: If any other error occurs during the connection.
143
+ """
144
+ try:
145
+
146
+ self._client = AsyncRedis(
147
+ host=str(self._meta.host),
148
+ port=int(self._meta.port), # type: ignore
149
+ username=self._meta.username,
150
+ password=self._meta.password,
151
+ db=other_database if other_database else self._meta.database,
152
+ )
153
+ except TimeoutError:
154
+ raise ValueError("Redis connection timed out.")
155
+ except Exception as e:
156
+ raise e
157
+
158
+ async def close(self) -> None:
159
+ """
160
+ Close the connection to the Redis server.
161
+
162
+ This method is a no-op if the connection is already closed.
163
+ """
164
+ if self._client:
165
+ await self._client.close()
@@ -7,6 +7,7 @@ class BaseResponseMeta(BaseModel):
7
7
  code: int
8
8
  message: str
9
9
 
10
+
10
11
  class PaginationResponseMeta(BaseResponseMeta):
11
12
  page: Optional[int] = Field(1, gt=0)
12
13
  size: Optional[int] = Field(10, ge=0)
@@ -19,7 +20,7 @@ class ServiceResponse:
19
20
  self.model = model
20
21
  self.auth = auth
21
22
 
22
- def basic(self, route: str) -> dict:
23
+ def basic(self, route_name: str) -> dict:
23
24
  """
24
25
  Generate a basic response, which is a dictionary of common response codes and models.
25
26
 
@@ -27,27 +28,32 @@ class ServiceResponse:
27
28
  - 400 Bad Request
28
29
  - 500 Internal Server Error
29
30
 
30
- :param route: The path of the route
31
+ :param route_name: The name of model for response
31
32
  :return: A dictionary of common response codes and models
32
33
  """
33
34
  return {
34
35
  400: {
35
- "model": create_model(
36
- route, code=(int, 400), message=(str, "Bad Request")
37
- ),
36
+ "model": create_model(route_name, message=(str, "Bad Request")),
38
37
  "description": "Occurs when the request you make does not match or is invalid",
39
38
  },
40
39
  500: {
41
40
  "model": create_model(
42
- route,
43
- code=(int, 500),
41
+ route_name,
44
42
  message=(str, "Internal Server Error"),
45
43
  ),
46
44
  "description": "Occurs when there is an engine or lib error in the engine",
47
45
  },
48
46
  }
49
47
 
50
- def get(self, route: str, model: Any = None, obj: str = "Data", auth: bool = False, exclude_codes: list = [], **kwargs) -> dict:
48
+ def get(
49
+ self,
50
+ route_name: str,
51
+ model: Any = None,
52
+ obj: str = "Data",
53
+ auth: bool = False,
54
+ exclude_codes: list = [],
55
+ **kwargs,
56
+ ) -> dict:
51
57
  """
52
58
  Generate a response for a get request, which is a dictionary of common response codes and models.
53
59
 
@@ -55,7 +61,7 @@ class ServiceResponse:
55
61
  - 200 Success
56
62
  - 404 Not Found
57
63
 
58
- :param route: The path of the route
64
+ :param route_name: The name of model for response
59
65
  :param model: The model to use for the response
60
66
  :param obj: The object name to use for the response
61
67
  :param auth: Whether or not the route requires authentication
@@ -65,39 +71,39 @@ class ServiceResponse:
65
71
  response: dict = {
66
72
  200: {
67
73
  "model": create_model(
68
- route,
69
- code=(int, 200),
74
+ route_name,
70
75
  message=(str, "Success"),
71
76
  data=(model if model else self.model, ...),
72
77
  ),
73
78
  "description": "Success get data",
74
79
  },
75
80
  404: {
76
- "model": create_model(
77
- route, code=(int, 404), message=(str, f"{obj} not found")
78
- ),
81
+ "model": create_model(route_name, message=(str, f"{obj} not found")),
79
82
  },
80
- **self.basic(route),
81
- **kwargs
83
+ **self.basic(route_name),
84
+ **kwargs,
82
85
  }
83
86
 
84
87
  if exclude_codes:
85
88
  for code in exclude_codes:
86
89
  del response[code]
87
-
88
90
 
89
91
  if auth or self.auth:
90
92
  response[401] = {
91
- "model": create_model(
92
- route, code=(int, 401), message=(str, "Unauthorized")
93
- )
93
+ "model": create_model(route_name, message=(str, "Unauthorized"))
94
94
  }
95
-
96
- return response
97
-
98
95
 
96
+ return response
99
97
 
100
- def pagination(self, route: str, model: Any = None, obj: str = "Data", auth: bool = False, exclude_codes: list = [], **kwargs) -> dict:
98
+ def pagination(
99
+ self,
100
+ route_name: str,
101
+ model: Any = None,
102
+ obj: str = "Data",
103
+ auth: bool = False,
104
+ exclude_codes: list = [],
105
+ **kwargs,
106
+ ) -> dict:
101
107
  """
102
108
  Generate a pagination response, which is a dictionary of common response codes and models.
103
109
 
@@ -106,7 +112,7 @@ class ServiceResponse:
106
112
  - 404 Not Found
107
113
  - 401 Unauthorized (optional)
108
114
 
109
- :param route: The path of the route
115
+ :param route_name: The name of model for response
110
116
  :param model: The model to use for the response
111
117
  :param obj: The object to be gotten
112
118
  :param auth: Whether or not the route requires authentication
@@ -116,8 +122,7 @@ class ServiceResponse:
116
122
  response: dict = {
117
123
  200: {
118
124
  "model": create_model(
119
- route,
120
- code=(int, 200),
125
+ route_name,
121
126
  message=(str, f"Success get all {obj}"),
122
127
  data=(model if model else self.model, ...),
123
128
  page=(int, 1),
@@ -126,29 +131,32 @@ class ServiceResponse:
126
131
  ),
127
132
  },
128
133
  404: {
129
- "model": create_model(
130
- route, code=(int, 404), message=(str, f"{obj} not found")
131
- ),
134
+ "model": create_model(route_name, message=(str, f"{obj} not found")),
132
135
  },
133
- **self.basic(route),
134
- **kwargs
136
+ **self.basic(route_name),
137
+ **kwargs,
135
138
  }
136
139
 
137
140
  if exclude_codes:
138
141
  for code in exclude_codes:
139
142
  del response[code]
140
-
141
143
 
142
144
  if auth or self.auth:
143
145
  response[401] = {
144
- "model": create_model(
145
- route, code=(int, 401), message=(str, "Unauthorized")
146
- )
146
+ "model": create_model(route_name, message=(str, "Unauthorized"))
147
147
  }
148
-
148
+
149
149
  return response
150
150
 
151
- def creation(self, route: str, model: Any = None, obj: str = "Data", auth: bool = False, exclude_codes: list = [], **kwargs) -> dict:
151
+ def creation(
152
+ self,
153
+ route_name: str,
154
+ model: Any = None,
155
+ obj: str = "Data",
156
+ auth: bool = False,
157
+ exclude_codes: list = [],
158
+ **kwargs,
159
+ ) -> dict:
152
160
  """
153
161
  Generate a response for a create request, which is a dictionary of common response codes and models.
154
162
 
@@ -156,139 +164,126 @@ class ServiceResponse:
156
164
  - 201 Created
157
165
  - 401 Unauthorized
158
166
 
159
- :param route: The path of the route
167
+ :param route_name: The name of model for response
160
168
  :param model: The model to use for the response
161
169
  :param obj: The object name to use for the response
162
170
  :param auth: Whether or not the route requires authentication
163
171
  :param exclude_codes: A list of codes to exclude from the response
164
172
  :return: A dictionary of common response codes and models
165
173
  """
166
- response: dict = {
174
+ response: dict = {
167
175
  201: {
168
176
  "model": create_model(
169
- route,
170
- code=(int, 201),
177
+ route_name,
171
178
  message=(str, f"{obj} created successfully"),
172
179
  data=(model if model else self.model, ...),
173
180
  ),
174
181
  },
175
- **self.basic(route),
176
- **kwargs
182
+ **self.basic(route_name),
183
+ **kwargs,
177
184
  }
178
185
 
179
186
  if exclude_codes:
180
187
  for code in exclude_codes:
181
188
  del response[code]
182
-
183
189
 
184
190
  if auth or self.auth:
185
191
  response[401] = {
186
- "model": create_model(
187
- route, code=(int, 401), message=(str, "Unauthorized")
188
- )
192
+ "model": create_model(route_name, message=(str, "Unauthorized"))
189
193
  }
190
-
194
+
191
195
  return response
192
196
 
193
- def update(self, route: str, model: Any = None, obj: str = "Data", auth: bool = False, exclude_codes: list = [], **kwargs) -> dict:
197
+ def update(
198
+ self,
199
+ route_name: str,
200
+ model: Any = None,
201
+ obj: str = "Data",
202
+ auth: bool = False,
203
+ exclude_codes: list = [],
204
+ **kwargs,
205
+ ) -> dict:
194
206
  """
195
207
  Generate a response for a update request, which is a dictionary of common response codes and models.
196
208
 
197
209
  Contains:
198
210
  - 200 OK
199
- - 204 No Content
200
211
  - 401 Unauthorized (optional)
201
212
 
202
- :param route: The path of the route
213
+ :param route_name: The name of model for response
203
214
  :param model: The model to use for the response
204
215
  :param obj: The object name to use for the response
205
216
  :param auth: Whether or not the route requires authentication
206
217
  :param exclude_codes: A list of codes to exclude from the response
207
218
  :return: A dictionary of common response codes and models
208
219
  """
209
- response: dict = {
220
+ response: dict = {
210
221
  200: {
211
222
  "model": create_model(
212
- route,
213
- code=(int, 200),
223
+ route_name,
214
224
  message=(str, f"{obj} updated successfully"),
215
225
  data=(model if model else self.model, ...),
216
226
  ),
217
227
  },
218
- 204: {
219
- "model": create_model(
220
- route,
221
- code=(int, 204),
222
- message=(str, f"{obj} updated successfully"),
223
- ),
224
- },
225
- **self.basic(route),
226
- **kwargs
228
+ **self.basic(route_name),
229
+ **kwargs,
227
230
  }
228
231
 
229
232
  if exclude_codes:
230
233
  for code in exclude_codes:
231
234
  del response[code]
232
-
233
235
 
234
236
  if auth or self.auth:
235
237
  response[401] = {
236
- "model": create_model(
237
- route, code=(int, 401), message=(str, "Unauthorized")
238
- )
238
+ "model": create_model(route_name, message=(str, "Unauthorized"))
239
239
  }
240
-
240
+
241
241
  return response
242
242
 
243
- def delete(self, route: str, model: Any = None, obj: str = "Data", auth: bool = False, exclude_codes: list = [], **kwargs) -> dict:
243
+ def delete(
244
+ self,
245
+ route_name: str,
246
+ model: Any = None,
247
+ obj: str = "Data",
248
+ auth: bool = False,
249
+ exclude_codes: list = [],
250
+ **kwargs,
251
+ ) -> dict:
244
252
  """
245
253
  Generate a response for a delete request, which is a dictionary of common response codes and models.
246
254
 
247
255
  Contains:
248
256
  - 200 Success
249
- - 204 Success
250
257
  - 400 Bad Request
251
258
  - 401 Unauthorized (optional)
252
259
  - 500 Internal Server Error
253
260
 
254
- :param route: The path of the route
261
+ :param route_name: The name of model for response
255
262
  :param model: The model to use for the response
256
263
  :param obj: The object name to use for the response
257
264
  :param auth: Whether or not the route requires authentication
258
265
  :param exclude_codes: A list of codes to exclude from the response
259
266
  :return: A dictionary of common response codes and models
260
267
  """
261
- response: dict = {
268
+ response: dict = {
262
269
  200: {
263
270
  "model": create_model(
264
- route,
265
- code=(int, 200),
271
+ route_name,
266
272
  message=(str, f"{obj} delete successfully"),
267
273
  data=(model if model else self.model, ...),
268
274
  ),
269
275
  },
270
- 204: {
271
- "model": create_model(
272
- route,
273
- code=(int, 204),
274
- message=(str, f"{obj} delete successfully"),
275
- ),
276
- },
277
- **self.basic(route),
278
- **kwargs
276
+ **self.basic(route_name),
277
+ **kwargs,
279
278
  }
280
279
 
281
280
  if exclude_codes:
282
281
  for code in exclude_codes:
283
282
  del response[code]
284
-
285
283
 
286
284
  if auth or self.auth:
287
285
  response[401] = {
288
- "model": create_model(
289
- route, code=(int, 401), message=(str, "Unauthorized")
290
- )
286
+ "model": create_model(route_name, message=(str, "Unauthorized"))
291
287
  }
292
-
288
+
293
289
  return response
294
-
@@ -0,0 +1,27 @@
1
+ from typing import Optional, Any
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class PaginationSchema(BaseModel):
7
+ page: Optional[int] = Field(1, gt=0, description="Page number")
8
+ size: Optional[int] = Field(10, ge=0, description="Page size")
9
+
10
+
11
+ class OrderSchema(BaseModel):
12
+ order_by: Optional[str] = Field(None, description="Order by field")
13
+ order_type: Optional[str | int] = Field(
14
+ None, description="Order type", examples=[1, "asc", "desc"]
15
+ )
16
+
17
+
18
+ class FilterValueSchema(BaseModel):
19
+ filter_value: Optional[Any] = Field(None, description="Filter value")
20
+
21
+
22
+ class FilterSchema(FilterValueSchema):
23
+ filter_by: Optional[str] = Field(None, description="Filter by field")
24
+
25
+
26
+ class FilterOpsSchema(FilterSchema):
27
+ filter_op: Optional[str] = Field(None, description="Filter operation")
@@ -0,0 +1,2 @@
1
+ from .enums import *
2
+ from .config import ProjectConfig
@@ -0,0 +1,35 @@
1
+ from typing import Tuple, Type
2
+
3
+ from pydantic_settings import (
4
+ BaseSettings,
5
+ SettingsConfigDict,
6
+ PyprojectTomlConfigSettingsSource,
7
+ PydanticBaseSettingsSource,
8
+ )
9
+
10
+
11
+ class ProjectConfig(BaseSettings):
12
+
13
+ name: str
14
+ version: str = "0.1.0"
15
+ description: str = ""
16
+ authors: list[str] = []
17
+
18
+ @classmethod
19
+ def settings_customise_sources(
20
+ cls,
21
+ settings_cls: Type[BaseSettings],
22
+ init_settings: PydanticBaseSettingsSource,
23
+ env_settings: PydanticBaseSettingsSource,
24
+ dotenv_settings: PydanticBaseSettingsSource,
25
+ file_secret_settings: PydanticBaseSettingsSource,
26
+ ) -> Tuple[PydanticBaseSettingsSource, ...]:
27
+ return (PyprojectTomlConfigSettingsSource(settings_cls),)
28
+
29
+ model_config = SettingsConfigDict(
30
+ pyproject_toml_table_header=("tool", "poetry"), extra="ignore"
31
+ )
32
+
33
+ @property
34
+ def title(self) -> str:
35
+ return self.name.replace("-", " ").title()
@@ -42,6 +42,7 @@ class EnumV2(Enum):
42
42
  """
43
43
  return self._description_
44
44
 
45
+
45
46
  class Operator(EnumV2):
46
47
  equal = ("eq", "value is equals to")
47
48
  unequal = ("ne", "value isn't equals to")
@@ -61,9 +62,9 @@ class FilterOption(EnumV2):
61
62
  mustnt = ("mustnt", "List of filter mustn't exact")
62
63
  should = ("should", "List of filter should exact")
63
64
  shouldnt = ("shouldnt", "List of filter shouldn't exact")
64
-
65
65
 
66
- class LocationLevel(str, EnumV2):
66
+
67
+ class LocationLevel(EnumV2):
67
68
  CONTINENT = ("continent", "Continent level data")
68
69
  COUNTRY = ("country", "Country level data")
69
70
  PROVINCE = ("province", "Province level data")
@@ -72,9 +73,9 @@ class LocationLevel(str, EnumV2):
72
73
  SUBDISTRICT = ("subdistrict", "Subdistrict level data")
73
74
 
74
75
 
75
- class MedallionTypes(str, EnumV2):
76
- LAKE = ("lake", 'Lake data')
77
- BRONZE = ("bronze", 'bronze level Medallion')
78
- SILVER = ("silver", 'silver level Medallion')
79
- GOLD = ("gold", 'gold level Medallion')
80
- OTHER = ("other", 'other than any level Medallion')
76
+ class MedallionTypes(EnumV2):
77
+ LAKE = ("lake", "Lake data")
78
+ BRONZE = ("bronze", "bronze level Medallion")
79
+ SILVER = ("silver", "silver level Medallion")
80
+ GOLD = ("gold", "gold level Medallion")
81
+ OTHER = ("other", "other than any level Medallion")
@@ -1 +0,0 @@
1
- from .enums import *