diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7068e17..4824b68 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,16 +1,23 @@ --- include: - project: nci-gdc/gitlab-templates - ref: master + ref: 0.7.11 file: - templates/artifacts/python-library.yaml tox: + parallel: + matrix: + - LANGUAGE_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 variables: - BASE_CONTAINER_VERSION: 2.3.1 # these are for postgres docker POSTGRES_DB: automated_test POSTGRES_USER: test diff --git a/.secrets.baseline b/.secrets.baseline index 954f8ca..00ba243 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": 24, "is_secret": false } ], @@ -149,5 +149,5 @@ } ] }, - "generated_at": "2024-07-26T21:01:14Z" + "generated_at": "2024-09-06T17:02:29Z" } diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d3612cc..0000000 --- a/.travis.yml +++ /dev/null @@ -1,36 +0,0 @@ ---- -dist: focal -language: python - -python: - - 3.7 - - 3.8 - - 3.9 - - 3.10 - -addons: - postgresql: '13' - apt: - sources: - - sourceline: deb http://apt.postgresql.org/pub/repos/apt/ focal-pgdg main 13 - key_url: https://www.postgresql.org/media/keys/ACCC4CF8.asc - packages: - - postgresql-13 - - postgresql-client-13 - -before_install: - # Copy custom configs from the repo because PG-13 isn't set up to run like - # it normally does on Travis out of the box. - - sudo cp travis/postgresql.conf /etc/postgresql/13/main/postgresql.conf - - sudo cp travis/pg_hba.conf /etc/postgresql/13/main/pg_hba.conf - - sudo pg_ctlcluster 13 main restart - -install: - - pip install tox - -before_script: - - psql -U postgres -c "create user test with superuser password 'test';" - - psql -U postgres -c "create database automated_test with owner test;" - -script: - - tox -r -e py 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..6b0f54b 100644 --- a/src/psqlgraph/psql.py +++ b/src/psqlgraph/psql.py @@ -66,9 +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( - user=user, password=password, host=host, database=database - ) + conn_str = f"postgresql+psycopg2://{user}:{password}@{host}/{database}" if kwargs["isolation_level"] not in self.acceptable_isolation_levels: logger.warning( ( 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