python-ppa 0.1.6__tar.gz → 0.1.8__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.
@@ -0,0 +1,283 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-ppa
3
+ Version: 0.1.8
4
+ Summary: JPA-like ORM for Python
5
+ Author: Pratyush Kumar
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.13
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: annotated-types==0.7.0
11
+ Requires-Dist: dnspython==2.8.0
12
+ Requires-Dist: email-validator==2.3.0
13
+ Requires-Dist: idna==3.18
14
+ Requires-Dist: pydantic==2.13.4
15
+ Requires-Dist: pydantic_core==2.46.4
16
+ Requires-Dist: psycopg[binary]>=3.2.0
17
+ Requires-Dist: pymongo==4.17.0
18
+ Requires-Dist: python-dotenv==1.2.2
19
+ Requires-Dist: PyYAML==6.0.3
20
+ Requires-Dist: typing-inspection==0.4.2
21
+ Requires-Dist: typing_extensions==4.15.0
22
+ Dynamic: license-file
23
+
24
+ # python-ppa
25
+
26
+ `python-ppa` is a lightweight, Spring Data ppa-inspired persistence framework for MongoDB and PostgreSQL built on top of **Python 3.13+**, **Pydantic v2**, **PyMongo**, and **psycopg**.
27
+
28
+ It uses advanced Python metaprogramming (`metaclass`) to dynamically generate MongoDB queries at runtime based on method naming conventions or explicit query declarations, drastically reducing boilerplate code.
29
+
30
+ ---
31
+
32
+ ## 🚀 Features
33
+
34
+ * **ppa-Like Repository Pattern:** Declare an interface, and let the metaclass handle query assembly dynamically.
35
+ * **Query Naming Conventions:** Instantly supports `find_by_*`, `find_all_by_*`, `exists_by_*`, and `count_by_*` pattern resolutions.
36
+ * **Custom Query Annotations:** Bind complex MongoDB or PostgreSQL query templates with custom parameter injections using the `@query` decorator.
37
+ * **Data Validation:** Fully powered by Pydantic v2 for robust runtime type checking and parsing.
38
+ * **Zero-Boilerplate ID Mapping:** Automatically handles conversions between stringified hexadecimal keys and native BSON `ObjectId` footprints.
39
+ * **Environment Profiles:** Flexible configurations handling multi-stage deployment environments (`dev`, `prod`, etc.) via YAML interpolation.
40
+
41
+ ---
42
+
43
+ ## 📁 Directory Structure
44
+
45
+ ```text
46
+ .
47
+ ├── README.md
48
+ ├── ppa
49
+ │ ├── __init__.py
50
+ │ ├── config.py # App bootstrapping, YAML parsing & environment mapping
51
+ │ ├── mongo
52
+ │ ├── __init__.py
53
+ │ └── interface.py # Repository core interface, custom metadata, and queries
54
+ │ └── postgres
55
+ │ ├── __init__.py
56
+ │ └── interface.py # PostgreSQL repository interface and SQL query conventions
57
+ ├── pyproject.toml
58
+ └── requirements.txt
59
+ ```
60
+
61
+ ## 🛠️ Getting Started
62
+
63
+ ### Prerequisites
64
+
65
+ * Python 3.13 or higher
66
+
67
+ ### Installation & Virtual Env Setup
68
+
69
+ 1. Clone the repository and navigate to its root: `cd python-ppa`
70
+ 2. Spin up a virtual environment and update your pip core dependencies:
71
+
72
+ ```bash
73
+ python3 -m venv venv
74
+ source venv/bin/activate
75
+ pip install --upgrade pip
76
+ ```
77
+
78
+ 3. Install the project library dependencies:
79
+
80
+ ```bash
81
+ pip install -r requirements.txt
82
+ ```
83
+
84
+ ## ⚙️ Configuration Management
85
+
86
+ The framework supports multi-profile YAML configurations with real-time environment substitution syntax (e.g., ${ENV_VAR:default_value}).
87
+ Create a `resources/` directory at the root of your execution workspace and include your application sheets.
88
+
89
+ 1. Master Configuration (`resources/settings.yml`)
90
+
91
+ ```yaml
92
+ app:
93
+ profile: ${APP_PROFILE:dev} # Swaps profile to target environment configuration
94
+ ```
95
+
96
+ 2. Environment Profile Configuration (`resources/settings-dev.yml`)
97
+
98
+ ```yaml
99
+ mongodb:
100
+ uri: ${MONGO_URI:mongodb://localhost:27017}
101
+ database: ${MONGO_DB_NAME:ppa_database}
102
+
103
+ # Or configure PostgreSQL instead.
104
+ postgresql:
105
+ dsn: ${POSTGRES_DSN:postgresql://postgres:postgres@localhost:5432/ppa_database}
106
+
107
+ # Equivalent field-based PostgreSQL configuration:
108
+ # postgresql:
109
+ # host: ${POSTGRES_HOST:localhost}
110
+ # port: ${POSTGRES_PORT:5432}
111
+ # database: ${POSTGRES_DB_NAME:ppa_database}
112
+ # user: ${POSTGRES_USER:postgres}
113
+ # password: ${POSTGRES_PASSWORD:postgres}
114
+
115
+ framework:
116
+ logging:
117
+ enabled: true
118
+ level: "DEBUG"
119
+ ```
120
+
121
+ ## PostgreSQL Quickstart
122
+
123
+ The `demo/` folder contains a complete local PostgreSQL verification app.
124
+
125
+ 1. Start PostgreSQL:
126
+
127
+ ```bash
128
+ cd demo
129
+ docker compose up -d
130
+ ```
131
+
132
+ 2. Run the demo from the repository root:
133
+
134
+ ```bash
135
+ ./venv/bin/python demo/main.py
136
+ ```
137
+
138
+ The demo creates a `users` table and verifies insert, convention queries, a custom SQL query, update, and delete through `ppa.postgres`.
139
+
140
+ For your own app, create `resources/settings.yml`:
141
+
142
+ ```yaml
143
+ app:
144
+ profile: ${APP_PROFILE:dev}
145
+ ```
146
+
147
+ Then create `resources/settings-dev.yml`:
148
+
149
+ ```yaml
150
+ postgresql:
151
+ host: ${POSTGRES_HOST:localhost}
152
+ port: ${POSTGRES_PORT:5432}
153
+ database: ${POSTGRES_DB_NAME:ppa_demo}
154
+ user: ${POSTGRES_USER:ppa_user}
155
+ password: ${POSTGRES_PASSWORD:ppa_password}
156
+
157
+ framework:
158
+ logging:
159
+ enabled: true
160
+ level: DEBUG
161
+ ```
162
+
163
+ You can also use a single DSN:
164
+
165
+ ```yaml
166
+ postgresql:
167
+ dsn: ${POSTGRES_DSN:postgresql://ppa_user:ppa_password@localhost:5432/ppa_demo}
168
+ ```
169
+
170
+ Create the database table before using the repository. `python-ppa` currently maps models to existing tables; it does not run migrations for you.
171
+
172
+ ```sql
173
+ CREATE TABLE IF NOT EXISTS users (
174
+ id SERIAL PRIMARY KEY,
175
+ username TEXT NOT NULL UNIQUE,
176
+ email TEXT NOT NULL,
177
+ age INTEGER NOT NULL
178
+ );
179
+ ```
180
+
181
+ Define a PostgreSQL-backed entity and repository:
182
+
183
+ ```python
184
+ from ppa.postgres import EntityModel, IRepository, query, table
185
+
186
+ @table(name="users")
187
+ class User(EntityModel):
188
+ username: str
189
+ email: str
190
+ age: int
191
+
192
+ class UserRepository(IRepository[User]):
193
+ def find_by_username(self, username: str) -> User | None: ...
194
+ def exists_by_username(self, username: str) -> bool: ...
195
+ def count_by_age(self, age: int) -> int: ...
196
+ def find_all_by_age(self, age: int) -> list[User]: ...
197
+
198
+ @query("SELECT * FROM users WHERE email = ?0 AND age >= ?1")
199
+ def find_by_email_and_min_age(self, email: str, min_age: int) -> list[User]: ...
200
+ ```
201
+
202
+ Use the repository:
203
+
204
+ ```python
205
+ from ppa.config import close_db_connection
206
+
207
+ repo = UserRepository()
208
+
209
+ user_id = repo.save(User(username="pratyush", email="pratyush@example.com", age=25))
210
+ user = repo.find_by_username("pratyush")
211
+
212
+ print(user.model_dump() if user else None)
213
+ print(repo.exists_by_username("pratyush"))
214
+ print(repo.count_by_age(25))
215
+ print(repo.find_by_email_and_min_age("pratyush@example.com", 21))
216
+
217
+ repo.update(user_id, {"age": 26})
218
+ repo.delete(user_id)
219
+
220
+ close_db_connection()
221
+ ```
222
+
223
+ ## MongoDB Usage Example
224
+
225
+ Here is a quick overview of how you can configure a domain entity and generate auto-implemented interface pipelines:
226
+
227
+ 1. Define your Document Model
228
+
229
+ ```python
230
+ from pydantic import Field
231
+ from ppa.mongo.interface import DocumentModel, document
232
+
233
+ @document(name="users")
234
+ class User(DocumentModel):
235
+ id: str = Field(alias="_id", default=None)
236
+ username: str
237
+ email: str
238
+ age: int
239
+ ```
240
+
241
+ 2. Declare your Interface Repository
242
+
243
+ By extending IRepository[T], method naming patterns are captured and converted into database interactions seamlessly.
244
+
245
+ ```python
246
+ from ppa.mongo.interface import IRepository, query
247
+ from typing import List, Optional
248
+
249
+ class UserRepository(IRepository[User]):
250
+
251
+ # 1. Query generation by structural method naming convention
252
+ def find_by_username(self, username: str) -> Optional[User]: ...
253
+
254
+ def count_by_age(self, age: int) -> int: ...
255
+
256
+ # 2. Templated declaration using placeholder substitutions
257
+ @query(definition={"email": "?0", "age": {"$gte": "?1"}})
258
+ def find_by_email_and_min_age(self, email: str, min_age: int) -> List[User]: ...
259
+ ```
260
+
261
+ 3. Execute CRUD Actions
262
+
263
+ ```python
264
+ from ppa.config import close_db_connection
265
+
266
+ # Initialize repository instance
267
+ user_repo = UserRepository()
268
+
269
+ # Save a document
270
+ new_user = User(username="pratyush", email="pratyush@example.com", age=25)
271
+ user_id = user_repo.save(new_user)
272
+
273
+ # Fetch using automatic query generation
274
+ user = user_repo.find_by_username("pratyush")
275
+ print(f"Found User: {user.email if user else 'Not Found'}")
276
+
277
+ # Clean up connections on process termination
278
+ close_db_connection()
279
+ ```
280
+
281
+ ## 📜 License
282
+
283
+ This project is open-source software licensed under the MIT License.
@@ -0,0 +1,260 @@
1
+ # python-ppa
2
+
3
+ `python-ppa` is a lightweight, Spring Data ppa-inspired persistence framework for MongoDB and PostgreSQL built on top of **Python 3.13+**, **Pydantic v2**, **PyMongo**, and **psycopg**.
4
+
5
+ It uses advanced Python metaprogramming (`metaclass`) to dynamically generate MongoDB queries at runtime based on method naming conventions or explicit query declarations, drastically reducing boilerplate code.
6
+
7
+ ---
8
+
9
+ ## 🚀 Features
10
+
11
+ * **ppa-Like Repository Pattern:** Declare an interface, and let the metaclass handle query assembly dynamically.
12
+ * **Query Naming Conventions:** Instantly supports `find_by_*`, `find_all_by_*`, `exists_by_*`, and `count_by_*` pattern resolutions.
13
+ * **Custom Query Annotations:** Bind complex MongoDB or PostgreSQL query templates with custom parameter injections using the `@query` decorator.
14
+ * **Data Validation:** Fully powered by Pydantic v2 for robust runtime type checking and parsing.
15
+ * **Zero-Boilerplate ID Mapping:** Automatically handles conversions between stringified hexadecimal keys and native BSON `ObjectId` footprints.
16
+ * **Environment Profiles:** Flexible configurations handling multi-stage deployment environments (`dev`, `prod`, etc.) via YAML interpolation.
17
+
18
+ ---
19
+
20
+ ## 📁 Directory Structure
21
+
22
+ ```text
23
+ .
24
+ ├── README.md
25
+ ├── ppa
26
+ │ ├── __init__.py
27
+ │ ├── config.py # App bootstrapping, YAML parsing & environment mapping
28
+ │ ├── mongo
29
+ │ ├── __init__.py
30
+ │ └── interface.py # Repository core interface, custom metadata, and queries
31
+ │ └── postgres
32
+ │ ├── __init__.py
33
+ │ └── interface.py # PostgreSQL repository interface and SQL query conventions
34
+ ├── pyproject.toml
35
+ └── requirements.txt
36
+ ```
37
+
38
+ ## 🛠️ Getting Started
39
+
40
+ ### Prerequisites
41
+
42
+ * Python 3.13 or higher
43
+
44
+ ### Installation & Virtual Env Setup
45
+
46
+ 1. Clone the repository and navigate to its root: `cd python-ppa`
47
+ 2. Spin up a virtual environment and update your pip core dependencies:
48
+
49
+ ```bash
50
+ python3 -m venv venv
51
+ source venv/bin/activate
52
+ pip install --upgrade pip
53
+ ```
54
+
55
+ 3. Install the project library dependencies:
56
+
57
+ ```bash
58
+ pip install -r requirements.txt
59
+ ```
60
+
61
+ ## ⚙️ Configuration Management
62
+
63
+ The framework supports multi-profile YAML configurations with real-time environment substitution syntax (e.g., ${ENV_VAR:default_value}).
64
+ Create a `resources/` directory at the root of your execution workspace and include your application sheets.
65
+
66
+ 1. Master Configuration (`resources/settings.yml`)
67
+
68
+ ```yaml
69
+ app:
70
+ profile: ${APP_PROFILE:dev} # Swaps profile to target environment configuration
71
+ ```
72
+
73
+ 2. Environment Profile Configuration (`resources/settings-dev.yml`)
74
+
75
+ ```yaml
76
+ mongodb:
77
+ uri: ${MONGO_URI:mongodb://localhost:27017}
78
+ database: ${MONGO_DB_NAME:ppa_database}
79
+
80
+ # Or configure PostgreSQL instead.
81
+ postgresql:
82
+ dsn: ${POSTGRES_DSN:postgresql://postgres:postgres@localhost:5432/ppa_database}
83
+
84
+ # Equivalent field-based PostgreSQL configuration:
85
+ # postgresql:
86
+ # host: ${POSTGRES_HOST:localhost}
87
+ # port: ${POSTGRES_PORT:5432}
88
+ # database: ${POSTGRES_DB_NAME:ppa_database}
89
+ # user: ${POSTGRES_USER:postgres}
90
+ # password: ${POSTGRES_PASSWORD:postgres}
91
+
92
+ framework:
93
+ logging:
94
+ enabled: true
95
+ level: "DEBUG"
96
+ ```
97
+
98
+ ## PostgreSQL Quickstart
99
+
100
+ The `demo/` folder contains a complete local PostgreSQL verification app.
101
+
102
+ 1. Start PostgreSQL:
103
+
104
+ ```bash
105
+ cd demo
106
+ docker compose up -d
107
+ ```
108
+
109
+ 2. Run the demo from the repository root:
110
+
111
+ ```bash
112
+ ./venv/bin/python demo/main.py
113
+ ```
114
+
115
+ The demo creates a `users` table and verifies insert, convention queries, a custom SQL query, update, and delete through `ppa.postgres`.
116
+
117
+ For your own app, create `resources/settings.yml`:
118
+
119
+ ```yaml
120
+ app:
121
+ profile: ${APP_PROFILE:dev}
122
+ ```
123
+
124
+ Then create `resources/settings-dev.yml`:
125
+
126
+ ```yaml
127
+ postgresql:
128
+ host: ${POSTGRES_HOST:localhost}
129
+ port: ${POSTGRES_PORT:5432}
130
+ database: ${POSTGRES_DB_NAME:ppa_demo}
131
+ user: ${POSTGRES_USER:ppa_user}
132
+ password: ${POSTGRES_PASSWORD:ppa_password}
133
+
134
+ framework:
135
+ logging:
136
+ enabled: true
137
+ level: DEBUG
138
+ ```
139
+
140
+ You can also use a single DSN:
141
+
142
+ ```yaml
143
+ postgresql:
144
+ dsn: ${POSTGRES_DSN:postgresql://ppa_user:ppa_password@localhost:5432/ppa_demo}
145
+ ```
146
+
147
+ Create the database table before using the repository. `python-ppa` currently maps models to existing tables; it does not run migrations for you.
148
+
149
+ ```sql
150
+ CREATE TABLE IF NOT EXISTS users (
151
+ id SERIAL PRIMARY KEY,
152
+ username TEXT NOT NULL UNIQUE,
153
+ email TEXT NOT NULL,
154
+ age INTEGER NOT NULL
155
+ );
156
+ ```
157
+
158
+ Define a PostgreSQL-backed entity and repository:
159
+
160
+ ```python
161
+ from ppa.postgres import EntityModel, IRepository, query, table
162
+
163
+ @table(name="users")
164
+ class User(EntityModel):
165
+ username: str
166
+ email: str
167
+ age: int
168
+
169
+ class UserRepository(IRepository[User]):
170
+ def find_by_username(self, username: str) -> User | None: ...
171
+ def exists_by_username(self, username: str) -> bool: ...
172
+ def count_by_age(self, age: int) -> int: ...
173
+ def find_all_by_age(self, age: int) -> list[User]: ...
174
+
175
+ @query("SELECT * FROM users WHERE email = ?0 AND age >= ?1")
176
+ def find_by_email_and_min_age(self, email: str, min_age: int) -> list[User]: ...
177
+ ```
178
+
179
+ Use the repository:
180
+
181
+ ```python
182
+ from ppa.config import close_db_connection
183
+
184
+ repo = UserRepository()
185
+
186
+ user_id = repo.save(User(username="pratyush", email="pratyush@example.com", age=25))
187
+ user = repo.find_by_username("pratyush")
188
+
189
+ print(user.model_dump() if user else None)
190
+ print(repo.exists_by_username("pratyush"))
191
+ print(repo.count_by_age(25))
192
+ print(repo.find_by_email_and_min_age("pratyush@example.com", 21))
193
+
194
+ repo.update(user_id, {"age": 26})
195
+ repo.delete(user_id)
196
+
197
+ close_db_connection()
198
+ ```
199
+
200
+ ## MongoDB Usage Example
201
+
202
+ Here is a quick overview of how you can configure a domain entity and generate auto-implemented interface pipelines:
203
+
204
+ 1. Define your Document Model
205
+
206
+ ```python
207
+ from pydantic import Field
208
+ from ppa.mongo.interface import DocumentModel, document
209
+
210
+ @document(name="users")
211
+ class User(DocumentModel):
212
+ id: str = Field(alias="_id", default=None)
213
+ username: str
214
+ email: str
215
+ age: int
216
+ ```
217
+
218
+ 2. Declare your Interface Repository
219
+
220
+ By extending IRepository[T], method naming patterns are captured and converted into database interactions seamlessly.
221
+
222
+ ```python
223
+ from ppa.mongo.interface import IRepository, query
224
+ from typing import List, Optional
225
+
226
+ class UserRepository(IRepository[User]):
227
+
228
+ # 1. Query generation by structural method naming convention
229
+ def find_by_username(self, username: str) -> Optional[User]: ...
230
+
231
+ def count_by_age(self, age: int) -> int: ...
232
+
233
+ # 2. Templated declaration using placeholder substitutions
234
+ @query(definition={"email": "?0", "age": {"$gte": "?1"}})
235
+ def find_by_email_and_min_age(self, email: str, min_age: int) -> List[User]: ...
236
+ ```
237
+
238
+ 3. Execute CRUD Actions
239
+
240
+ ```python
241
+ from ppa.config import close_db_connection
242
+
243
+ # Initialize repository instance
244
+ user_repo = UserRepository()
245
+
246
+ # Save a document
247
+ new_user = User(username="pratyush", email="pratyush@example.com", age=25)
248
+ user_id = user_repo.save(new_user)
249
+
250
+ # Fetch using automatic query generation
251
+ user = user_repo.find_by_username("pratyush")
252
+ print(f"Found User: {user.email if user else 'Not Found'}")
253
+
254
+ # Clean up connections on process termination
255
+ close_db_connection()
256
+ ```
257
+
258
+ ## 📜 License
259
+
260
+ This project is open-source software licensed under the MIT License.
@@ -0,0 +1,3 @@
1
+ from ppa import mongo
2
+ from ppa import postgres
3
+ from ppa import config
@@ -96,16 +96,33 @@ class ConfigSettings:
96
96
 
