hdx-python-database 1.3.8__py3-none-any.whl → 1.4.0__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.
hdx/database/_version.py CHANGED
@@ -1,5 +1,6 @@
1
- # file generated by setuptools-scm
1
+ # file generated by vcs-versioning
2
2
  # don't change, don't track in version control
3
+ from __future__ import annotations
3
4
 
4
5
  __all__ = [
5
6
  "__version__",
@@ -10,25 +11,14 @@ __all__ = [
10
11
  "commit_id",
11
12
  ]
12
13
 
13
- TYPE_CHECKING = False
14
- if TYPE_CHECKING:
15
- from typing import Tuple
16
- from typing import Union
17
-
18
- VERSION_TUPLE = Tuple[Union[int, str], ...]
19
- COMMIT_ID = Union[str, None]
20
- else:
21
- VERSION_TUPLE = object
22
- COMMIT_ID = object
23
-
24
14
  version: str
25
15
  __version__: str
26
- __version_tuple__: VERSION_TUPLE
27
- version_tuple: VERSION_TUPLE
28
- commit_id: COMMIT_ID
29
- __commit_id__: COMMIT_ID
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
30
20
 
31
- __version__ = version = '1.3.8'
32
- __version_tuple__ = version_tuple = (1, 3, 8)
21
+ __version__ = version = '1.4.0'
22
+ __version_tuple__ = version_tuple = (1, 4, 0)
33
23
 
34
24
  __commit_id__ = commit_id = None
hdx/database/database.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """Database utilities"""
2
2
 
3
3
  import logging
4
- from typing import Any, Dict, List, Optional, Tuple, Type, Union
4
+ from typing import Any
5
5
 
6
6
  from sqlalchemy import Engine, TableClause, create_engine, insert
7
7
  from sqlalchemy.exc import SQLAlchemyError
@@ -64,7 +64,8 @@ class Database:
64
64
  table_base (Optional[Type[DeclarativeBase]]): Override table base. Defaults to None.
65
65
  reflect (bool): Whether to reflect existing tables. Defaults to False.
66
66
  **kwargs: See below
67
- pg_restore_file (str): Restore database from pg_restore file
67
+ pg_restore_file (Path | str): Restore database from pg_restore file
68
+ pg_timeout (int): Seconds to wait for PostgreSQL. Defaults to None (wait forever).
68
69
  recreate_schema (bool): Whether to recreate schema
69
70
  schema_name (str): Database schema name. Defaults to "public".
70
71
  prepare_fn (Callable[[], None]]): Function to call before Base.metadata.create_all.
@@ -73,23 +74,24 @@ class Database:
73
74
  ssh_username (str): SSH username
74
75
  ssh_password (str): SSH password
75
76
  ssh_private_key: Location of SSH private key (instead of ssh_password)
77
+ ssh_host_key (str): SSH server host key to verify against. Strongly recommended.
76
78
  For more advanced usage, see SSHTunnelForwarder documentation.
77
79
 
