risclog.sqlalchemy 7.2__tar.gz → 9.0__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.
Files changed (47) hide show
  1. risclog_sqlalchemy-9.0/.github/workflows/tests.yml +41 -0
  2. risclog_sqlalchemy-9.0/.gitignore +24 -0
  3. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/CHANGES.rst +12 -0
  4. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/HACKING.rst +1 -1
  5. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/PKG-INFO +26 -14
  6. {risclog.sqlalchemy-7.2/doc → risclog_sqlalchemy-9.0/docs}/conf.py +8 -8
  7. risclog_sqlalchemy-9.0/pyproject.toml +105 -0
  8. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/cache.py +15 -15
  9. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/db.py +52 -49
  10. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/fixtures.py +1 -1
  11. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/interfaces.py +2 -2
  12. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/model.py +2 -3
  13. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/serializer.py +4 -4
  14. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/testing.py +7 -6
  15. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/testlayer.py +1 -2
  16. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/tests/conftest.py +11 -11
  17. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/tests/fixtures/alembic/env.py +2 -2
  18. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/tests/fixtures/alembic/versions/3f735078d5a8_foo.py +4 -3
  19. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/tests/test_cache.py +58 -56
  20. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/tests/test_db.py +51 -49
  21. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/tests/test_fixtures.py +6 -6
  22. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/tests/test_model.py +1 -1
  23. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/tests/test_serializer.py +11 -11
  24. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog.sqlalchemy.egg-info/PKG-INFO +26 -14
  25. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog.sqlalchemy.egg-info/SOURCES.txt +8 -10
  26. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog.sqlalchemy.egg-info/requires.txt +5 -1
  27. risclog_sqlalchemy-9.0/uv.lock +2671 -0
  28. risclog.sqlalchemy-7.2/pyproject.toml +0 -60
  29. risclog.sqlalchemy-7.2/requirements.txt +0 -10
  30. risclog.sqlalchemy-7.2/setup.py +0 -76
  31. risclog.sqlalchemy-7.2/src/risclog/__init__.py +0 -1
  32. risclog.sqlalchemy-7.2/src/risclog.sqlalchemy.egg-info/namespace_packages.txt +0 -1
  33. risclog.sqlalchemy-7.2/src/risclog.sqlalchemy.egg-info/not-zip-safe +0 -1
  34. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/COPYRIGHT.rst +0 -0
  35. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/LICENSE.txt +0 -0
  36. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/README.rst +0 -0
  37. {risclog.sqlalchemy-7.2/doc → risclog_sqlalchemy-9.0/docs}/about.txt +0 -0
  38. {risclog.sqlalchemy-7.2/doc → risclog_sqlalchemy-9.0/docs}/api.txt +0 -0
  39. {risclog.sqlalchemy-7.2/doc → risclog_sqlalchemy-9.0/docs}/changes.txt +0 -0
  40. {risclog.sqlalchemy-7.2/doc → risclog_sqlalchemy-9.0/docs}/index.txt +0 -0
  41. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/setup.cfg +0 -0
  42. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/__init__.py +0 -0
  43. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/functions.py +0 -0
  44. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/tests/__init__.py +0 -0
  45. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog/sqlalchemy/tests/fixtures/alembic/script.py.mako +0 -0
  46. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog.sqlalchemy.egg-info/dependency_links.txt +0 -0
  47. {risclog.sqlalchemy-7.2 → risclog_sqlalchemy-9.0}/src/risclog.sqlalchemy.egg-info/top_level.txt +0 -0
