python-datastore-sqlalchemy 0.0.1__py3-none-any.whl → 0.0.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-datastore-sqlalchemy
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: SQLAlchemy dialect for google cloud datastore
5
5
  Home-page: https://github.com/splasky/python-datastore-sqlalchemy
6
6
  Author: HY Chang(splasky)
@@ -21,6 +21,13 @@ Requires-Python: >=3.9
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: sqlalchemy<3.0.0,>=1.4.16
24
+ Requires-Dist: google-cloud-datastore>=2.21.0
25
+ Requires-Dist: google-cloud-firestore>=2.21.0
26
+ Requires-Dist: google-cloud-bigquery>=3.35.0
27
+ Requires-Dist: google-auth>=2.40.0
28
+ Requires-Dist: sqlglot>=28.0.0
29
+ Requires-Dist: pandas>=2.0.0
30
+ Requires-Dist: requests
24
31
  Dynamic: author
25
32
  Dynamic: author-email
26
33
  Dynamic: classifier
@@ -41,14 +48,18 @@ How to install
41
48
  ```
42
49
  python3 setup.py install
43
50
  ```
51
+ or
52
+ ```
53
+ pip install python-datastore-sqlalchemy
54
+ ```
44
55
  How to use
45
56
  ```python
46
57
  from sqlalchemy import *
47
58
  from sqlalchemy.engine import create_engine
48
59
  from sqlalchemy.schema import *
49
- engine = create_engine('datastore://test-api-1', credentials='path/to/credentials.json')
60
+ engine = create_engine("datastore://my-project/?database=my-db", credentials='path/to/credentials.json')
50
61
  conn = engine.connect()
51
- result = conn.execute("SELECT * from test_table)
62
+ result = conn.execute("SELECT * fro" test_table")
52
63
  print(result.fetchall())
53
64
  ```
54
65
 