78
80
  """
79
81
 
80
82
  def __init__(
81
83
  self,
82
- engine: Optional[Engine] = None,
83
- db_uri: Optional[str] = None,
84
- database: Optional[str] = None,
85
- host: Optional[str] = None,
86
- port: Union[int, str, None] = None,
87
- username: Optional[str] = None,
88
- password: Optional[str] = None,
84
+ engine: Engine | None = None,
85
+ db_uri: str | None = None,
86
+ database: str | None = None,
87
+ host: str | None = None,
88
+ port: int | str | None = None,
89
+ username: str | None = None,
90
+ password: str | None = None,
89
91
  dialect: str = "postgresql",
90
- driver: Optional[str] = None,
92
+ driver: str | None = None,
91
93
  db_has_tz: bool = False,
92
- table_base: Optional[Type[DeclarativeBase]] = None,
94
+ table_base: type[DeclarativeBase] | None = None,
93
95
  reflect: bool = False,
94
96
  **kwargs: Any,
95
97
  ) -> None:
@@ -98,10 +100,12 @@ class Database:
98
100
  schema_name = None
99
101
  if len(kwargs) == 0:
100
102
  pg_restore_file = None
103
+ pg_timeout = None
101
104
  recreate_schema = False
102
105
  prepare_fn = do_nothing_fn
103
106
  else:
104
107
  pg_restore_file = kwargs.pop("pg_restore_file", None)
108
+ pg_timeout = kwargs.pop("pg_timeout", None)
105
109
  recreate_schema = kwargs.pop("recreate_schema", False)
106
110
  schema_name = kwargs.pop("schema", "public")
107
111
  prepare_fn = kwargs.pop("prepare_fn", do_nothing_fn)
@@ -116,6 +120,10 @@ class Database:
116
120
  raise
117
121
  ssh_host = kwargs["ssh_host"]
118
122
  del kwargs["ssh_host"]
123
+ if "ssh_host_key" not in kwargs:
124
+ logger.warning(
125
+ "ssh_host_key not supplied: SSH server identity will not be verified."
126
+ )
119
127
  ssh_port = kwargs.get("ssh_port")
120
128
  if ssh_port is not None:
121
129
  ssh_port = int(ssh_port)
@@ -147,7 +155,7 @@ class Database:
147
155
  "No engine, database uri or connection parameters provided!"
148
156
  )
149
157
  if not engine and dialect == "postgresql":
150
- wait_for_postgresql(db_uri)
158
+ wait_for_postgresql(db_uri, pg_timeout)
151
159
  if pg_restore_file and db_uri:
152
160
  restore_from_pgfile(db_uri, pg_restore_file)
153
161
  if not table_base:
@@ -230,8 +238,8 @@ class Database:
230
238
 
231
239
  def batch_populate(
232
240
  self,
233
- rows: List[Dict],
234
- dbtable: Type[DeclarativeBase],
241
+ rows: list[dict],
242
+ dbtable: type[DeclarativeBase],
235
243
  batch_size: int = 1000,
236
244
  ) -> None:
237
245
  """Batch populate database table.
@@ -252,11 +260,11 @@ class Database:
252
260
 
253
261
  @staticmethod
254
262
  def create_session(
255
- engine: Optional[Engine] = None,
256
- db_uri: Optional[str] = None,
257
- table_base: Type[DeclarativeBase] = NoTZBase,
263
+ engine: Engine | None = None,
264
+ db_uri: str | None = None,
265
+ table_base: type[DeclarativeBase] = NoTZBase,
258
266
  reflect: bool = False,
259
- ) -> Tuple[Session, Any]:
267
+ ) -> tuple[Session, Any]:
260
268
  """Creates SQLAlchemy session given SQLAlchemy engine or database uri
261
269
  (one of which must be supplied). Tables must inherit from Base in
262
270
  hdx.utilities.database unless base is defined. If reflect is True,
@@ -332,7 +340,7 @@ class Database:
332
340
  return False
333
341
 
334
342
  @staticmethod
335
- def prepare_view(view_params: Dict) -> TableClause:
343
+ def prepare_view(view_params: dict) -> TableClause:
336
344
  """Prepare SQLAlchemy view from dictionary with keys: name, metadata and
337
345
  selectable. Must be run before Base.metadata.create_all.
338
346
 
@@ -345,7 +353,7 @@ class Database:
345
353
  return view(**view_params)
346
354
 
347
355
  @classmethod
348
- def prepare_views(cls, view_params_list: List[Dict]) -> List[TableClause]:
356
+ def prepare_views(cls, view_params_list: list[dict]) -> list[TableClause]:
349
357
  """Prepare SQLAlchemy views from a list of dictionaries with keys: name,
350
358
  metadata and selectable. Must be run before Base.metadata.create_all.
351
359
 
hdx/database/dburi.py CHANGED
@@ -1,13 +1,13 @@
1
1
  """Connection URI utilities"""
2
2
 
3
- from typing import Any, Dict, Optional, Union
4
- from urllib.parse import urlsplit
3
+ from typing import Any
4
+ from urllib.parse import quote_plus, unquote, urlsplit
5
5
 
6
6
 
7
7
  def get_params_from_connection_uri(
8
8
  db_uri: str,
9
9
  include_driver: bool = True,
10
- ) -> Dict[str, Any]:
10
+ ) -> dict[str, Any]:
11
11
  """Gets PostgreSQL database connection parameters from connection URI