97
97
  self._config_data = _load_and_interpolate_yaml(profile_config_path)
98
98
  mongodb_config = _as_dict(self._config_data.get("mongodb"), context=f"{profile_filename}.mongodb")
99
+ postgres_config = _as_dict(
100
+ self._config_data.get("postgresql", self._config_data.get("postgres")),
101
+ context=f"{profile_filename}.postgresql",
102
+ )
99
103
 
100
104
  # 3. Expose parameters to the framework
101
105
  self.mongo_uri = mongodb_config.get("uri")
102
106
  self.database_name = mongodb_config.get("database")
107
+ self.postgres_dsn = self._resolve_postgres_dsn(postgres_config)
108
+ self.postgres_database_name = postgres_config.get("database") or postgres_config.get("dbname")
103
109
 
104
- if not isinstance(self.mongo_uri, str) or not self.mongo_uri or not isinstance(self.database_name,
105
- str) or not self.database_name:
110
+ has_mongo_config = bool(self.mongo_uri and self.database_name)
111
+ has_postgres_config = bool(self.postgres_dsn)
112
+
113
+ if mongodb_config and not has_mongo_config:
106
114
  raise KeyError(
107
115
  f"Invalid configuration format in {profile_filename}. Missing 'mongodb.uri' or 'mongodb.database'.")