@@ -0,0 +1,12 @@
1
+ python_datastore_sqlalchemy-0.0.2.dist-info/licenses/LICENSE,sha256=vcsHP9P3NtGefy7ojTRCgi3w0QqF_igVlmFP5A47YVA,1093
2
+ sqlalchemy_datastore/__init__.py,sha256=UmMpE_Sn41BU88qdX5KVvJPH7wMD2SufDBDQ-2cCwNQ,1239
3
+ sqlalchemy_datastore/_helpers.py,sha256=bRvNeMz6QgJMW1M5K-dXksdnURYBZE-NIzt3tue92rg,4854
4
+ sqlalchemy_datastore/_types.py,sha256=zs-Gr1XPP-UnppzsuWHEmD-wshkuosXM7yGeUIt-VVY,5092
5
+ sqlalchemy_datastore/base.py,sha256=QpgQ19h9LA2h86TH14w4h_Ilg9_St76qrwD5r3Ln4bA,9322
6
+ sqlalchemy_datastore/datastore_dbapi.py,sha256=xJVZR0Ymzj1XjB6-UeWQRHRLLYLzlOrjaYOEv3-13lY,94404
7
+ sqlalchemy_datastore/parse_url.py,sha256=sTeNqBm-V4Dec97R4K264qvG05aUXG7iNFyKNCNkPJ8,10058
8
+ python_datastore_sqlalchemy-0.0.2.dist-info/METADATA,sha256=zQgLqKvD-e8nqx-WLy2wGXhDjcOFiUD5ibrASBsXZbs,2471
9
+ python_datastore_sqlalchemy-0.0.2.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
10
+ python_datastore_sqlalchemy-0.0.2.dist-info/entry_points.txt,sha256=h54O-P7HFeCKpoeQ-XSSyP_GVZ1FRAY3gFCS5lkDBIs,77
11
+ python_datastore_sqlalchemy-0.0.2.dist-info/top_level.txt,sha256=SE6uZfiwIQaJ1RDBfMm37dm7ZIZoqDcwQ1fDm32x0Rg,21
12
+ python_datastore_sqlalchemy-0.0.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.2)
2
+ Generator: setuptools (82.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2025 hychang <hychang.1997.tw@gmail.com>
1
+ # Copyright (c) 2025 hychang <hychang.1997.tw@gmail.com>
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining a copy of
4
4
  # this software and associated documentation files (the "Software"), to deal in
@@ -19,7 +19,7 @@
19
19
 
20
20
  from .base import CloudDatastoreDialect
21
21
 
22
- __version__ = "0.0.1"
22
+ __version__ = "0.0.2"
23
23
 
24
24
  __all__ = [
25
25
  "__version__",
@@ -60,21 +60,23 @@ def create_datastore_client(
60
60
  user_agent: Optional[client_info.ClientInfo] = None,
61
61
  database: Optional[str] = None
62
62
  ) -> Tuple[datastore.Client, Optional[Credentials]]:
63
- """Construct a BigQuery client object.
63
+ """Construct a Datastore client object.
64
64
 
65
65
  Args:
66
- credentials_info Optional[dict]:
67
- credentials_path Optional[str]:
68
- credentials_base64 Optional[str]:
69
- location (Optional[str]):
70
- Default location for jobs / datasets / tables.
66
+ credentials_info (Optional[dict]):
67
+ Service account info dictionary.
68
+ credentials_path (Optional[str]):
69
+ Path to a service account JSON key file.
70
+ credentials_base64 (Optional[str]):
71
+ Base64-encoded service account JSON key.
71
72
  project_id (Optional[str]):
72
73
  Project ID for the project which the client acts on behalf of.
73
74
  user_agent (Optional[google.api_core.client_info.ClientInfo]):
74
75
  The client info used to send a user-agent string along with API
75
- requests. If ``None``, then default info will be used. Generally,
76
- you only need to set this if you're developing your own library
77
- or partner tool.
76
+ requests. If ``None``, then default info will be used.
77
+ database (Optional[str]):
78
+ Datastore database ID. ``None`` or ``"(default)"`` uses the
79
+ default database.
78
80
  """
79
81
 
80
82
  default_project = None
@@ -17,20 +17,15 @@
17
17
  # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
18
  # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19
19
  import logging
20
- from concurrent import futures
21
20
  from typing import Any, List, Optional
22
21
 
23
22
  from google.cloud import firestore_admin_v1
24
- from google.cloud.firestore_admin_v1.types import Database
25
23
  from google.oauth2 import service_account
26
24
  from sqlalchemy import exc
27
25
  from sqlalchemy.engine import Connection, default
28
26
  from sqlalchemy.engine.interfaces import (
29
- # DBAPICursor,
30
- # _DBAPISingleExecuteParams,
31
27
  ExecutionContext,
32
28
  )
33
- from sqlalchemy.sql import Select
34
29
 
35
30
  from . import _types, datastore_dbapi
36
31
  from ._helpers import create_datastore_client
@@ -88,6 +83,7 @@ class CloudDatastoreDialect(default.DefaultDialect):
88
83
  self.location = location
89
84
  self.identifier_preparer = self.preparer(self)
90
85
  self.dataset_id = None
86
+ self.database_id = None
91
87
  self.list_tables_page_size = list_tables_page_size
92
88
  self._client = None
93
89
 
@@ -129,6 +125,7 @@ class CloudDatastoreDialect(default.DefaultDialect):
129
125
  provided_job_config,
130
126
  list_tables_page_size,
131
127
  user_supplied_client,
128
+ database_id,
132
129
  ) = parse_url(url)
133
130
 
134
131
  self.arraysize = arraysize or self.arraysize
@@ -138,6 +135,7 @@ class CloudDatastoreDialect(default.DefaultDialect):
138
135
  self.credentials_path = credentials_path
139
136
  self.credentials_base64 = credentials_base64 or self.credentials_base64
140
137
  self.dataset_id = dataset_id
138
+ self.database_id = database_id
141
139
  self.billing_project_id = self.billing_project_id or self.project_id
142
140
 
143
141
  if user_supplied_client:
@@ -148,7 +146,7 @@ class CloudDatastoreDialect(default.DefaultDialect):
148
146
  credentials_info=self.credentials_info,
149
147
  credentials_base64=self.credentials_base64,
150
148
  project_id=self.billing_project_id,
151
- database=None,
149
+ database=self.database_id,
152
150
  )
153
151
  self.project_id = self.project_id if self.project_id else client.project
154
152
  self.billing_project_id = (
@@ -162,10 +160,10 @@ class CloudDatastoreDialect(default.DefaultDialect):
162
160
 
163
161
  self._client = client
164
162
  self.credentials = credentials
165
- setattr(self._client, "credentials_path", self.credentials_path)
166
- setattr(self._client, "credentials_info", self.credentials_info)
167
- setattr(self._client, "credentials_base64", self.credentials_base64)
168
- setattr(self._client, "scoped_credentials", credentials)
163
+ self._client.credentials_path = self.credentials_path
164
+ self._client.credentials_info = self.credentials_info
165
+ self._client.credentials_base64 = self.credentials_base64
166
+ self._client.scoped_credentials = credentials
169
167
  return ([], {"client": client})
170
168
 
171
169
  def get_schema_names(self, connection: Connection, **kw) -> List[str]:
@@ -182,18 +180,11 @@ class CloudDatastoreDialect(default.DefaultDialect):
182
180
 
183
181
  try:
184
182
  list_database_resp = client.list_databases(parent=parent)
185
-
186
- def get_database_short_name(database: Database) -> Optional[List[str]]:
187
- full_name = database.name
188
- if full_name:
189
- return full_name.split("/")[-1]
190
- return None
191
-
192
- with futures.ThreadPoolExecutor() as executor:
193
- schemas = list(
194
- executor.map(get_database_short_name, list_database_resp.databases)
195
- )
196
-
183
+ schemas = [
184
+ full_name.split("/")[-1]
185
+ for db in list_database_resp.databases
186
+ if (full_name := db.name)
187
+ ]
197
188
  return schemas
198
189
  except Exception as e:
199
190
  logging.error(e)
@@ -206,57 +197,34 @@ class CloudDatastoreDialect(default.DefaultDialect):
206
197
  query = client.query(kind="__kind__")
207
198
  kinds = list(query.fetch())
208
199
 
209
- def get_kind_name(kind):
210
- return (
211
- name
212
- if (name := getattr(getattr(kind, "key", None), "name", None))
213
- is not None
214
- and isinstance(name, str)
215
- and not name.startswith("__")
216
- else None
217
- )
218
-
219
- with futures.ThreadPoolExecutor() as executor:
220
- result = list(executor.map(get_kind_name, kinds))
221
-
222
- return [t for t in result if t is not None]
200
+ return [
201
+ name
202
+ for kind in kinds
203
+ if (name := getattr(getattr(kind, "key", None), "name", None))
204
+ is not None
205
+ and isinstance(name, str)
206
+ and not name.startswith("__")
207
+ ]
223
208
 
224
209
  def get_columns(
225
210
  self, connection: Connection, table_name: str, schema: str | None = None, **kw
226
211
  ):
227
- """Retrieve column information from the database with optimized parallel processing."""
212
+ """Retrieve column information from the database."""
228
213
  client = self._client
229
214
  query = client.query(kind="__Stat_PropertyType_PropertyName_Kind__")
230
215
  query.add_filter("kind_name", "=", table_name)
231
216
  properties = list(query.fetch())
232
217
 
233
- def process_property(property):
234
- return {
235
- "name": property["property_name"],
236
- "type": _types._property_type[property["property_type"]],
218
+ return [
219
+ {
220
+ "name": prop["property_name"],
221
+ "type": _types._property_type[prop["property_type"]],
237
222
  "nullable": True,
238
223
  "comment": "",
239
224
  "default": None,
240
225
  }
241
-
242
- with futures.ThreadPoolExecutor() as executor:
243
- columns = list(executor.map(process_property, properties))
244
- return columns
245
-
246
- def _contains_select_subquery(self, node) -> bool:
247
- """
248
- Check the AST node contains select subquery
249
- """
250
- if isinstance(node, Select):
251
- for child in node.get_children():
252
- if isinstance(child, Select) or self._contains_select_subquery(child):
253
- return True
254
-
255
- for child in node.get_children():
256
- if isinstance(child, Select) or self._contains_select_subquery(child):
257
- return True
258
-
259
- return False
226
+ for prop in properties
227
+ ]
260
228
 
261
229
  def do_execute(
262
230
  self,
@@ -228,7 +228,7 @@ class Cursor:
228
228
 
229
229
  except Exception as e:
230
230
  logging.error(f"INSERT failed: {e}")
231
- raise ProgrammingError(f"INSERT failed: {e}")
231
+ raise ProgrammingError(f"INSERT failed: {e}") from e
232
232
 
233
233
  def _execute_update(self, statement: str, parameters=None):
234
234
  """Execute an UPDATE statement using Datastore client."""
@@ -454,6 +454,8 @@ class Cursor:
454
454
  r"\bCOUNT_UP_TO\s*\(",
455
455
  r"\bSUM\s*\(",
456
456
  r"\bAVG\s*\(",
457
+ r"\bMIN\s*\(",
458
+ r"\bMAX\s*\(",
457
459
  ]
458
460
  for pattern in agg_patterns:
459
461
  if re.search(pattern, upper):
@@ -931,9 +933,14 @@ class Cursor:
931
933
  # This is a simplified evaluator for common patterns
932
934
  try:
933
935
  return self._eval_condition(context, where_clause)
934
- except Exception:
935
- # If evaluation fails, include the row (fail open)
936
- return True
936
+ except Exception as e:
937
+ logging.warning(
938
+ "Client-side WHERE evaluation failed for clause '%s': %s. "
939
+ "Row will be excluded (fail closed).",
940
+ where_clause,
941
+ e,
942
+ )
943
+ return False
937
944
 
938
945
  def _eval_condition(self, context: Dict[str, Any], condition: str) -> bool:
939
946
  """Evaluate a single condition or compound condition."""
@@ -1566,10 +1573,12 @@ class Cursor:
1566
1573
  self.description = list(agg_fields.values())
1567
1574
 
1568
1575
  def execute_orm(
1569
- self, statement: str, parameters=None, tokens: List[tokens.Token] = []
1576
+ self, statement: str, parameters=None, tokens: Optional[List[tokens.Token]] = None
1570
1577
  ):
1571
1578
  if parameters is None:
1572
1579
  parameters = {}
1580
+ if tokens is None:
1581
+ tokens = []
1573
1582
 
1574
1583
  logging.debug(
1575
1584
  f"[DataStore DBAPI] Executing ORM query: {statement} with parameters: {parameters}"
@@ -1812,11 +1821,15 @@ class Cursor:
1812
1821
  def fetchall(self):
1813
1822
  if self._closed:
1814
1823
  raise Error("Cursor is closed.")
1824
+ if self._query_rows is None:
1825
+ raise ProgrammingError("No query has been executed.")
1815
1826
  return list(self._query_rows)
1816
1827
 
1817
1828
  def fetchmany(self, size=None):
1818
1829
  if self._closed:
1819
1830
  raise Error("Cursor is closed.")
1831
+ if self._query_rows is None:
1832
+ raise ProgrammingError("No query has been executed.")
1820
1833
  if size is None:
1821
1834
  size = self.arraysize or 1
1822
1835
  results = []
@@ -1830,6 +1843,8 @@ class Cursor:
1830
1843
  def fetchone(self):
1831
1844
  if self._closed:
1832
1845
  raise Error("Cursor is closed.")
1846
+ if self._query_rows is None:
1847
+ raise ProgrammingError("No query has been executed.")
1833
1848
  try:
1834
1849
  return next(self._query_rows)
1835
1850
  except StopIteration:
@@ -72,6 +72,11 @@ def parse_url(url): # noqa: C901
72
72
  credentials_base64 = None
73
73
  list_tables_page_size = None
74
74
  user_supplied_client = False
75
+ database = None
76
+
77
+ # database
78
+ if "database" in query:
79
+ database = query.pop("database")
75
80
 
76
81
  # location
77
82
  if "location" in query:
@@ -122,6 +127,7 @@ def parse_url(url): # noqa: C901
122
127
  QueryJobConfig(),
123
128
  list_tables_page_size,
124
129
  user_supplied_client,
130
+ database,
125
131
  )
126
132
  else:
127
133
  return (
@@ -134,6 +140,7 @@ def parse_url(url): # noqa: C901
134
140
  None,
135
141
  list_tables_page_size,
136
142
  user_supplied_client,
143
+ database,
137
144
  )
138
145
 
139
146
  job_config = QueryJobConfig()
@@ -284,4 +291,5 @@ def parse_url(url): # noqa: C901
284
291
  job_config,
285
292
  list_tables_page_size,
286
293
  user_supplied_client,
294
+ database,
287
295
  )
@@ -1,12 +0,0 @@
1
- python_datastore_sqlalchemy-0.0.1.dist-info/licenses/LICENSE,sha256=vcsHP9P3NtGefy7ojTRCgi3w0QqF_igVlmFP5A47YVA,1093
2
- sqlalchemy_datastore/__init__.py,sha256=IUWv-9bXoRdJckJOLqNpkibKLTPD5GNpeck2HZTE3wU,1240
3
- sqlalchemy_datastore/_helpers.py,sha256=4rtRSCBhc0WMDimIWtD7vhxl4n3k-l8DzGR8Q6DiUaw,4769
4
- sqlalchemy_datastore/_types.py,sha256=zs-Gr1XPP-UnppzsuWHEmD-wshkuosXM7yGeUIt-VVY,5092
5
- sqlalchemy_datastore/base.py,sha256=DvYYV_qrGb86x0SwWMcY5_ZHXLXxzuVZ3JPEukF0tYQ,10610
6
- sqlalchemy_datastore/datastore_dbapi.py,sha256=wyR2wCyCYUoksmivAgB6mDh_XMxAj-KBIiaIJ5XxRVo,93805
7
- sqlalchemy_datastore/parse_url.py,sha256=Q8UO-No5he1_-iozbYoIrlnwm2Aw5ePGEBjP_4U1eyM,9883
8
- python_datastore_sqlalchemy-0.0.1.dist-info/METADATA,sha256=W9Q5j0Z90WhVIZACeaPgWsl0zqOFj_D_LASVI3jW4So,2147
9
- python_datastore_sqlalchemy-0.0.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
10
- python_datastore_sqlalchemy-0.0.1.dist-info/entry_points.txt,sha256=h54O-P7HFeCKpoeQ-XSSyP_GVZ1FRAY3gFCS5lkDBIs,77
11
- python_datastore_sqlalchemy-0.0.1.dist-info/top_level.txt,sha256=SE6uZfiwIQaJ1RDBfMm37dm7ZIZoqDcwQ1fDm32x0Rg,21
12
- python_datastore_sqlalchemy-0.0.1.dist-info/RECORD,,