12
12
 
13
13
  Args:
@@ -28,8 +28,8 @@ def get_params_from_connection_uri(
28
28
  "database": result.path[1:],
29
29
  "host": result.hostname,
30
30
  "port": result.port,
31
- "username": result.username,
32
- "password": result.password,
31
+ "username": unquote(result.username) if result.username else None,
32
+ "password": unquote(result.password) if result.password else None,
33
33
  "dialect": dialect,
34
34
  }
35
35
  if include_driver:
@@ -38,13 +38,13 @@ def get_params_from_connection_uri(
38
38
 
39
39
 
40
40
  def get_connection_uri(
41
- database: Optional[str] = None,
42
- host: Optional[str] = None,
43
- port: Union[int, str, None] = None,
44
- username: Optional[str] = None,
45
- password: Optional[str] = None,
41
+ database: str | None = None,
42
+ host: str | None = None,
43
+ port: int | str | None = None,
44
+ username: str | None = None,
45
+ password: str | None = None,
46
46
  dialect: str = "postgresql",
47
- driver: Optional[str] = None,
47
+ driver: str | None = None,
48
48
  include_driver: bool = True,
49
49
  ) -> str:
50
50
  """Gets connection uri from database connection parameters
@@ -72,9 +72,9 @@ def get_connection_uri(
72
72
  strings.append(f"+{driver}")
73
73
  strings.append("://")
74
74
  if username:
75
- strings.append(username)
75
+ strings.append(quote_plus(username))
76
76
  if password:
77
- strings.append(f":{password}@")
77
+ strings.append(f":{quote_plus(password)}@")
78
78
  else:
79
79
  strings.append("@")
80
80
  if host:
@@ -1,7 +1,6 @@
1
1
  """Utilities for database datetime columns without timezone"""
2
2
 
3
- from datetime import datetime, timezone
4
- from typing import Optional
3
+ from datetime import UTC, datetime
5
4
 
6
5
  from sqlalchemy import DateTime, TypeDecorator
7
6
  from sqlalchemy.orm import DeclarativeBase, declared_attr
@@ -16,11 +15,11 @@ class ConversionNoTZ(TypeDecorator):
16
15
  impl = DateTime
17
16
  cache_ok = True
18
17
 
19
- def process_bind_param(self, value: Optional[datetime], _):
18
+ def process_bind_param(self, value: datetime | None, _):
20
19
  if value is None:
21
20
  return value
22
21
  if value.tzinfo:
23
- value = value.astimezone(timezone.utc)
22
+ value = value.astimezone(UTC)
24
23
 
25
24
  return value.replace(tzinfo=None)
26
25
 
@@ -28,9 +27,9 @@ class ConversionNoTZ(TypeDecorator):
28
27
  if value is None:
29
28
  return value
30
29
  if value.tzinfo is None:
31
- return value.replace(tzinfo=timezone.utc)
30
+ return value.replace(tzinfo=UTC)
32
31
 
33
- return value.astimezone(timezone.utc)
32
+ return value.astimezone(UTC)
34
33
 
35
34
 
36
35
  class Base(DeclarativeBase):
@@ -4,6 +4,7 @@ import logging
4
4
  import subprocess
5
5
  import time
6
6
  from os import environ
7
+ from pathlib import Path
7
8
 
8
9
  from .dburi import get_params_from_connection_uri, remove_driver_from_uri
9
10
 
@@ -25,17 +26,19 @@ class PostgresError(Exception):
25
26
  pass
26
27
 
27
28
 
28
- def wait_for_postgresql(db_uri: str) -> None:
29
+ def wait_for_postgresql(db_uri: str, timeout: int | None = None) -> None:
29
30
  """Waits for PostgreSQL database to be up
30
31
 
31
32
  Args:
32
33
  db_uri (str): Connection URI
34
+ timeout (Optional[int]): Seconds to wait. Defaults to None (wait forever).
33
35
 
34
36
  Returns:
35
37
  None
36
38
  """
37
39
  connecting_string = "Checking for PostgreSQL..."
38
40
  db_uri_nd = remove_driver_from_uri(db_uri)
41
+ start = time.monotonic()
39
42
  while True:
40
43
  try:
41
44
  logger.info(connecting_string)
@@ -44,22 +47,25 @@ def wait_for_postgresql(db_uri: str) -> None:
44
47
  logger.info("PostgreSQL is running!")
45
48
  break
46
49
  except psycopg.OperationalError:
50
+ if timeout is not None and time.monotonic() - start > timeout:
51
+ raise PostgresError(f"PostgreSQL not available after {timeout}s")
47
52
  time.sleep(1)
48
53
 
49
54
 
50
- def restore_from_pgfile(db_uri: str, pg_restore_file: str) -> str:
55
+ def restore_from_pgfile(db_uri: str, pg_restore_file: Path | str) -> str:
51
56
  """Restore database from a pg_restore file created by pg_backup using the
52
57
  pg_restore command.
53
58
 
54
59
  Args:
55
60
  db_uri (str): Connection URI
56
- pg_restore_file (str): Path to the pg_restore database file
61
+ pg_restore_file (Path | str): Path to the pg_restore database file
57
62
 
58
63
  Returns:
59
64
  str: Output from the pg_restore command
60
65
  """
66
+ pg_restore_file = Path(pg_restore_file).absolute()
61
67
  db_params = get_params_from_connection_uri(db_uri)
62
- if pg_restore_file[-3:] == ".xz":
68
+ if pg_restore_file.suffix == ".xz":
63
69
  decompress = subprocess.Popen(
64
70
  ("unxz", "-c", "-d", pg_restore_file), stdout=subprocess.PIPE
65
71
  ).stdout
@@ -81,7 +87,7 @@ def restore_from_pgfile(db_uri: str, pg_restore_file: str) -> str:
81
87
  subprocess_params.append(f"{value}")
82
88
 
83
89
  if not decompress:
84
- subprocess_params.append(pg_restore_file)
90
+ subprocess_params.append(str(pg_restore_file))
85
91
  env = environ.copy()
86
92
  password = db_params.get("password")
87
93
  if password:
hdx/database/views.py CHANGED
@@ -24,15 +24,13 @@ class DropView(DDLElement):
24
24
 
25
25
  @compiler.compiles(CreateView)
26
26
  def _create_view(element, compiler, **kw):
27
- return "CREATE VIEW %s AS %s" % (
28
- element.name,
29
- compiler.sql_compiler.process(element.selectable, literal_binds=True),
30
- )
27
+ name = compiler.preparer.quote(element.name)
28
+ return f"CREATE VIEW {name} AS {compiler.sql_compiler.process(element.selectable, literal_binds=True)}"
31
29
 
32
30
 
33
31
  @compiler.compiles(DropView)
34
32
  def _drop_view(element, compiler, **kw):
35
- return "DROP VIEW %s" % (element.name)
33
+ return f"DROP VIEW {compiler.preparer.quote(element.name)}"
36
34
 
37
35
 
38
36
  def view_exists(ddl, target, connection, **kw):
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: hdx-python-database
3
- Version: 1.3.8
3
+ Version: 1.4.0
4
4
  Summary: HDX Python database utilities
5
5
  Project-URL: Homepage, https://github.com/OCHA-DAP/hdx-python-database
6
- Author-email: Michael Rans <rans@email.com>
6
+ Author-email: Michael Rans <rans@un.org>
7
7
  License: MIT
8
8
  License-File: LICENSE
9
9
  Keywords: HDX,database,postgresql
@@ -16,29 +16,16 @@ Classifier: Operating System :: Microsoft :: Windows
16
16
  Classifier: Operating System :: POSIX :: Linux
17
17
  Classifier: Operating System :: Unix
18
18
  Classifier: Programming Language :: Python
19
- Classifier: Programming Language :: Python :: 3
20
- Classifier: Programming Language :: Python :: 3 :: Only
21
- Classifier: Programming Language :: Python :: 3.8
22
- Classifier: Programming Language :: Python :: 3.9
23
- Classifier: Programming Language :: Python :: 3.10
24
- Classifier: Programming Language :: Python :: 3.11
25
- Classifier: Programming Language :: Python :: 3.12
26
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
- Requires-Python: >=3.10
20
+ Requires-Python: >=3.11
28
21
  Requires-Dist: sqlalchemy>=2
29
- Provides-Extra: dev
30
- Requires-Dist: pre-commit; extra == 'dev'
31
22
  Provides-Extra: docs
32
23
  Requires-Dist: mkapi; extra == 'docs'
33
24
  Provides-Extra: postgresql
34
25
  Requires-Dist: psycopg[binary]; extra == 'postgresql'
35
26
  Provides-Extra: sshtunnel
36
- Requires-Dist: sshtunnel; extra == 'sshtunnel'
37
- Provides-Extra: test
38
- Requires-Dist: psycopg[binary]; extra == 'test'
39
- Requires-Dist: pytest; extra == 'test'
40
- Requires-Dist: pytest-cov; extra == 'test'
41
- Requires-Dist: sshtunnel; extra == 'test'
27
+ Requires-Dist: cryptography>=48.0.1; extra == 'sshtunnel'
28
+ Requires-Dist: sshtunnel>=0.5.1; extra == 'sshtunnel'
42
29
  Description-Content-Type: text/markdown
43
30
 
44
31
  [![Build Status](https://github.com/OCHA-DAP/hdx-python-database/actions/workflows/run-python-tests.yaml/badge.svg)](https://github.com/OCHA-DAP/hdx-python-database/actions/workflows/run-python-tests.yaml)
@@ -53,3 +40,78 @@ For more information, please read the [documentation](https://hdx-python-databas
53
40
 
54
41
  This library is part of the [Humanitarian Data Exchange](https://data.humdata.org/) (HDX) project. If you have
55
42
  humanitarian related data, please upload your datasets to HDX.
43
+
44
+ # Development
45
+
46
+ ## Environment
47
+
48
+ Development is currently done using Python 3.13. The environment can be created with:
49
+
50
+ ```shell
51
+ uv sync
52
+ ```
53
+
54
+ This creates a .venv folder with the versions specified in the project's uv.lock file.
55
+
56
+ ### Pre-commit
57
+
58
+ pre-commit will be installed when syncing uv. It is run every time you make a git
59
+ commit if you call it like this:
60
+
61
+ ```shell
62
+ pre-commit install
63
+ ```
64
+
65
+ With pre-commit, all code is formatted according to
66
+ [ruff](https://docs.astral.sh/ruff/) guidelines.
67
+
68
+ To check if your changes pass pre-commit without committing, run:
69
+
70
+ ```shell
71
+ pre-commit run --all-files
72
+ ```
73
+
74
+ ## Packages
75
+
76
+ [uv](https://github.com/astral-sh/uv) is used for package management. If
77
+ you've introduced a new package to the source code (i.e. anywhere in `src/`),
78
+ please add it to the `project.dependencies` section of `pyproject.toml` with
79
+ any known version constraints.
80
+
81
+ To add packages required only for testing, add them to the
82
+ `[dependency-groups]`.
83
+
84
+ Any changes to the dependencies will be automatically reflected in
85
+ `uv.lock` with `pre-commit`, but you can re-generate the files without committing by
86
+ executing:
87
+
88
+ ```shell
89
+ uv lock --upgrade
90
+ ```
91
+
92
+ ## Project
93
+
94
+ [uv](https://github.com/astral-sh/uv) is used for project management. The project can be
95
+ built using:
96
+
97
+ ```shell
98
+ uv build
99
+ ```
100
+
101
+ Linting and syntax checking can be run with:
102
+
103
+ ```shell
104
+ uv run ruff check
105
+ ```
106
+
107
+ To run the tests and view coverage, execute:
108
+
109
+ ```shell
110
+ uv run pytest
111
+ ```
112
+
113
+ ## Documentation
114
+
115
+ The documentation, including API documentation, is generated using ReadtheDocs and
116
+ MkDocs with Material. As you change the source code, remember to update the
117
+ documentation at `documentation/index.md`.
@@ -0,0 +1,13 @@
1
+ hdx/database/__init__.py,sha256=sG2OYV5LHskgiDkB4IeVeQyPHL22Ta1kuxf-xN92-5E,155
2
+ hdx/database/_version.py,sha256=u2oUj6dCZIqQ271Pm6INHvXKeiux4BSapl6g0c_tllg,520
3
+ hdx/database/database.py,sha256=SA3XwmWMg7VIrUaWWRXVLMxXtKV4F9gz-jK5CJEPI18,13320
4
+ hdx/database/dburi.py,sha256=5PbvVe56MglpHxflAFeRY0TXli3XThZrDphPWmBp4ho,3055
5
+ hdx/database/no_timezone.py,sha256=MG4Z2t7SwjPKDnwDD0CVftoIZC0hxpSg-C-1FyEy0R0,1067
6
+ hdx/database/postgresql.py,sha256=JSLWQrAYps7Dxkb0En0kndr-9lNQhPpaY5UvxydTmb8,3321
7
+ hdx/database/utils.py,sha256=nzChgDj2_Mq8GS6xWaNmqOgpAiwK9tGYV_skKbErEVA,401
8
+ hdx/database/views.py,sha256=3ZQT5jS-q3w30zKypnyuy8-2BKwkyYC0It9om9hxmCs,1669
9
+ hdx/database/with_timezone.py,sha256=GUUylmTwQHp22yedmCVKpVJzvjWgdS9lg9KeqciNJkY,235
10
+ hdx_python_database-1.4.0.dist-info/METADATA,sha256=ezN2GRZ808XVFheZ6d7DFaWqsX-am5kQ8jLvbrVAegQ,3985
11
+ hdx_python_database-1.4.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
12
+ hdx_python_database-1.4.0.dist-info/licenses/LICENSE,sha256=wc-4GpMn-ODs-U_bTe1YCiPVgvcjzrpYOx2wPuyAeII,1079
13
+ hdx_python_database-1.4.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,13 +0,0 @@
1
- hdx/database/__init__.py,sha256=sG2OYV5LHskgiDkB4IeVeQyPHL22Ta1kuxf-xN92-5E,155
2
- hdx/database/_version.py,sha256=_hPMnsWQOeGw9lqV0fueN3y4Gw7vLcBaoT0Bio9pJWo,704
3
- hdx/database/database.py,sha256=pOmshEfhkvlVN-cWJ9LX8ZL99TyZnwPAHOC-v0nF1pM,12926
4
- hdx/database/dburi.py,sha256=rw8RuLpU85vxYl5-Zk1SdZRAz-tqre_ot5aEnkjhxAQ,2977
5
- hdx/database/no_timezone.py,sha256=Lw087G3qOf8H70o8Hq_VvOMZ3ANei-lIA_sUlpFU_Uo,1130
6
- hdx/database/postgresql.py,sha256=5xmGFbzjTPTr9clQw4rOD1ZQEzYuY25Ho2hhz-WdCC8,2923
7
- hdx/database/utils.py,sha256=nzChgDj2_Mq8GS6xWaNmqOgpAiwK9tGYV_skKbErEVA,401
8
- hdx/database/views.py,sha256=U4BN9tbjlr93qKkeNbZNo_PCIwGsng09mQ2xFqh5CH0,1636
9
- hdx/database/with_timezone.py,sha256=GUUylmTwQHp22yedmCVKpVJzvjWgdS9lg9KeqciNJkY,235
10
- hdx_python_database-1.3.8.dist-info/METADATA,sha256=fC99IRhtjoty5SpmkVDobyMMYS3SYhs68ftDUoxOrZM,2841
11
- hdx_python_database-1.3.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
- hdx_python_database-1.3.8.dist-info/licenses/LICENSE,sha256=wc-4GpMn-ODs-U_bTe1YCiPVgvcjzrpYOx2wPuyAeII,1079
13
- hdx_python_database-1.3.8.dist-info/RECORD,,