@@ -0,0 +1,41 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ - testing
8
+ pull_request:
9
+ schedule:
10
+ # * is a special character in YAML so you have to quote this string
11
+ - cron: '0 1 * * *'
12
+
13
+ jobs:
14
+ test:
15
+ uses: risclog-solution/gha_workflow_templates/.github/workflows/pytest_uv.yml@master
16
+ with:
17
+ timeout_minutes: 30
18
+ secrets:
19
+ UVCONF: ${{ secrets.UVCONF }}
20
+
21
+ coverage:
22
+ needs: test
23
+ uses: risclog-solution/gha_workflow_templates/.github/workflows/coverage.yml@master
24
+ with:
25
+ min_coverage: 80
26
+
27
+ pre-commit:
28
+ uses: risclog-solution/gha_workflow_templates/.github/workflows/pre_commit.yml@master
29
+
30
+ sonarqube:
31
+ needs: coverage
32
+ uses: risclog-solution/gha_workflow_templates/.github/workflows/sonarqube.yml@master
33
+ secrets:
34
+ SONARQUBETOKEN: ${{ secrets.SONARQUBETOKEN }}
35
+
36
+ publish-test-results:
37
+ needs: test
38
+ uses: risclog-solution/gha_workflow_templates/.github/workflows/publish_test_results.yml@master
39
+
40
+ cleanup:
41
+ uses: risclog-solution/gha_workflow_templates/.github/workflows/cleanup.yml@master
@@ -0,0 +1,24 @@
1
+ *.egg-info
2
+ *.pyc
3
+ .Python
4
+ .cache/v/cache/lastfailed
5
+ .coverage
6
+ .installed.cfg
7
+ .tags
8
+ .tox
9
+ __pycache__
10
+ bin
11
+ build
12
+ coverage-report
13
+ coverage.xml
14
+ develop-eggs
15
+ dist
16
+ docs
17
+ docs/_api
18
+ eggs
19
+ include
20
+ lib
21
+ local.cfg
22
+ parts
23
+ testreport.xml
24
+ .venv
@@ -2,6 +2,18 @@
2
2
  Change log for risclog.sqlalchemy
3
3
  =================================
4
4
 
5
+ 9.0 (2026-08-24)
6
+ ================
7
+
8
+ - deps: Switch to uv, pyproject.toml and PEP-420 namespace package.
9
+
10
+
11
+ 8.0 (2025-12-02)
12
+ ================
13
+
14
+ - Add compatability to sqlalchemy 2. Break with compatability to sqlalchemy < 1.4.
15
+
16
+
5
17
  7.2 (2024-04-19)
6
18
  ================
7
19
 
@@ -24,7 +24,7 @@ Developing risclog.sqlalchemy
24
24
  Running tests
25
25
  =============
26
26
 
27
- Simply run `./pytest` in the root of the repository.
27
+ Simply run `uv run pytest` in the root of the repository.
28
28
 
29
29
 
30
30
  Documentation
@@ -1,12 +1,11 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: risclog.sqlalchemy
3
- Version: 7.2
3
+ Version: 9.0
4
4
  Summary: Encapsulate sqlalchemy modelling infrastructure.
5
- Home-page: https://github.com/gocept/risclog.sqlalchemy
6
- Author: gocept <mail@gocept.com>
7
- Author-email: mail@gocept.com
5
+ Author-email: gocept <mail@gocept.com>
8
6
  License: ZPL 2.1
9
- Keywords: sqlalchemy alembic model infrastructure utility
7
+ Project-URL: Homepage, https://github.com/gocept/risclog.sqlalchemy
8
+ Keywords: sqlalchemy,alembic,model,infrastructure,utility
10
9
  Classifier: Development Status :: 5 - Production/Stable
11
10
  Classifier: Framework :: Pyramid
12
11
  Classifier: Intended Audience :: Developers
@@ -16,20 +15,19 @@ Classifier: Natural Language :: English
16
15
  Classifier: Operating System :: OS Independent
17
16
  Classifier: Programming Language :: Python
18
17
  Classifier: Programming Language :: Python :: 3
19
- Classifier: Programming Language :: Python :: 3.7
20
- Classifier: Programming Language :: Python :: 3.8
21
- Classifier: Programming Language :: Python :: 3.9
22
18
  Classifier: Programming Language :: Python :: 3.10
