tracktolib 0.63.0__py3-none-any.whl → 0.65.0__py3-none-any.whl

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.
tracktolib/pg/query.py CHANGED
@@ -160,12 +160,34 @@ class PGInsertQuery(PGQuery):
160
160
  def is_returning(self):
161
161
  return self.returning is not None
162
162
 
163
+ def _get_values_query(self) -> str:
164
+ """Generate the VALUES clause for the query."""
165
+ _columns = self.columns
166
+ num_cols = len(_columns)
167
+
168
+ if len(self.items) == 1 or not self.is_returning:
169
+ # Single row or no returning: simple placeholders
170
+ return ", ".join(f"${i + 1}" for i in range(num_cols))
171
+ else:
172
+ # Multiple rows with returning: generate all value groups
173
+ value_groups = []
174
+ for row_idx in range(len(self.items)):
175
+ offset = row_idx * num_cols
176
+ group = ", ".join(f"${offset + i + 1}" for i in range(num_cols))
177
+ value_groups.append(f"({group})")
178
+ return ", ".join(value_groups)
179
+
163
180
  @property
164
181
  def query(self) -> str:
165
182
  _columns = self.columns
166
- _values = ", ".join(f"${i + 1}" for i, _ in enumerate(_columns))
183
+ _values = self._get_values_query()
167
184
 
168
- query = _get_insert_query(self.table, _columns, _values)
185
+ if len(self.items) > 1 and self.is_returning:
186
+ # For multi-row insert with returning, build full VALUES clause
187
+ _columns_str = ", ".join(_columns)
188
+ query = f"INSERT INTO {self.table} AS t ({_columns_str}) VALUES {_values}"
189
+ else:
190
+ query = _get_insert_query(self.table, _columns, _values)
169
191
 
170
192
  # Conflict
171
193
  if self.on_conflict:
@@ -184,13 +206,14 @@ class PGInsertQuery(PGQuery):
184
206
  if self.returning is not None:
185
207
  if self.returning.returning_ids is None:
186
208
  raise ValueError("No returning ids found")
187
- if len(self.items) == 1:
188
- query = _get_returning_query(query, self.returning.returning_ids)
189
- else:
190
- raise NotImplementedError("Cannot return value when inserting many.")
209
+ query = _get_returning_query(query, self.returning.returning_ids)
191
210
 
192
211
  return query
193
212
 
213
+ def _get_flat_values(self) -> list:
214
+ """Get all values as a flat list for multi-row insert with returning."""
215
+ return [val for item_values in self.iter_values() for val in item_values]
216
+
194
217
 
