gn2pg_client 1.9.0.dev0__tar.gz → 1.10.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 (44) hide show
  1. gn2pg_client-1.10.0/PKG-INFO +107 -0
  2. gn2pg_client-1.10.0/README.md +58 -0
  3. gn2pg_client-1.10.0/gn2pg/alembic/env.py +72 -0
  4. gn2pg_client-1.10.0/gn2pg/alembic/script.py.mako +25 -0
  5. gn2pg_client-1.10.0/gn2pg/alembic/versions/20260818_01_initial_schema.py +134 -0
  6. gn2pg_client-1.10.0/gn2pg/api.py +330 -0
  7. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/app/admin_views.py +0 -1
  8. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/app/app.py +2 -2
  9. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/app/config.py +4 -2
  10. gn2pg_client-1.10.0/gn2pg/app/models.py +33 -0
  11. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/check_conf.py +102 -26
  12. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/data/gn2pgconfig.toml +8 -3
  13. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/data/to_gnsynthese.sql +284 -112
  14. gn2pg_client-1.10.0/gn2pg/database/__init__.py +5 -0
  15. gn2pg_client-1.10.0/gn2pg/database/migrations.py +208 -0
  16. gn2pg_client-1.10.0/gn2pg/database/tables.py +102 -0
  17. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/download.py +217 -105
  18. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/helpers.py +46 -30
  19. gn2pg_client-1.9.0.dev0/gn2pg/locale/fr_FR/LC_MESSAGES/gn2pg.po~ → gn2pg_client-1.10.0/gn2pg/locale/fr_FR/LC_MESSAGES/gn2pg.po +293 -152
  20. gn2pg_client-1.9.0.dev0/gn2pg/locale/fr_FR/LC_MESSAGES/gn2pg.po → gn2pg_client-1.10.0/gn2pg/locale/fr_FR/LC_MESSAGES/gn2pg.po~ +138 -89
  21. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/main.py +57 -13
  22. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/store_postgresql.py +311 -351
  23. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/utils.py +16 -0
  24. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/pyproject.toml +19 -18
  25. gn2pg_client-1.9.0.dev0/PKG-INFO +0 -251
  26. gn2pg_client-1.9.0.dev0/README.md +0 -204
  27. gn2pg_client-1.9.0.dev0/gn2pg/api.py +0 -252
  28. gn2pg_client-1.9.0.dev0/gn2pg/app/models.py +0 -69
  29. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/LICENSE +0 -0
  30. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/__init__.py +0 -0
  31. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/app/__init__.py +0 -0
  32. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/app/database.py +0 -0
  33. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/app/env.py +0 -0
  34. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/app/index.py +0 -0
  35. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/app/static/css/custom_flaks_admin.css +0 -0
  36. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/app/static/img/src_gn2pg.png +0 -0
  37. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/app/templates/admin/master.html +0 -0
  38. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/app/templates/index.html +0 -0
  39. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/app/utils/__init__.py +0 -0
  40. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/app/utils/admin_views.py +0 -0
  41. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/data/__init__.py +0 -0
  42. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/env.py +0 -0
  43. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/locale/en_US/LC_MESAGES/gn2pg.po +0 -0
  44. {gn2pg_client-1.9.0.dev0 → gn2pg_client-1.10.0}/gn2pg/logger.py +0 -0
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: gn2pg_client
3
+ Version: 1.10.0
4
+ Summary: Import tool from GeoNature to a PostgreSQL database through Export module API (client side)
5
+ License: AGPL-3.0-or-later
6
+ License-File: LICENSE
7
+ Keywords: GeoNature,Export,SINP,opendata,biodiversity
8
+ Author: Collectif
9
+ Maintainer: lpofredc
10
+ Maintainer-email: frederic.cloitre@lpo.fr
11
+ Requires-Python: >=3.10,<4.0
12
+ Classifier: Development Status :: 1 - Planning
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
16
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
17
+ Classifier: Natural Language :: French
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Programming Language :: Python :: 3.8
26
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
27
+ Classifier: Topic :: Documentation
28
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
29
+ Classifier: Topic :: System :: Installation/Setup
30
+ Classifier: Topic :: System :: Software Distribution
31
+ Provides-Extra: dashboard
32
+ Requires-Dist: alembic (>=1.16,<2.0)
33
+ Requires-Dist: coloredlogs (>=0.0.0)
34
+ Requires-Dist: flask (>=2,<3) ; extra == "dashboard"
35
+ Requires-Dist: flask-admin (>=1,<2) ; extra == "dashboard"
36
+ Requires-Dist: flask-sqlalchemy (>=3,<4) ; extra == "dashboard"
37
+ Requires-Dist: gunicorn (>=0.0.0) ; extra == "dashboard"
38
+ Requires-Dist: psycopg2-binary (>=2,<3)
39
+ Requires-Dist: python-decouple (>=3.8,<4.0) ; extra == "dashboard"
40
+ Requires-Dist: python-dotenv (>=1,<2) ; extra == "dashboard"
41
+ Requires-Dist: requests (>=2,<3)
42
+ Requires-Dist: schema (>=0,<1)
43
+ Requires-Dist: sqlalchemy (>=2.0,<3.0)
44
+ Requires-Dist: toml (>=0,<1)
45
+ Project-URL: Documentation, https://lpoaura.github.io/GN2PG/
46
+ Project-URL: Homepage, https://github.com/lpoaura/gn2gn_client/
47
+ Description-Content-Type: text/markdown
48
+
49
+ # GN2PG Client
50
+
51
+ ![https://www.python.org/](https://img.shields.io/badge/python-3.10+-yellowgreen)
52
+ ![https://www.postgresql.org/](https://img.shields.io/badge/PostgreSQL-10+-blue)
53
+ ![https://python-poetry.org/](https://img.shields.io/badge/packaging%20tool-poetry-important)
54
+ ![https://github.com/psf/black](https://img.shields.io/badge/code%20style-black-black)
55
+ ![https://opensource.org/licenses/AGPL-3.0](https://img.shields.io/badge/licence-AGPL--3.0-blue)
56
+
57
+ This project provides an import data from [GeoNature] instances to a PostgreSQL database (client side).
58
+ Widely inspired from [ClientApiVN](https://framagit.org/lpo/Client_API_VN/)
59
+
60
+ > [!WARNING]
61
+ > The minimum version of the source GeoNature instance required for the incremental update must be a version 2.12.0
62
+
63
+ ![Project logo](./docs/_static/src_gn2pg.png)
64
+
65
+ ## Project Setup
66
+
67
+ GN2PG Client can be installed by running `pip`. It requires Python 3.10 or above to run.
68
+
69
+ ```bash
70
+ pip install gn2pg-client
71
+ ```
72
+
73
+ ## Issues
74
+
75
+ Please report any bugs or requests that you have using the [GitHub issue tracker](https://github.com/lpoaura/gn2pg_client/issues)!
76
+
77
+ ## [HowTo](https://lpoaura.github.io/GN2PG/usage/howto.html)
78
+
79
+ ## [Contributing](https://lpoaura.github.io/GN2PG/development/contribute.html)
80
+
81
+ ## Licence
82
+
83
+ [GNU AGPLv3](https://www.gnu.org/licenses/gpl.html)
84
+
85
+ ## Team
86
+
87
+ <a href="">
88
+ <img height="100px" src="https://auvergne-rhone-alpes.lpo.fr/wp-content/uploads/LPO_AuRA.svg" title="DREAL AuRA">
89
+ </a>
90
+
91
+ [@lpofredc](https://github.com/lpofredc/) ([LPO Auvergne-Rhône-Alpes](https://github.com/lpoaura/)), lead developer
92
+
93
+ ---
94
+
95
+ <a href="https://github.com/lpoaura/GN2PG/graphs/contributors">
96
+ <img src="https://contrib.rocks/image?repo=lpoaura/GN2PG" />
97
+ </a>
98
+
99
+ ---
100
+
101
+ With the financial support of the [DREAL Auvergne-Rhône-Alpes](http://www.auvergne-rhone-alpes.developpement-durable.gouv.fr/) and the [Office français de la biodiversité](https://www.ofb.gouv.fr/).
102
+
103
+
104
+ <img height="100px" src="https://data.lpo-aura.org/web/images/blocmarque_pref_region_auvergne_rhone_alpes_rvb_web.png" title="DREAL AuRA"> <img height="100px" src="https://www.ofb.gouv.fr/sites/default/files/logo-ofb.png" title="OFB" style="padding-left:10px;">
105
+
106
+ [geonature]: https://geonature.fr
107
+
@@ -0,0 +1,58 @@
1
+ # GN2PG Client
2
+
3
+ ![https://www.python.org/](https://img.shields.io/badge/python-3.10+-yellowgreen)
4
+ ![https://www.postgresql.org/](https://img.shields.io/badge/PostgreSQL-10+-blue)
5
+ ![https://python-poetry.org/](https://img.shields.io/badge/packaging%20tool-poetry-important)
6
+ ![https://github.com/psf/black](https://img.shields.io/badge/code%20style-black-black)
7
+ ![https://opensource.org/licenses/AGPL-3.0](https://img.shields.io/badge/licence-AGPL--3.0-blue)
8
+
9
+ This project provides an import data from [GeoNature] instances to a PostgreSQL database (client side).
10
+ Widely inspired from [ClientApiVN](https://framagit.org/lpo/Client_API_VN/)
11
+
12
+ > [!WARNING]
13
+ > The minimum version of the source GeoNature instance required for the incremental update must be a version 2.12.0
14
+
15
+ ![Project logo](./docs/_static/src_gn2pg.png)
16
+
17
+ ## Project Setup
18
+
19
+ GN2PG Client can be installed by running `pip`. It requires Python 3.10 or above to run.
20
+
21
+ ```bash
22
+ pip install gn2pg-client
23
+ ```
24
+
25
+ ## Issues
26
+
27
+ Please report any bugs or requests that you have using the [GitHub issue tracker](https://github.com/lpoaura/gn2pg_client/issues)!
28
+
29
+ ## [HowTo](https://lpoaura.github.io/GN2PG/usage/howto.html)
30
+
31
+ ## [Contributing](https://lpoaura.github.io/GN2PG/development/contribute.html)
32
+
33
+ ## Licence
34
+
35
+ [GNU AGPLv3](https://www.gnu.org/licenses/gpl.html)
36
+
37
+ ## Team
38
+
39
+ <a href="">
40
+ <img height="100px" src="https://auvergne-rhone-alpes.lpo.fr/wp-content/uploads/LPO_AuRA.svg" title="DREAL AuRA">
41
+ </a>
42
+
43
+ [@lpofredc](https://github.com/lpofredc/) ([LPO Auvergne-Rhône-Alpes](https://github.com/lpoaura/)), lead developer
44
+
45
+ ---
46
+
47
+ <a href="https://github.com/lpoaura/GN2PG/graphs/contributors">
48
+ <img src="https://contrib.rocks/image?repo=lpoaura/GN2PG" />
49
+ </a>
50
+
51
+ ---
52
+
53
+ With the financial support of the [DREAL Auvergne-Rhône-Alpes](http://www.auvergne-rhone-alpes.developpement-durable.gouv.fr/) and the [Office français de la biodiversité](https://www.ofb.gouv.fr/).
54
+
55
+
56
+ <img height="100px" src="https://data.lpo-aura.org/web/images/blocmarque_pref_region_auvergne_rhone_alpes_rvb_web.png" title="DREAL AuRA"> <img height="100px" src="https://www.ofb.gouv.fr/sites/default/files/logo-ofb.png" title="OFB" style="padding-left:10px;">
57
+
58
+ [geonature]: https://geonature.fr
@@ -0,0 +1,72 @@
1
+ """Alembic environment for GN2PG's configurable import schema."""
2
+
3
+ # Alembic exposes context operations dynamically at migration runtime.
4
+ # pylint: disable=no-member
5
+
6
+ from alembic import context
7
+ from sqlalchemy import engine_from_config, pool, text
8
+ from sqlalchemy.dialects import postgresql
9
+
10
+ from gn2pg.database import build_metadata
11
+
12
+ config = context.config
13
+ schema = config.attributes.get("schema", "gn2pg_import")
14
+ target_metadata = build_metadata(schema)
15
+
16
+
17
+ def include_name(name, object_type, parent_names) -> bool:
18
+ """Restrict reflection to the configured GN2PG schema."""
19
+ if object_type == "schema":
20
+ return name == schema
21
+ if object_type == "table":
22
+ return parent_names.get("schema_name") == schema
23
+ return True
24
+
25
+
26
+ def run_migrations_offline() -> None:
27
+ """Run migrations without creating an Engine."""
28
+ context.configure(
29
+ url=config.get_main_option("sqlalchemy.url"),
30
+ target_metadata=target_metadata,
31
+ literal_binds=True,
32
+ dialect_opts={"paramstyle": "named"},
33
+ include_schemas=True,
34
+ include_name=include_name,
35
+ version_table="alembic_version",
36
+ version_table_schema=schema,
37
+ schema=schema,
38
+ )
39
+ quoted_schema = postgresql.dialect().identifier_preparer.quote(schema)
40
+ context.execute(f"CREATE SCHEMA IF NOT EXISTS {quoted_schema}")
41
+ with context.begin_transaction():
42
+ context.run_migrations()
43
+
44
+
45
+ def run_migrations_online() -> None:
46
+ """Run migrations with a live database connection."""
47
+ connectable = engine_from_config(
48
+ config.get_section(config.config_ini_section, {}),
49
+ prefix="sqlalchemy.",
50
+ poolclass=pool.NullPool,
51
+ )
52
+ with connectable.connect() as connection:
53
+ quoted_schema = connection.dialect.identifier_preparer.quote(schema)
54
+ connection.execute(text(f"CREATE SCHEMA IF NOT EXISTS {quoted_schema}"))
55
+ connection.commit()
56
+ context.configure(
57
+ connection=connection,
58
+ target_metadata=target_metadata,
59
+ include_schemas=True,
60
+ include_name=include_name,
61
+ version_table="alembic_version",
62
+ version_table_schema=schema,
63
+ schema=schema,
64
+ )
65
+ with context.begin_transaction():
66
+ context.run_migrations()
67
+
68
+
69
+ if context.is_offline_mode():
70
+ run_migrations_offline()
71
+ else:
72
+ run_migrations_online()
@@ -0,0 +1,25 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+ """
7
+
8
+ from typing import Optional, Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ ${imports if imports else ""}
13
+
14
+ revision: str = ${repr(up_revision)}
15
+ down_revision: Optional[str] = ${repr(down_revision)}
16
+ branch_labels: Optional[Union[str, Sequence[str]]] = ${repr(branch_labels)}
17
+ depends_on: Optional[Union[str, Sequence[str]]] = ${repr(depends_on)}
18
+
19
+
20
+ def upgrade() -> None:
21
+ ${upgrades if upgrades else "pass"}
22
+
23
+
24
+ def downgrade() -> None:
25
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,134 @@
1
+ """Create the initial GN2PG import schema.
2
+
3
+ Revision ID: 20260818_01
4
+ Revises: None
5
+ """
6
+
7
+ # pylint: disable=invalid-name,no-member
8
+
9
+ import sqlalchemy as sa
10
+ from alembic import op
11
+ from sqlalchemy.dialects import postgresql
12
+
13
+ revision = "20260818_01"
14
+ down_revision = None
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def _schema() -> str:
20
+ return op.get_context().opts.get("schema", "gn2pg_import")
21
+
22
+
23
+ def _index(schema: str, table: str, column: str) -> str:
24
+ return f"ix_{schema}_{table}_{column}"
25
+
26
+
27
+ def upgrade() -> None:
28
+ """Create extensions and an immutable snapshot of the initial schema."""
29
+ schema = _schema()
30
+ op.execute('CREATE EXTENSION IF NOT EXISTS "pgcrypto"')
31
+ op.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"')
32
+ op.create_table(
33
+ "import_log",
34
+ sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
35
+ sa.Column("source", sa.String(), nullable=False),
36
+ sa.Column("controler", sa.String(), nullable=False),
37
+ sa.Column("xfer_type", sa.String(), nullable=True),
38
+ sa.Column("xfer_status", sa.String(), nullable=True),
39
+ sa.Column("xfer_start_ts", sa.DateTime(), nullable=False),
40
+ sa.Column("xfer_end_ts", sa.DateTime(), nullable=True),
41
+ sa.Column("api_count_items", sa.Integer(), server_default=sa.text("0"), nullable=False),
42
+ sa.Column("api_count_errors", sa.Integer(), server_default=sa.text("0"), nullable=False),
43
+ sa.Column("data_count_upserts", sa.Integer(), server_default=sa.text("0"), nullable=False),
44
+ sa.Column("data_count_delete", sa.Integer(), server_default=sa.text("0"), nullable=False),
45
+ sa.Column("data_count_errors", sa.Integer(), server_default=sa.text("0"), nullable=False),
46
+ sa.Column(
47
+ "metadata_count_upserts", sa.Integer(), server_default=sa.text("0"), nullable=False
48
+ ),
49
+ sa.Column(
50
+ "metadata_count_errors", sa.Integer(), server_default=sa.text("0"), nullable=False
51
+ ),
52
+ sa.Column(
53
+ "xfer_filters",
54
+ postgresql.JSONB(astext_type=sa.Text()),
55
+ server_default=sa.text("'{}'::jsonb"),
56
+ nullable=True,
57
+ ),
58
+ sa.Column("comment", sa.Text(), nullable=True),
59
+ sa.PrimaryKeyConstraint("id"),
60
+ schema=schema,
61
+ )
62
+ op.create_index(
63
+ _index(schema, "import_log", "source"), "import_log", ["source"], schema=schema
64
+ )
65
+ op.create_index(
66
+ _index(schema, "import_log", "xfer_type"), "import_log", ["xfer_type"], schema=schema
67
+ )
68
+ op.create_table(
69
+ "error_log",
70
+ sa.Column("source", sa.String(), nullable=False),
71
+ sa.Column("uuid", postgresql.UUID(), nullable=False),
72
+ sa.Column("controler", sa.String(), nullable=False),
73
+ sa.Column("last_ts", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
74
+ sa.Column("item", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
75
+ sa.Column("error", sa.String(), nullable=True),
76
+ sa.Column("import_id", sa.Integer(), nullable=True),
77
+ sa.ForeignKeyConstraint(
78
+ ["import_id"],
79
+ [f"{schema}.import_log.id"],
80
+ onupdate="CASCADE",
81
+ ondelete="CASCADE",
82
+ ),
83
+ schema=schema,
84
+ )
85
+ op.create_index(
86
+ _index(schema, "error_log", "import_id"), "error_log", ["import_id"], schema=schema
87
+ )
88
+ op.create_index(_index(schema, "error_log", "uuid"), "error_log", ["uuid"], schema=schema)
89
+ op.create_table(
90
+ "data_json",
91
+ sa.Column("source", sa.String(), nullable=False),
92
+ sa.Column("controler", sa.String(), nullable=False),
93
+ sa.Column("type", sa.String(), nullable=False),
94
+ sa.Column("id_data", sa.Integer(), nullable=False),
95
+ sa.Column("uuid", postgresql.UUID(), nullable=True),
96
+ sa.Column("item", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
97
+ sa.Column("update_ts", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
98
+ sa.Column("import_id", sa.Integer(), nullable=True),
99
+ sa.ForeignKeyConstraint(["import_id"], [f"{schema}.import_log.id"], onupdate="CASCADE"),
100
+ sa.PrimaryKeyConstraint("id_data", "source", "type", name="pk_source_data"),
101
+ sa.UniqueConstraint("uuid", name="unique_uuid"),
102
+ schema=schema,
103
+ )
104
+ op.create_index(
105
+ _index(schema, "data_json", "id_data"), "data_json", ["id_data"], schema=schema
106
+ )
107
+ op.create_index(_index(schema, "data_json", "uuid"), "data_json", ["uuid"], schema=schema)
108
+ op.create_table(
109
+ "metadata_json",
110
+ sa.Column("source", sa.String(), nullable=False),
111
+ sa.Column("controler", sa.String(), nullable=False),
112
+ sa.Column("type", sa.String(), nullable=False),
113
+ sa.Column("level", sa.String(), nullable=False),
114
+ sa.Column("uuid", postgresql.UUID(), nullable=False),
115
+ sa.Column("item", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
116
+ sa.Column("update_ts", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
117
+ sa.Column("import_id", sa.Integer(), nullable=True),
118
+ sa.ForeignKeyConstraint(["import_id"], [f"{schema}.import_log.id"], onupdate="CASCADE"),
119
+ sa.PrimaryKeyConstraint("uuid", "source", name="pk_source_metadata"),
120
+ sa.UniqueConstraint("uuid", name="metadata_unique_uuid"),
121
+ schema=schema,
122
+ )
123
+ op.create_index(
124
+ _index(schema, "metadata_json", "uuid"), "metadata_json", ["uuid"], schema=schema
125
+ )
126
+
127
+
128
+ def downgrade() -> None:
129
+ """Drop GN2PG tables while preserving the schema and shared extensions."""
130
+ schema = _schema()
131
+ op.drop_table("metadata_json", schema=schema)
132
+ op.drop_table("data_json", schema=schema)
133
+ op.drop_table("error_log", schema=schema)
134
+ op.drop_table("import_log", schema=schema)