23
- Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
24
20
  Classifier: Programming Language :: Python :: Implementation
25
21
  Classifier: Programming Language :: Python :: Implementation :: CPython
26
22
  Classifier: Topic :: Database
27
23
  Classifier: Topic :: Software Development :: Libraries
28
24
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
29
25
  Requires-Python: >=3.7
26
+ Description-Content-Type: text/x-rst
30
27
  License-File: LICENSE.txt
31
- Requires-Dist: SQLAlchemy<2,>=1.0
28
+ Requires-Dist: SQLAlchemy>=1.4
32
29
  Requires-Dist: alembic>=0.7
30
+ Requires-Dist: importlib_resources
33
31
  Requires-Dist: pytz
34
32
  Requires-Dist: setuptools
35
33
  Requires-Dist: transaction
@@ -42,8 +40,12 @@ Provides-Extra: self-test
42
40
  Requires-Dist: mock; extra == "self-test"
43
41
  Requires-Dist: pyramid; extra == "self-test"
44
42
  Requires-Dist: pytest; extra == "self-test"
43
+ Requires-Dist: psycopg2-binary; extra == "self-test"
44
+ Requires-Dist: greenlet; extra == "self-test"
45
+ Requires-Dist: pytest-cov; extra == "self-test"
45
46
  Provides-Extra: pyramid
46
47
  Requires-Dist: pyramid; extra == "pyramid"
48
+ Dynamic: license-file
47
49
 
48
50
  ==================
49
51
  risclog.sqlalchemy
@@ -57,7 +59,6 @@ This package is compatible with Python version 3.7 up to 3.11.
57
59
 
58
60
  .. _`sqlalchemy`: http://www.sqlalchemy.org
59
61
 
60
-
61
62
  =============================
62
63
  Developing risclog.sqlalchemy
63
64
  =============================
@@ -84,7 +85,7 @@ Developing risclog.sqlalchemy
84
85
  Running tests
85
86
  =============
86
87
 
87
- Simply run `./pytest` in the root of the repository.
88
+ Simply run `uv run pytest` in the root of the repository.
88
89
 
89
90
 
90
91
  Documentation
@@ -101,11 +102,22 @@ python, where ``sphinx-pypi-upload`` is installed::
101
102
 
102
103
  $ python setup.py upload_sphinx
103
104
 
104
-
105
105
  =================================
106
106
  Change log for risclog.sqlalchemy
107
107
  =================================
108
108
 
109
+ 9.0 (2026-08-24)
110
+ ================
111
+
112
+ - deps: Switch to uv, pyproject.toml and PEP-420 namespace package.
113
+
114
+
115
+ 8.0 (2025-12-02)
116
+ ================
117
+
118
+ - Add compatability to sqlalchemy 2. Break with compatability to sqlalchemy < 1.4.
119
+
120
+
109
121
  7.2 (2024-04-19)
110
122
  ================
111
123
 
@@ -4,20 +4,20 @@ import sys
4
4
  # If extensions (or modules to document with autodoc) are in another directory,
5
5
  # add these directories to sys.path here. If the directory is relative to the
6
6
  # documentation root, use os.path.abspath to make it absolute, like shown here.
7
- sys.path.insert(0, os.path.abspath('_themes'))
7
+ sys.path.insert(0, os.path.abspath("_themes"))
8
8
  extensions = []
9
9
 
10
- source_suffix = '.txt'
11
- master_doc = 'index'
12
- exclude_patterns = ['_build']
10
+ source_suffix = ".txt"
11
+ master_doc = "index"
12
+ exclude_patterns = ["_build"]
13
13
 
14
- project = u'risclog.sqlalchemy'
15
- copyright = u"""\
14
+ project = "risclog.sqlalchemy"
15
+ copyright = """\
16
16
  2011 - 2017 Zope Foundation and Contributors.
17
17
  <a href="http://sphinx.pocoo.org/">Sphinx</a>-Theme adapted from