108
116
 
117
+ if postgres_config and not has_postgres_config:
118
+ raise KeyError(
119
+ f"Invalid configuration format in {profile_filename}. Provide 'postgresql.dsn' or "
120
+ "'postgresql.host', 'postgresql.database', and 'postgresql.user'.")
121
+
122
+ if not has_mongo_config and not has_postgres_config:
123
+ raise KeyError(
124
+ f"Invalid configuration format in {profile_filename}. Configure either 'mongodb' or 'postgresql'.")
125
+
109
126
  framework_config = _as_dict(self._config_data.get("framework"), context=f"{profile_filename}.framework")
110
127
  framework_logging_config = _as_dict(framework_config.get("logging"),
111
128
  context=f"{profile_filename}.framework.logging")
@@ -115,6 +132,32 @@ class ConfigSettings:
115
132
  self.framework_log_date_format = str(
116
133
  framework_logging_config.get("date_format", DEFAULT_FRAMEWORK_LOG_DATE_FORMAT))
117
134
 
135
+ @staticmethod
136
+ def _resolve_postgres_dsn(postgres_config: dict[str, Any]) -> str | None:
137
+ dsn = postgres_config.get("dsn") or postgres_config.get("uri") or postgres_config.get("url")
138
+ if isinstance(dsn, str) and dsn:
139
+ return dsn
140
+
141
+ host = postgres_config.get("host")
142
+ database = postgres_config.get("database") or postgres_config.get("dbname")
143
+ user = postgres_config.get("user") or postgres_config.get("username")
144
+ if not host or not database or not user:
145
+ return None
146
+
147
+ parts = [
148
+ f"host={host}",
149
+ f"dbname={database}",
150
+ f"user={user}",
151
+ ]
152
+ optional_fields = {
153
+ "password": postgres_config.get("password"),
154
+ "port": postgres_config.get("port"),
155
+ "sslmode": postgres_config.get("sslmode"),
156
+ "connect_timeout": postgres_config.get("connect_timeout"),
157
+ }
158
+ parts.extend(f"{key}={value}" for key, value in optional_fields.items() if value not in (None, ""))
159
+ return " ".join(parts)
160
+
118
161
 