195
218
  def get_update_fields(
196
219
  item: dict,
@@ -341,6 +364,7 @@ async def insert_one(
341
364
  await query.run(conn)
342
365
 
343
366
 
367
+ @overload
344
368
  async def insert_many(
345
369
  conn: _Connection,
346
370
  table: str,
@@ -349,12 +373,53 @@ async def insert_many(
349
373
  on_conflict: OnConflict | None = None,
350
374
  fill: bool = False,
351
375
  quote_columns: bool = False,
376
+ returning: None = None,
352
377
  query_callback: QueryCallback[PGInsertQuery] | None = None,
353
- ):
354
- query = insert_pg(table=table, items=items, on_conflict=on_conflict, fill=fill, quote_columns=quote_columns)
378
+ ) -> None: ...
379
+
380
+
381
+ @overload
382
+ async def insert_many(
383
+ conn: _Connection,
384
+ table: str,
385
+ items: list[dict],
386
+ *,
387
+ on_conflict: OnConflict | None = None,
388
+ fill: bool = False,
389
+ quote_columns: bool = False,
390
+ returning: str | list[str],
391
+ query_callback: QueryCallback[PGInsertQuery] | None = None,
392
+ ) -> list[asyncpg.Record]: ...
393
+
394
+
395
+ async def insert_many(
396
+ conn: _Connection,
397
+ table: str,
398
+ items: list[dict],
399
+ *,
400
+ on_conflict: OnConflict | None = None,
401
+ fill: bool = False,
402
+ quote_columns: bool = False,
403
+ returning: str | list[str] | None = None,
404
+ query_callback: QueryCallback[PGInsertQuery] | None = None,
405
+ ) -> list[asyncpg.Record] | None:
406
+ returning_values = [returning] if isinstance(returning, str) else returning
407
+ query = insert_pg(
408
+ table=table,
409
+ items=items,
410
+ on_conflict=on_conflict,
411
+ fill=fill,
412
+ quote_columns=quote_columns,
413
+ returning=returning_values,
414
+ )
355
415
  if query_callback is not None:
356
416
  query_callback(query)
357
- await query.run(conn)
417
+
418
+ if returning is not None:
419
+ return await conn.fetch(query.query, *query._get_flat_values())
420
+ else:
421
+ await query.run(conn)
422
+ return None
358
423
 
359
424
 
360
425
  @overload
@@ -381,22 +446,41 @@ async def insert_returning(
381
446
  ) -> asyncpg.Record | None: ...
382
447
 
383
448
 
449
+ @overload
384
450
  async def insert_returning(
385
451
  conn: _Connection,
386
452
  table: str,
387
- item: dict,
453
+ item: list[dict],
454
+ returning: str | list[str],
455
+ on_conflict: OnConflict | None = None,
456
+ fill: bool = False,
457
+ query_callback: QueryCallback[PGInsertQuery] | None = None,
458
+ ) -> list[asyncpg.Record]: ...
459
+
460
+
461
+ async def insert_returning(
462
+ conn: _Connection,
463
+ table: str,
464
+ item: dict | list[dict],
388
465
  returning: list[str] | str,
389
466
  on_conflict: OnConflict | None = None,
390
467
  fill: bool = False,
391
468
  query_callback: QueryCallback[PGInsertQuery] | None = None,
392
- ) -> asyncpg.Record | Any | None:
469
+ ) -> asyncpg.Record | Any | list[asyncpg.Record] | None:
393
470
  returning_values = [returning] if isinstance(returning, str) else returning
394
- query = insert_pg(table=table, items=[item], on_conflict=on_conflict, fill=fill, returning=returning_values)
471
+ items = item if isinstance(item, list) else [item]
472
+
473
+ query = insert_pg(table=table, items=items, on_conflict=on_conflict, fill=fill, returning=returning_values)
395
474
  if query_callback is not None:
396
475
  query_callback(query)
397
- fn = conn.fetchval if len(returning_values) == 1 and returning != "*" else conn.fetchrow
398
476
 
399
- return await fn(query.query, *query.values)
477
+ if len(items) > 1:
478
+ # Multi-row insert: use fetch() with flat values
479
+ return await conn.fetch(query.query, *query._get_flat_values())
480
+ else:
481
+ # Single row insert: use fetchval or fetchrow
482
+ fn = conn.fetchval if len(returning_values) == 1 and returning != "*" else conn.fetchrow
483
+ return await fn(query.query, *query.values)
400
484
 
401
485
 
402
486
  async def fetch_count(conn: _Connection, query: str, *args) -> int:
@@ -1,11 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tracktolib
3
- Version: 0.63.0
3
+ Version: 0.65.0
4
4
  Summary: Utility library for python
5
5
  Keywords: utility
6
6
  Author-email: julien.brayere@tracktor.fr
7
7
  License-Expression: MIT
8
8
  Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
9
13
  Requires-Dist: fastapi>=0.103.2 ; extra == 'api'
10
14
  Requires-Dist: pydantic>=2 ; extra == 'api'
11
15
  Requires-Dist: httpx>=0.25.0 ; extra == 'http'
@@ -36,21 +40,25 @@ Description-Content-Type: text/markdown
36
40
  [![Latest PyPI version](https://img.shields.io/pypi/v/tracktolib?logo=pypi)](https://pypi.python.org/pypi/tracktolib)
37
41
  [![CircleCI](https://circleci.com/gh/Tracktor/tracktolib/tree/master.svg?style=shield)](https://app.circleci.com/pipelines/github/Tracktor/tracktolib?branch=master)
38
42
 
39
- Utility library for python
43
+ Utility library for Python 3.12+
40
44
 
41
- # Installation
45
+ ## Installation
42
46
 
43
- Just run:
47
+ ```bash
48
+ uv add tracktolib
49
+ ```
50
+
51
+ With specific extras:
44
52
 
45
53
  ```bash
46
- uv add tracktolib@latest
54
+ uv add tracktolib[pg,api]
47
55
  ```
48
56
 
49
- # Utilities
57
+ ## Modules
50
58
 
51
- - **log**
59
+ ### logs
52
60
 
53
- Utility functions for logging.
61
+ Utility functions to initialize logging formatting and streams.
54
62
 
55
63
  ```python
56
64
  import logging
@@ -60,56 +68,87 @@ logger = logging.getLogger()
60
68
  formatter, stream_handler = init_logging(logger, 'json', version='0.0.1')
61
69
  ```
62
70
 
63
- - **pg**
71
+ ### pg
64
72
 
65
- Utility functions for [asyncpg](https://github.com/MagicStack/asyncpg)
73
+ Async PostgreSQL helpers using [asyncpg](https://github.com/MagicStack/asyncpg).
66
74
 
67
- - **pg-sync**
75
+ ```bash
76
+ uv add tracktolib[pg]
77
+ ```
68
78
 
69
- Utility functions based on psycopg such as `fetch_one`, `insert_many`, `fetch_count` ...
79
+ ### pg-sync
70
80
 
71
- To use the functions, create a `Connection` using psycopg: `conn = psycopg2.connect()`
81
+ Sync PostgreSQL helpers using [psycopg](https://www.psycopg.org/psycopg3/) (v3).
72
82
 
73
- *fetch_one*
83
+ ```bash
84
+ uv add tracktolib[pg-sync]
85
+ ```
74
86
 
75
87
  ```python
76
- from tracktolib.pg.pg_sync import (
77
- insert_many, fetch_one, fetch_count, fetch_all
78
- )
88
+ from psycopg import connect
89
+ from tracktolib.pg_sync import insert_many, fetch_one, fetch_count, fetch_all
90
+
91
+ conn = connect('postgresql://user:pass@localhost/db')
79
92
 
80
93
  data = [
81
94
  {'foo': 'bar', 'value': 1},
82
95
  {'foo': 'baz', 'value': 2}
83
96
  ]
84
- insert_many(conn, 'public.test', data) # Will insert the 2 dict
97
+ insert_many(conn, 'public.test', data)
98
+
85
99
  query = 'SELECT foo from public.test order by value asc'
86
- value = fetch_one(conn, query, required=True) # Will return {'foo': 'bar'}, raise an error is not found
100
+ value = fetch_one(conn, query, required=True) # {'foo': 'bar'}, raises if not found
101
+
87
102
  assert fetch_count(conn, 'public.test') == 2
103
+
88
104
  query = 'SELECT * from public.test order by value asc'
89
105
  assert fetch_all(conn, query) == data
106
+ ```
107
+
108
+ ### s3
109
+
110
+ Async S3 helpers using [aiobotocore](https://github.com/aio-libs/aiobotocore).
90
111
 
112
+ ```bash
113
+ uv add tracktolib[s3]
91
114
  ```
92
115
 
93
- - **tests**
116
+ ### s3-minio
94
117
 
95
- Utility functions for testing
118
+ S3 helpers using [minio](https://min.io/docs/minio/linux/developers/python/API.html).
96
119
 
97
- - **s3-minio**
120
+ ```bash
121
+ uv add tracktolib[s3-minio]
122
+ ```
98
123
 
99
- Utility functions for [minio](https://min.io/docs/minio/linux/developers/python/API.html)
124
+ ### http
100
125
 
101
- - **s3**
126
+ HTTP client helpers using [httpx](https://www.python-httpx.org/).
102
127
 
103
- Utility functions for [aiobotocore](https://github.com/aio-libs/aiobotocore)
128
+ ```bash
129
+ uv add tracktolib[http]
130
+ ```
104
131
 
105
- - **logs**
132
+ ### api
106
133
 
107
- Utility functions to initialize the logging formatting and streams
134
+ FastAPI utilities using [fastapi](https://fastapi.tiangolo.com/) and [pydantic](https://docs.pydantic.dev/).
108
135
 
109
- - **http**
136
+ ```bash
137
+ uv add tracktolib[api]
138
+ ```
110
139
 
111
- Utility functions using [httpx](https://www.python-httpx.org/)
140
+ ### notion
112
141
 
113
- - **api**
142
+ Notion API helpers using [niquests](https://github.com/jawah/niquests).
114
143
 
115
- Utility functions using [fastapi](https://fastapi.tiangolo.com/)
144
+ ```bash
145
+ uv add tracktolib[notion]
146
+ ```
147
+
148
+ ### tests
149
+
150
+ Testing utilities using [deepdiff](https://github.com/seperman/deepdiff).
151
+
152
+ ```bash
153
+ uv add tracktolib[tests]
154
+ ```
@@ -6,7 +6,7 @@ tracktolib/notion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
6
6
  tracktolib/notion/fetch.py,sha256=zIhFo9T18eS75l0wZN_WaXD51OUxTsgvFuvqk3R87o4,9974
7
7
  tracktolib/notion/models.py,sha256=D-px7Ht2xoXKKZICuEkZhRwVBvVCaKCvhpzPtVVYawI,5390
8
8
  tracktolib/pg/__init__.py,sha256=Ul_hgwvTXZvQBt7sHKi4ZI-0DDpnXmoFtmVkGRy-1J0,366
9
- tracktolib/pg/query.py,sha256=_wL9MQU_z8Sk0ZOYGVE0TjUbwBZ1OJJuEq2jlmWoeeM,16693
9
+ tracktolib/pg/query.py,sha256=zstc-QkBby7e6LybS8ed0d_6QLQNujY2H0lLNXFLNQ8,19366
10
10
  tracktolib/pg/utils.py,sha256=ygQn63EBDaEGB0p7P2ibellO2mv-StafanpXKcCUiZU,6324
11
11
  tracktolib/pg_sync.py,sha256=MKDaV7dYsRy59Y0EE5RGZL0DlZ-RUdBeaN9eSBwiQJg,6718
12
12
  tracktolib/pg_utils.py,sha256=ArYNdf9qsdYdzGEWmev8tZpyx8_1jaGGdkfYkauM7UM,2582
@@ -15,6 +15,6 @@ tracktolib/s3/minio.py,sha256=wMEjkSes9Fp39fD17IctALpD6zB2xwDRQEmO7Vzan3g,1387
15
15
  tracktolib/s3/s3.py,sha256=0HbSAPoaup5-W4LK54zRCjrQ5mr8OWR-N9WjW99Q4aw,5937
16
16
  tracktolib/tests.py,sha256=gKE--epQjgMZGXc5ydbl4zjOdmwztJS42UMV0p4hXEA,399
17
17
  tracktolib/utils.py,sha256=ysTBF9V35fVXQVBPk0kfE_84SGRxzrayqmg9RbtoJq4,5761
18
- tracktolib-0.63.0.dist-info/WHEEL,sha256=z-mOpxbJHqy3cq6SvUThBZdaLGFZzdZPtgWLcP2NKjQ,79
19
- tracktolib-0.63.0.dist-info/METADATA,sha256=q3HcfLHVoybHIDc4Bgayt3kYd5v1spd62OCGyxjCfZw,3128
20
- tracktolib-0.63.0.dist-info/RECORD,,
18
+ tracktolib-0.65.0.dist-info/WHEEL,sha256=z-mOpxbJHqy3cq6SvUThBZdaLGFZzdZPtgWLcP2NKjQ,79
19
+ tracktolib-0.65.0.dist-info/METADATA,sha256=4Pqiq-4w38C6n7eYaAikT-qo4D_3iie3ueSQsjXGI54,3719
20
+ tracktolib-0.65.0.dist-info/RECORD,,