18
18
  <a href="http://jinja.pocoo.org/docs/">Jinja</a>
19
19
  """
20
- version = '2.0' # XXX determine automatically
20
+ version = "2.0" # XXX determine automatically
21
21
  release = version
22
22
 
23
- pygments_style = 'sphinx'
23
+ pygments_style = "sphinx"
@@ -0,0 +1,105 @@
1
+ [build-system]
2
+ requires = ["setuptools>=40.6.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "risclog.sqlalchemy"
7
+ version = "9.0"
8
+ description = "Encapsulate sqlalchemy modelling infrastructure."
9
+ keywords = ["sqlalchemy", "alembic", "model", "infrastructure", "utility"]
10
+ requires-python = ">=3.7"
11
+ authors = [{ name = "gocept", email = "mail@gocept.com" }]
12
+ license = { text = "ZPL 2.1" }
13
+ urls = { Homepage = "https://github.com/gocept/risclog.sqlalchemy" }
14
+ dynamic = ["readme"]
15
+ classifiers = [
16
+ "Development Status :: 5 - Production/Stable",
17
+ "Framework :: Pyramid",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved",
20
+ "License :: OSI Approved :: Zope Public License",
21
+ "Natural Language :: English",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: Implementation",
28
+ "Programming Language :: Python :: Implementation :: CPython",
29
+ "Topic :: Database",
30
+ "Topic :: Software Development :: Libraries",
31
+ "Topic :: Software Development :: Libraries :: Python Modules",
32
+ ]
33
+ dependencies = [
34
+ "SQLAlchemy >= 1.4",
35
+ "alembic >= 0.7",
36
+ "importlib_resources",
37
+ "pytz",
38
+ "setuptools",
39
+ "transaction",
40
+ "zope.component >= 4.0.1",
41
+ "zope.interface",
42
+ "zope.sqlalchemy >= 1.3",
43
+ ]
44
+
45
+ [project.optional-dependencies]
46
+ test = ["gocept.testdb"]
47
+ self-test = [
48
+ "mock",
49
+ "pyramid",
50
+ "pytest",
51
+ "psycopg2-binary",
52
+ "greenlet",
53
+ "pytest-cov",
54
+ ]
55
+ pyramid = ["pyramid"]
56
+
57
+ [tool.setuptools]
58
+ include-package-data = true
59
+
60
+ [tool.setuptools.packages.find]
61
+ where = ["src"]
62
+ include = ["risclog.*"]
63
+
64
+ [tool.setuptools.dynamic]
65
+ readme = { file = ["README.rst", "HACKING.rst", "CHANGES.rst"], content-type = "text/x-rst" }
66
+
67
+ [tool.coverage.run]
68
+ relative_files = true
69
+ parallel = true
70
+ branch = true
71
+ source = ["risclog.sqlalchemy"]
72
+
73
+ [tool.coverage.paths]
74
+ source = ["src", ".pytest/*/site-packages"]
75
+
76
+ [tool.coverage.html]
77
+ directory = "coverage-report"
78
+
79
+ [tool.coverage.report]
80
+ precision = 2
81
+ show_missing = true
82
+ skip_covered = true
83
+ exclude_lines = [
84
+ # a more strict default pragma
85
+ "\\# pragma: no cover\\b",
86
+
87
+ # allow defensive code
88
+ "^\\s*raise AssertionError\\b",
89
+ "^\\s*raise NotImplementedError\\b",
90
+ "^\\s*return NotImplemented\\b",
91
+ "^\\s*raise$",
92
+
93
+ # typing-related code
94
+ "^if (False|TYPE_CHECKING):",
95
+ ": \\.\\.\\.(\\s*#.*)?$",
96
+ "^ +\\.\\.\\.$",
97
+ "-> ['\"]?NoReturn['\"]?:",
98
+ ]
99
+
100
+ [tool.ruff]
101
+ target-version = "py312"
102
+ line-length = 79
103
+
104
+ [tool.check-manifest]
105
+ ignore-bad-ideas = ["*.mo"]
@@ -100,26 +100,26 @@ class ModelCache:
100
100
  self.hp.setrelheap()
101
101
 
102
102
  def __sizeof__(self):
103
- if not hasattr(self, 'hp'):
103
+ if not hasattr(self, "hp"):
104
104
  return 0
105
105
 
106
106
  h = self.hp.heap()
107
107
  return h.size
108
108
 
109
- def _sizeof_fmt(self, num, suffix='B'):
110
- for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
109
+ def _sizeof_fmt(self, num, suffix="B"):
110
+ for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
111
111
  if abs(num) < 1024.0:
112
- return f'{num:3.1f} {unit}{suffix}'
112
+ return f"{num:3.1f} {unit}{suffix}"
113
113
  num /= 1024.0
114
- return f'{num:.1f}Yi{suffix}'
114
+ return f"{num:.1f}Yi{suffix}"
115
115
 
116
116
  def log_memory_usage(self):
117
- if not hasattr(self, 'hp'):
117
+ if not hasattr(self, "hp"):
118
118
  return
119
119
 
120
120
  bsize = sys.getsizeof(self)
121
121
  size = self._sizeof_fmt(bsize)
122
- self._log('info', f'Memory usage: {size} ({bsize})')
122
+ self._log("info", f"Memory usage: {size} ({bsize})")
123
123
 
124
124
  def find(self, model, **kwargs):
125
125
  """