119
162
  def _configure_framework_logger() -> logging.Logger:
120
163
  logger = logging.getLogger(FRAMEWORK_LOGGER_NAME)
@@ -144,11 +187,23 @@ def get_framework_logger(name: str | None = None) -> logging.Logger:
144
187
  settings = ConfigSettings()
145
188
  framework_logger = _configure_framework_logger()
146
189
  framework_logger.info("Bootstrapping framework with active profile [%s]", settings.profile)
147
- _mongo_client = MongoClient(settings.mongo_uri)
148
- if not settings or not settings.database_name and not isinstance(settings.database_name, str):
149
- raise ValueError("Database name must be provided and it must be a string")
150
- db = _mongo_client[settings.database_name]
151
- framework_logger.debug("Mongo client initialized for database [%s]", settings.database_name)
190
+ _mongo_client: MongoClient | None = None
191
+ db = None
192
+
193
+
194
+ def _get_mongo_client() -> MongoClient:
195
+ global _mongo_client, db
196
+ if not isinstance(settings.mongo_uri, str) or not settings.mongo_uri:
197
+ raise ValueError("MongoDB URI must be provided before using Mongo repositories")
198
+ if not isinstance(settings.database_name, str) or not settings.database_name:
199
+ raise ValueError("MongoDB database name must be provided before using Mongo repositories")
200
+
201
+ if _mongo_client is None:
202
+ _mongo_client = MongoClient(settings.mongo_uri)
203
+ db = _mongo_client[settings.database_name]
204
+ framework_logger.debug("Mongo client initialized for database [%s]", settings.database_name)
205
+
206
+ return _mongo_client
152
207
 
