From 8449c08c81c4a3715de51052da978637b79055dc Mon Sep 17 00:00:00 2001 From: Michael Byrnes Date: Fri, 6 Sep 2024 11:09:08 -0500 Subject: [PATCH] DEV-2542: Use sqlalchemy 1.4 --- .gitlab-ci.yml | 8 ++++++++ .secrets.baseline | 4 ++-- .travis.yml | 2 ++ setup.cfg | 7 +++---- src/psqlgraph/__init__.py | 14 ++++++++++++++ src/psqlgraph/base.py | 38 ++++++++++++++++---------------------- src/psqlgraph/psql.py | 2 +- src/psqlgraph/query.py | 17 ++++++++++++----- src/psqlgraph/session.py | 7 ++++++- tox.ini | 2 +- 10 files changed, 65 insertions(+), 36 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7068e17..de6f329 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,14 @@ include: - templates/artifacts/python-library.yaml tox: + parallel: + matrix: + - BUILD_PY_VERSION: + - python3.8 + - python3.9 + - python3.10 + - python3.11 + - python3.12 services: - name: docker.osdc.io/ncigdc/ci-postgres-13:${BASE_CONTAINER_VERSION} alias: postgres diff --git a/.secrets.baseline b/.secrets.baseline index 954f8ca..de0e13f 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -124,7 +124,7 @@ "filename": ".gitlab-ci.yml", "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", "is_verified": false, - "line_number": 17, + "line_number": 25, "is_secret": false } ], @@ -149,5 +149,5 @@ } ] }, - "generated_at": "2024-07-26T21:01:14Z" + "generated_at": "2024-09-06T16:08:25Z" } diff --git a/.travis.yml b/.travis.yml index d3612cc..df8dbd5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,8 @@ python: - 3.8 - 3.9 - 3.10 + - 3.11 + - 3.12 addons: postgresql: '13' diff --git a/setup.cfg b/setup.cfg index 08375d5..f38897a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,11 +18,11 @@ classifiers = Topic :: Internet Topic :: Software Development :: Libraries :: Python Modules Programming Language :: Python - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Development Status :: 5 - Production/Stable [options] @@ -30,11 +30,10 @@ zip_safe = True packages = find: package_dir = =src -python_requires = >=3.7 +python_requires = >=3.8 include_package_data = True install_requires = - psycopg2 - sqlalchemy>=1.3,<1.4 + sqlalchemy[postgresql]~=1.4 xlocal rstr diff --git a/src/psqlgraph/__init__.py b/src/psqlgraph/__init__.py index 23b35c7..21680fb 100644 --- a/src/psqlgraph/__init__.py +++ b/src/psqlgraph/__init__.py @@ -5,3 +5,17 @@ from psqlgraph.util import pg_property, sanitize, validate from psqlgraph.voided_edge import VoidedEdge from psqlgraph.voided_node import VoidedNode + +__all__ = ( + "Edge", + "Node", + "PolyEdge", + "PolyNode", + "PsqlGraphDriver", + "VoidedEdge", + "VoidedNode", + "create_all", + "pg_property", + "sanitize", + "validate", +) diff --git a/src/psqlgraph/base.py b/src/psqlgraph/base.py index 0fc4628..5fe385c 100644 --- a/src/psqlgraph/base.py +++ b/src/psqlgraph/base.py @@ -1,4 +1,6 @@ -from sqlalchemy import event +import inspect +from typing import ClassVar + from sqlalchemy.dialects import postgresql from sqlalchemy.ext import declarative from sqlalchemy.ext.declarative import declared_attr @@ -18,6 +20,18 @@ class CommonBase: _session_hooks_before_insert = [] _session_hooks_before_update = [] _session_hooks_before_delete = [] + __pg_properties__: ClassVar[dict] + + def __init_subclass__(cls) -> None: + cls.__pg_properties__ = {} + pg_properties = ( + (k, v) for k, v in vars(cls).items() if getattr(v, "__pg_setter__", False) + ) + + for name, property in pg_properties: + h_prop = _create_hybrid_property(name, property) + setattr(cls, name, h_prop) + cls.__pg_properties__[name] = property.__pg_types__ # ======== Columns ======== created = schema.Column( @@ -237,7 +251,7 @@ def get_pg_properties(cls): return cls.__pg_properties__ -def create_hybrid_property(name, fset): +def _create_hybrid_property(name, fset): @hybrid_property def hybrid_prop(instance): # Note: this does not use an 'in' clause or a .get() with a @@ -256,26 +270,6 @@ def hybrid_prop(instance, value): return hybrid_prop -@event.listens_for(CommonBase, "mapper_configured", propagate=True) -def create_hybrid_properties(mapper, cls): - # This dictionary will be a property name to allowed types - # dictionary. It will be populated at mapper configuration using - # all model properties defined with @pg_property - cls.__pg_properties__ = {} - - for pg_attr in dir(cls): - if pg_attr in ["properties", "props", "system_annotations", "sysan"]: - continue - - f = getattr(cls, pg_attr) - if not getattr(f, "__pg_setter__", False): - continue - - h_prop = create_hybrid_property(pg_attr, f) - setattr(cls, pg_attr, h_prop) - cls.__pg_properties__[pg_attr] = f.__pg_types__ - - class VoidedBaseClass: @hybrid_property def props(self): diff --git a/src/psqlgraph/psql.py b/src/psqlgraph/psql.py index 310832a..8106a07 100644 --- a/src/psqlgraph/psql.py +++ b/src/psqlgraph/psql.py @@ -66,7 +66,7 @@ def __init__(self, host, user, password, database, **kwargs): # Construct connection string host = "" if host is None else host - conn_str = "postgresql://{user}:{password}@{host}/{database}".format( + conn_str = "postgresql+psycopg2://{user}:{password}@{host}/{database}".format( user=user, password=password, host=host, database=database ) if kwargs["isolation_level"] not in self.acceptable_isolation_levels: diff --git a/src/psqlgraph/query.py b/src/psqlgraph/query.py index 3e1dce9..bb0f6ee 100644 --- a/src/psqlgraph/query.py +++ b/src/psqlgraph/query.py @@ -31,8 +31,10 @@ def entity(self): entity. """ + if self._last_joined_entity: + return self._last_joined_entity.entity - return self._joinpoint_zero().entity + return self.column_descriptions[0]["entity"] # ======== Edges ======== def with_edge_to_node(self, edge_type, target_node): @@ -208,13 +210,18 @@ def path(self, *paths): query.path().reset_joinpoint().filter() """ - entities = [p.strip() for path in paths for p in path.split(".")] + entities = (p.strip() for path in paths for p in path.split(".")) + query = self + assert ( not self.entity().is_abstract_base() ), "Please narrow your search by specifying a node subclass" - for e in entities: - self = self.join(*getattr(self.entity(), e).attr) - return self + + for entity in entities: + proxy = getattr(query.entity(), entity) + query = query.join(proxy.local_attr).join(proxy.remote_attr) + + return query def _get_link_details(self, entity, link_name): """ "Lookup the (edge_class, left_edge_id, right_edge_id, node_class) diff --git a/src/psqlgraph/session.py b/src/psqlgraph/session.py index a36566f..718aa78 100644 --- a/src/psqlgraph/session.py +++ b/src/psqlgraph/session.py @@ -22,9 +22,14 @@ def __init__(self, *args, **kwargs): self.package_namespace = kwargs.pop("package_namespace", None) super().__init__(*args, **kwargs) + def _autobegin(self): + if self._psqlgraph_closed: + raise exc.SessionClosedError("session closed") + + return super()._autobegin() + @inherit_docstring_from(Session) def connection(self, *args, **kwargs): - if self._psqlgraph_closed: raise exc.SessionClosedError("session closed") diff --git a/tox.ini b/tox.ini index 52acdc9..a3be5e4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,py310,py311 +envlist = py3{8,9,10,11,12} skip_missing_interpreters = True isolated_build = True