@@ -205,7 +205,7 @@ class ModelCache:
205
205
  .connection.cursor()
206
206
  )
207
207
 
208
- self._log('debug', 'Flushing model cache.')
208
+ self._log("debug", "Flushing model cache.")
209
209
  self._assign_sequences()
210
210
  self._sync_relationship_attrs()
211
211
 
@@ -239,7 +239,7 @@ class ModelCache:
239
239
  cursor.connection.commit()
240
240
 
241
241
  self.clear(session)
242
- self._log('info', 'Flushed model cache.')
242
+ self._log("info", "Flushed model cache.")
243
243
 
244
244
  def _save_by_copy(self, cursor, objects):
245
245
  """
@@ -273,7 +273,7 @@ class ModelCache:
273
273
  if column.default is not None:
274
274
  value = column.default.arg
275
275
  else:
276
- value = r'\N'
276
+ value = r"\N"
277
277
  elif type(value) != column.type.python_type:
278
278
  value = column.type.python_type(value)
279
279
  row[column.key] = value
@@ -281,11 +281,11 @@ class ModelCache:
281
281
 
282
282
  file.seek(0)
283
283
 
284
- columns_string = ','.join(
284
+ columns_string = ",".join(
285
285
  [f'"{column}"' for column in table.columns.keys()]
286
286
  )
287
287
  cursor.copy_expert(
288
- f'COPY {table} ({columns_string}) '
288
+ f"COPY {table} ({columns_string}) "
289
289
  "FROM STDIN WITH CSV DELIMITER ',' NULL '\\N'",
290
290
  file,
291
291
  )
@@ -451,12 +451,12 @@ class ModelCache:
451
451
  def _register_change_handler(self, model, event_handler):
452
452
  """Register an event handler for every column of a model."""
453
453
  for attr in inspect(model).column_attrs:
454
- sqlalchemy.event.listen(attr, 'set', event_handler)
454
+ sqlalchemy.event.listen(attr, "set", event_handler)
455
455
 
456
456
  def _deregister_change_handler(self, model, event_handler):
457
457
  """Removes an event handler for every column of a model."""
458
458
  for attr in inspect(model).column_attrs:
459
- sqlalchemy.event.listen(attr, 'set', event_handler)
459
+ sqlalchemy.event.listen(attr, "set", event_handler)
460
460
 
461
461
  def _instance_change_handler(self, instance, value, oldvalue, initiator):
462
462
  """