153
208
 
154
209
  def value(place_holder: str) -> Any:
@@ -196,10 +251,26 @@ def value(place_holder: str) -> Any:
196
251
 
197
252
  def get_collection(collection_name: str):
198
253
  """Provides repositories access to the shared single client pool."""
254
+ _get_mongo_client()
199
255
  return db[collection_name]
200
256
 
201
257
 
258
+ def get_postgres_connection():
259
+ """Creates a PostgreSQL connection using the active profile settings."""
260
+ if not isinstance(settings.postgres_dsn, str) or not settings.postgres_dsn:
261
+ raise ValueError("PostgreSQL DSN must be provided before using Postgres repositories")
262
+
263
+ try:
264
+ import psycopg
265
+ from psycopg.rows import dict_row
266
+ except ImportError as exc:
267
+ raise ImportError("Install psycopg to use PostgreSQL repositories.") from exc
268
+
269
+ return psycopg.connect(settings.postgres_dsn, row_factory=dict_row)
270
+
271
+
202
272
  def close_db_connection():
203
- """Gracefully terminates connection pool sockets on shutdown."""
204
- framework_logger.debug("Closing MongoDB client connection pool")
205
- _mongo_client.close()
273
+ """Gracefully terminates open framework connection pools."""
274
+ if _mongo_client is not None:
275
+ framework_logger.debug("Closing MongoDB client connection pool")
276
+ _mongo_client.close()
@@ -0,0 +1,5 @@
1
+ from ppa.postgres.interface import EntityList
2
+ from ppa.postgres.interface import EntityModel
3
+ from ppa.postgres.interface import IRepository
4
+ from ppa.postgres.interface import query
5
+ from ppa.postgres.interface import table