@@ -466,7 +466,7 @@ class ModelCache:
466
466
  attribute was changed. For more information,
467
467
  see: https://docs.sqlalchemy.org/en/13/orm/events.html
468
468
  """ # noqa: E501
469
- if oldvalue is sqlalchemy.util.symbol('NEVER_SET'):
469
+ if oldvalue is sqlalchemy.util.symbol("NEVER_SET"):
470
470
  return
471
471
 
472
472
  model = type(instance)
@@ -14,18 +14,19 @@ import zope.interface
14
14
  import zope.sqlalchemy
15
15
  from packaging.version import Version as parse_version
16
16
  from sqlalchemy import __version__ as sqlalchemy_version
17
+ from sqlalchemy import text
17
18
 
18
19
  # Mapping engine name registered using Database.register_engine --> base class
19
20
  _ENGINE_CLASS_MAPPING = {}
20
21
 
21
- SA_GE_14 = parse_version(sqlalchemy_version) >= parse_version('1.4.0')
22
+ SA_GE_14 = parse_version(sqlalchemy_version) >= parse_version("1.4.0")
22
23
 
23
24
 
24
25
  def assert_engine_not_registered(name, mapping):
25
26
  """Ensure a consistent error message."""
26
27
  assert (
27
28
  name not in mapping
28
- ), f'An engine for name `{name}` is already registered.'
29
+ ), f"An engine for name `{name}` is already registered."
29
30
 
30
31
 
31
32
  def register_class(class_):
@@ -74,13 +75,10 @@ class RoutingSession(sqlalchemy.orm.Session):
74
75
  if issubclass(mapper.class_, class_):
75
76
  return db_util.get_engine(engine_name)
76
77
 
77
- raise RuntimeError(f'Did not find an engine for {mapper.class_}')
78
+ raise RuntimeError(f"Did not find an engine for {mapper.class_}")
78
79
 
79
80
  def _bound_execute(self, bind, *args, **kwargs):
80
- if SA_GE_14:
81
- return self.execute(*args, bind_arguments={'bind': bind}, **kwargs)
82
- else:
83
- return self.execute(*args, bind=bind, **kwargs)
81
+ return self.execute(*args, bind_arguments={"bind": bind}, **kwargs)
84
82
 
85
83
  def execute_with_bind(self, engine_name, *args, **kwargs):
86
84
  """
@@ -141,10 +139,9 @@ def get_database(testing=False, keep_session=False, expire_on_commit=False):
141
139
  db = zope.component.queryUtility(risclog.sqlalchemy.interfaces.IDatabase)
142
140
  if db is None:
143
141
  db = Database(testing, keep_session, expire_on_commit)
144
- assert (
145
- db.testing == testing
146
- ), 'Requested testing status `%s` does not match Database.testing.' % (
147
- testing
142
+ assert db.testing == testing, (
143
+ "Requested testing status `%s` does not match Database.testing."
144
+ % (testing)
148
145
  )
149
146
  return db
150
147
 
@@ -160,8 +157,8 @@ class Database:
160
157
  )
161
158
  is None
162
159
  ), (
163
- 'Cannot create Database twice, use `.get_database()` to get '
164
- 'the instance.'
160
+ "Cannot create Database twice, use `.get_database()` to get "
161
+ "the instance."
165
162
  )
166
163
  self._engines = {}
167
164
  self.testing = testing
@@ -176,11 +173,11 @@ class Database:
176
173
  self._setup_utility()
177
174
 
178
175
  def register_engine(
179
- self, dsn, engine_args={}, name='', alembic_location=None
176
+ self, dsn, engine_args={}, name="", alembic_location=None
180
177
  ):
181
178
  assert_engine_not_registered(name, self._engines)
182
- engine_args['echo'] = bool(
183
- int(os.environ.get('ECHO_SQLALCHEMY_QUERIES', '0'))
179
+ engine_args["echo"] = bool(
180
+ int(os.environ.get("ECHO_SQLALCHEMY_QUERIES", "0"))
184
181
  )
185
182
  engine = sqlalchemy.create_engine(dsn, **engine_args)
186
183
  self._verify_engine(engine)
@@ -192,13 +189,13 @@ class Database:
192
189
  # reflection now.
193
190
  self.prepare_deferred(_ENGINE_CLASS_MAPPING.get(name))
194
191
 
195
- def get_engine(self, name=''):
196
- return self._engines[name]['engine']
192
+ def get_engine(self, name=""):
193
+ return self._engines[name]["engine"]
197
194
 
198
195
  def get_all_engines(self):
199
- return [x['engine'] for x in self._engines.values()]
196
+ return [x["engine"] for x in self._engines.values()]
200
197
 
201
- def drop_engine(self, name=''):
198
+ def drop_engine(self, name=""):
202
199
  engine = self.get_engine(name)
203
200
  engine.dispose()
204
201
  del self._engines[name]
@@ -209,30 +206,32 @@ class Database:
209
206
  if issubclass(class_, sqlalchemy.ext.declarative.DeferredReflection):
210
207
  class_.prepare(self.get_engine(class_._engine_name))
211
208
 
212
- def create_all(self, engine_name='', create_defaults=True):
209
+ def create_all(self, engine_name="", create_defaults=True):
213
210
  """Create all tables etc. for an engine."""
214
211
  engine = self._engines[engine_name]
215
212
  _ENGINE_CLASS_MAPPING[engine_name].metadata.create_all(
216
- engine['engine']
213
+ engine["engine"]
217
214
  )
218
215
 
219
216
  # mark the database to be in the latest revision
220
- location = engine['alembic_location']
217
+ location = engine["alembic_location"]
221
218
  if location:
222
- with alembic_context(engine['engine'], location) as ac:
223
- ac.migration_context.stamp(
224
- ac.script, ac.script.get_current_head()
225
- )
219
+ with alembic_context(engine["engine"], location) as ac:
220
+ with ac.migration_context.begin_transaction():
221
+ ac.migration_context.stamp(
222
+ ac.script, ac.script.get_current_head()
223
+ )
224
+
226
225
  if create_defaults:
227
226
  self.create_defaults(engine_name)
228
227
 
229
- def create_defaults(self, engine_name=''):
228
+ def create_defaults(self, engine_name=""):
230
229
  for name, class_ in risclog.sqlalchemy.model.class_registry.items():
231
230
  # do not call create_defaults for foreign engines,
232
231
  # since they may not be set up yet
233
- if engine_name != getattr(class_, '_engine_name', NotImplemented):
232
+ if engine_name != getattr(class_, "_engine_name", NotImplemented):
234
233
  continue
235
- if hasattr(class_, 'create_defaults'):
234
+ if hasattr(class_, "create_defaults"):
236
235
  class_.create_defaults()
237
236
 
238
237
  def _verify_engine(self, engine):
@@ -241,7 +240,7 @@ class Database:
241
240
  from sqlalchemy import text
242
241
 
243
242
  try:
244
- conn.execute(text('SELECT * FROM tmp_functest'))
243
+ conn.execute(text("SELECT * FROM tmp_functest"))
245
244
  except sqlalchemy.exc.DatabaseError:
246
245
  db_is_testing = False
247
246
  else:
@@ -258,25 +257,25 @@ class Database:
258
257
 
259
258
  # We're not in a valid state. Bail out.
260
259
  raise SystemExit(
261
- 'Not working against correct database (live vs '
262
- 'testing). Refusing to set up database connection '
263
- 'to {}.'.format(engine.url)
260
+ "Not working against correct database (live vs "
261
+ "testing). Refusing to set up database connection "
262
+ "to {}.".format(engine.url)
264
263
  )
265
264
 
266
- def assert_database_revision_is_current(self, engine_name=''):
265
+ def assert_database_revision_is_current(self, engine_name=""):
267
266
  def assert_revision(ac, head_rev, db_rev):
268
267
  if head_rev != db_rev:
269
268
  raise ValueError(
270
269
  'Database revision {} of engine "{}" does not match '
271
- 'current revision {}.\nMaybe you want to call '
272
- '`bin/alembic upgrade head`.'.format(
270
+ "current revision {}.\nMaybe you want to call "
271
+ "`bin/alembic upgrade head`.".format(
273
272
  db_rev, engine_name, head_rev
274
273
  )
275
274
  )
276
275
 
277
276
  self._run_in_alembic_context(assert_revision, engine_name)
278
277
 
279
- def update_database_revision_to_current(self, engine_name=''):
278
+ def update_database_revision_to_current(self, engine_name=""):
280
279
  def upgrade_revision(ac, head_rev, db_rev):
281
280
  if head_rev != db_rev:
282
281
  ac.upgrade(head_rev)
@@ -318,7 +317,7 @@ class Database:
318
317
 
319
318
  try:
320
319
  # never truncate the PostGIS table
321
- table_names.remove('spatial_ref_sys')
320
+ table_names.remove("spatial_ref_sys")
322
321
  except ValueError:
323
322
  pass
324
323
 
@@ -328,25 +327,27 @@ class Database:
328
327
  # does not change the schema. Clearing the alembic_version will
329
328
  # cause "confusion and delay".
330
329
  try:
331
- table_names.remove('alembic_version')
330
+ table_names.remove("alembic_version")
332
331
  except ValueError:
333
332
  pass
334
333
  if not table_names:
335
334
  return
336
- tables = ', '.join('"%s"' % x for x in table_names)
335
+ tables = ", ".join('"%s"' % x for x in table_names)
337
336
  self.session._bound_execute(
338
337
  engine,
339
- 'TRUNCATE {} {} {}'.format(
340
- tables,
341
- 'RESTART IDENTITY' if restart_sequences else '',
342
- 'CASCADE' if cascade else '',
338
+ text(
339
+ "TRUNCATE {} {} {}".format(
340
+ tables,
341
+ "RESTART IDENTITY" if restart_sequences else "",
342
+ "CASCADE" if cascade else "",
343
+ )
343
344
  ),
344
345
  )
345
346
  zope.sqlalchemy.mark_changed(self.session)
346
347
  if commit:
347
348
  transaction.commit()
348
349
 
349
- def _run_in_alembic_context(self, func, engine_name=''):
350
+ def _run_in_alembic_context(self, func, engine_name=""):
350
351
  """Run a function in `alembic_context`.
351
352
 
352
353
  The function must take three parameters:
@@ -356,10 +357,10 @@ class Database:
356
357
 
357
358
  """
358
359
  engine = self._engines[engine_name]
359
- location = engine['alembic_location']
360
+ location = engine["alembic_location"]
360
361
  if not location:
361
362
  return
362
- with alembic_context(engine['engine'], location) as ac:
363
+ with alembic_context(engine["engine"], location) as ac:
363
364
  head_rev = ac.script.get_current_head()
364
365
  db_rev = ac.migration_context.get_current_revision()
365
366
  return func(ac, head_rev, db_rev)
@@ -385,7 +386,7 @@ class AlembicContext:
385
386
  def __init__(self, conn, script_location):
386
387
  self.conn = conn
387
388
  self.config = alembic.config.Config()
388
- self.config.set_main_option('script_location', script_location)
389
+ self.config.set_main_option("script_location", script_location)
389
390
  self.migration_context = alembic.migration.MigrationContext.configure(
390
391
  conn
391
392
  )
@@ -402,3 +403,5 @@ class AlembicContext:
402
403
  ) as ec:
403
404
  ec.configure(self.conn)
404
405
  ec.run_migrations()
406
+ if hasattr(self.conn, "commit"):
407
+ self.conn.commit()