rda-python-common 2.1.10__tar.gz → 3.0.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 (35) hide show
  1. {rda_python_common-2.1.10/src/rda_python_common.egg-info → rda_python_common-3.0.0}/PKG-INFO +125 -25
  2. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/README.md +118 -22
  3. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/pyproject.toml +10 -3
  4. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgDBI.py +48 -7
  5. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgFile.py +1 -1
  6. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgLOG.py +37 -22
  7. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgOPT.py +2 -2
  8. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgSIG.py +2 -2
  9. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/__init__.py +1 -1
  10. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_dbi.py +77 -21
  11. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_file.py +19 -10
  12. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_log.py +54 -33
  13. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_opt.py +2 -2
  14. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_sig.py +96 -81
  15. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_util.py +36 -89
  16. {rda_python_common-2.1.10 → rda_python_common-3.0.0/src/rda_python_common.egg-info}/PKG-INFO +125 -25
  17. rda_python_common-3.0.0/src/rda_python_common.egg-info/requires.txt +11 -0
  18. rda_python_common-2.1.10/src/rda_python_common.egg-info/requires.txt +0 -5
  19. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/LICENSE +0 -0
  20. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/setup.cfg +0 -0
  21. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgCMD.py +0 -0
  22. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgLock.py +0 -0
  23. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgSplit.py +0 -0
  24. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgUtil.py +0 -0
  25. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_cmd.py +0 -0
  26. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_lock.py +0 -0
  27. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_password.py +0 -0
  28. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_split.py +0 -0
  29. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pgpassword.py +0 -0
  30. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pgpassword.usg +0 -0
  31. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common.egg-info/SOURCES.txt +0 -0
  32. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common.egg-info/dependency_links.txt +0 -0
  33. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common.egg-info/entry_points.txt +0 -0
  34. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common.egg-info/top_level.txt +0 -0
  35. {rda_python_common-2.1.10 → rda_python_common-3.0.0}/test/test_common.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_common
3
- Version: 2.1.10
3
+ Version: 3.0.0
4
4
  Summary: RDA Python common library codes shared by other RDA python packages
5
5
  Author-email: Zaihua Ji <zji@ucar.edu>
6
6
  Project-URL: Homepage, https://github.com/NCAR/rda-python-common
@@ -11,23 +11,45 @@ Classifier: Development Status :: 5 - Production/Stable
11
11
  Requires-Python: >=3.7
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Requires-Dist: hvac
15
- Requires-Dist: psycopg2==2.9.10
14
+ Requires-Dist: psycopg
15
+ Requires-Dist: psutil
16
16
  Requires-Dist: rda-python-globus
17
17
  Requires-Dist: unidecode
18
18
  Requires-Dist: hvac
19
+ Provides-Extra: psycopg2
20
+ Requires-Dist: psycopg2; extra == "psycopg2"
21
+ Provides-Extra: psycopg2-binary
22
+ Requires-Dist: psycopg2-binary; extra == "psycopg2-binary"
19
23
  Dynamic: license-file
20
24
 
21
25
  # rda-python-common
22
26
 
23
27
  Python common library codes to be shared by other RDA python utility programs.
24
28
 
25
- ## Installing and using in another RDA python repo
29
+ ## Environment setup
26
30
 
27
- `rda-python-common` is the foundation that every other `rda-python-*` repo
28
- builds on. To consume it from a new or existing repo, follow these steps.
31
+ Create a Python environment first; the install command in the next section
32
+ runs inside whichever environment you activate here.
33
+
34
+ ### Option A — Python venv (DECS machines)
35
+
36
+ ```bash
37
+ python3 -m venv $ENVHOME # e.g. /glade/u/home/gdexdata/gdexmsenv
38
+ source $ENVHOME/bin/activate
39
+ ```
29
40
 
30
- ### 1. Install the package
41
+ ### Option B Conda (DAV/Casper)
42
+
43
+ ```bash
44
+ conda create --prefix $ENVHOME python=3.12 # e.g. /glade/work/gdexdata/conda-envs/pg-gdex
45
+ conda activate $ENVHOME
46
+ ```
47
+
48
+ ## Installing rda-python-common
49
+
50
+ Pick whichever install mode fits your workflow. All four pull in the
51
+ transitive dependencies (`psycopg`, `rda-python-globus`, `unidecode`,
52
+ `hvac`) automatically.
31
53
 
32
54
  For local development, clone this repo alongside your project and install it
33
55
  in editable mode so that changes are picked up without re-installing:
@@ -38,6 +60,15 @@ cd rda-python-common
38
60
  pip install -e .
39
61
  ```
40
62
 
63
+ To test a specific branch (e.g. an in-progress feature or fix branch), pass
64
+ `-b/--branch` to `git clone`:
65
+
66
+ ```bash
67
+ git clone -b <branch-name> https://github.com/NCAR/rda-python-common.git
68
+ cd rda-python-common
69
+ pip install -e .
70
+ ```
71
+
41
72
  For a regular (non-editable) install from a checkout:
42
73
 
43
74
  ```bash
@@ -50,10 +81,75 @@ For a production install on a system that uses the published distribution:
50
81
  pip install rda_python_common
51
82
  ```
52
83
 
53
- The package brings in its own transitive dependencies (`psycopg2-binary`,
54
- `rda-python-globus`, `unidecode`, `hvac`).
84
+ ### PostgreSQL driver: psycopg v3 (default) and psycopg2 (fallback)
85
+
86
+ `rda-python-common` uses **psycopg v3** by default. `pg_dbi.py`
87
+ auto-detects which driver is installed at import time and prefers psycopg v3
88
+ when both are present; no code changes are needed to switch drivers.
89
+
90
+ The required dependency is the base `psycopg` package, which works whether
91
+ psycopg was compiled from source or installed via a binary wheel. If psycopg
92
+ is not available on your system, install whichever driver works:
93
+
94
+ ```bash
95
+ pip install psycopg || pip install psycopg2
96
+ ```
97
+
98
+ To explicitly install the legacy psycopg2 driver:
55
99
 
56
- ### 2. Declare it as a dependency in your project
100
+ ```bash
101
+ pip install "rda_python_common[psycopg2]" # build from source
102
+ pip install "rda_python_common[psycopg2-binary]" # pre-built wheel
103
+ ```
104
+
105
+ ## Configuration: COMMONUSER and ADMINUSER
106
+
107
+ `PGLOG['COMMONUSER']` is the shared common user that setuid-wrapped programs
108
+ execute as (default `gdexdata`), and `PGLOG['ADMINUSER']` is the admin
109
+ specialist user that receives email notifications and is permitted to invoke
110
+ `pgstart_<user>` (default `zji`).
111
+
112
+ Both values are initialized via the `SETPGLOG(key, default)` helper, which
113
+ reads the environment variable `PG<KEY>` and falls back to the supplied
114
+ default when the variable is unset:
115
+
116
+ ```python
117
+ # pg_log.py (class-based)
118
+ self.SETPGLOG("COMMONUSER", "gdexdata") # reads $PGCOMMONUSER
119
+ self.SETPGLOG("ADMINUSER", "zji") # reads $PGADMINUSER
120
+
121
+ # PgLOG.py (module-level) exposes the same helper as a function
122
+ SETPGLOG("COMMONUSER", "gdexdata")
123
+ SETPGLOG("ADMINUSER", "zji")
124
+ ```
125
+
126
+ To override the defaults per environment **once** so the values persist
127
+ across `pip install --upgrade`, set the environment variables:
128
+
129
+ ```bash
130
+ export PGCOMMONUSER=gdexdata # overrides PGLOG['COMMONUSER']
131
+ export PGADMINUSER=zji # overrides PGLOG['ADMINUSER']
132
+ ```
133
+
134
+ Place these `export` lines in `$ENVHOME/bin/activate` (venv), or set them as
135
+ conda environment variables so they are applied whenever the environment is
136
+ activated:
137
+
138
+ ```bash
139
+ conda env config vars set PGCOMMONUSER=gdexdata PGADMINUSER=zji
140
+ conda activate $ENVHOME # reactivate to pick up the values
141
+ ```
142
+
143
+ If the variables are unset, the built-in defaults (`gdexdata` / `zji`) are
144
+ used, preserving existing behavior.
145
+
146
+ ## Using rda-python-common in another RDA python repo
147
+
148
+ `rda-python-common` is the foundation that every other `rda-python-*` repo
149
+ builds on. Once it is installed in the active environment, consuming it from
150
+ a new or existing repo takes three short steps.
151
+
152
+ ### 1. Declare it as a dependency in your project
57
153
 
58
154
  Add `rda_python_common` to the `dependencies` list of your project's
59
155
  `pyproject.toml` so that downstream installs pull it in automatically:
@@ -72,9 +168,10 @@ This is the same pattern used by `rda-python-dsarch`, `rda-python-dsupdt`,
72
168
  `rda-python-dsrqst`, `rda-python-dscheck`, `rda-python-metrics`, and
73
169
  `rda-python-miscs`.
74
170
 
75
- ### 3. Import the modules you need
171
+ ### 2. Import the modules you need
76
172
 
77
- Two import styles are supported (see [Usage examples](#usage-examples) below):
173
+ Two import styles are supported (see [Usage examples](#usage-examples) below
174
+ for fuller patterns):
78
175
 
79
176
  ```python
80
177
  # Preferred for new code -- import the class from the lower-case module
@@ -86,26 +183,26 @@ from rda_python_common import PgLOG, PgDBI
86
183
  PgLOG.pglog("hello", PgLOG.LOGWRN)
87
184
  ```
88
185
 
89
- ### 4. Verify the install
186
+ ### 3. Verify the install
90
187
 
91
188
  ```bash
92
189
  python -c "import rda_python_common; print(rda_python_common.__version__)"
93
190
  ```
94
191
 
95
- You should see the installed version (currently `2.1.10`). If the import
192
+ You should see the installed version (currently `3.0.0`). If the import
96
193
  fails, double-check that the active Python environment is the one where you
97
194
  ran `pip install`.
98
195
 
99
196
  ## Modules
100
197
 
101
- All shared functionality lives under `src/rda_python_common/` and is organised as
102
- a single-inheritance class hierarchy. Each module defines exactly one class;
103
- later classes extend earlier ones, so an application that instantiates the
104
- top-of-chain class (typically `PgOPT` or `PgCMD`) gets every helper through one
105
- object.
198
+ All shared functionality lives under `src/rda_python_common/` and is organised
199
+ as a (mostly) single-inheritance class hierarchy. Each module defines exactly
200
+ one class; later classes extend earlier ones, so an application that
201
+ instantiates the top-of-chain class (typically `PgOPT` or `PgCMD`) gets every
202
+ helper through one object.
106
203
 
107
- Inheritance tree (top-down; multi-inheritance shown as two arrows
108
- converging on the same child):
204
+ The inheritance tree below is read top-down; the two multi-inheritance joins
205
+ are shown as two arrows converging on the same child:
109
206
 
110
207
  ```
111
208
  PgLOG
@@ -142,6 +239,8 @@ The tree is single inheritance everywhere except at two join points:
142
239
  operations (`PgDBI`) it needs to keep the shared `wfile` table and the
143
240
  per-dataset `wfile_<dsid>` partitions in sync.
144
241
 
242
+ Each class lives in its own module. Walking the tree from the root:
243
+
145
244
  - **`pg_log.py`** — `PgLOG`. Root of the hierarchy. Provides the central
146
245
  logging facility (bit-mask `logact` flags such as `MSGLOG`, `WARNLG`,
147
246
  `ERRLOG`, `EXITLG`), e-mail dispatch, system-command execution, process
@@ -165,7 +264,8 @@ The tree is single inheritance everywhere except at two join points:
165
264
  long-running batch jobs coordinate cleanly.
166
265
 
167
266
  - **`pg_dbi.py`** — `PgDBI(PgLOG)`. PostgreSQL database interface built on
168
- `psycopg2`. Wraps connection management, batch `INSERT`/`SELECT`/
267
+ `psycopg` (v3 by default, with `psycopg2` as an opt-in fallback). Wraps
268
+ connection management, batch `INSERT`/`SELECT`/
169
269
  `UPDATE`/`DELETE`, transaction control, and credential lookup from
170
270
  `.pgpass` or OpenBao. All RDA tools talk to the `rdadb` database through
171
271
  this class.
@@ -199,9 +299,9 @@ The tree is single inheritance everywhere except at two join points:
199
299
 
200
300
  ## Usage examples
201
301
 
202
- Each class lives in its own submodule. Import the class you need, then
203
- either instantiate it directly or subclass it to add application-specific
204
- state and methods.
302
+ The patterns below show the typical ways the classes above are used in
303
+ practice. Import the class you need, then either instantiate it directly or
304
+ subclass it to add application-specific state and methods.
205
305
 
206
306
  ### 1. Direct instantiation — use the helpers as-is
207
307
 
@@ -2,12 +2,30 @@
2
2
 
3
3
  Python common library codes to be shared by other RDA python utility programs.
4
4
 
5
- ## Installing and using in another RDA python repo
5
+ ## Environment setup
6
6
 
7
- `rda-python-common` is the foundation that every other `rda-python-*` repo
8
- builds on. To consume it from a new or existing repo, follow these steps.
7
+ Create a Python environment first; the install command in the next section
8
+ runs inside whichever environment you activate here.
9
+
10
+ ### Option A — Python venv (DECS machines)
11
+
12
+ ```bash
13
+ python3 -m venv $ENVHOME # e.g. /glade/u/home/gdexdata/gdexmsenv
14
+ source $ENVHOME/bin/activate
15
+ ```
16
+
17
+ ### Option B — Conda (DAV/Casper)
9
18
 
10
- ### 1. Install the package
19
+ ```bash
20
+ conda create --prefix $ENVHOME python=3.12 # e.g. /glade/work/gdexdata/conda-envs/pg-gdex
21
+ conda activate $ENVHOME
22
+ ```
23
+
24
+ ## Installing rda-python-common
25
+
26
+ Pick whichever install mode fits your workflow. All four pull in the
27
+ transitive dependencies (`psycopg`, `rda-python-globus`, `unidecode`,
28
+ `hvac`) automatically.
11
29
 
12
30
  For local development, clone this repo alongside your project and install it
13
31
  in editable mode so that changes are picked up without re-installing:
@@ -18,6 +36,15 @@ cd rda-python-common
18
36
  pip install -e .
19
37
  ```
20
38
 
39
+ To test a specific branch (e.g. an in-progress feature or fix branch), pass
40
+ `-b/--branch` to `git clone`:
41
+
42
+ ```bash
43
+ git clone -b <branch-name> https://github.com/NCAR/rda-python-common.git
44
+ cd rda-python-common
45
+ pip install -e .
46
+ ```
47
+
21
48
  For a regular (non-editable) install from a checkout:
22
49
 
23
50
  ```bash
@@ -30,10 +57,75 @@ For a production install on a system that uses the published distribution:
30
57
  pip install rda_python_common
31
58
  ```
32
59
 
33
- The package brings in its own transitive dependencies (`psycopg2-binary`,
34
- `rda-python-globus`, `unidecode`, `hvac`).
60
+ ### PostgreSQL driver: psycopg v3 (default) and psycopg2 (fallback)
61
+
62
+ `rda-python-common` uses **psycopg v3** by default. `pg_dbi.py`
63
+ auto-detects which driver is installed at import time and prefers psycopg v3
64
+ when both are present; no code changes are needed to switch drivers.
35
65
 
36
- ### 2. Declare it as a dependency in your project
66
+ The required dependency is the base `psycopg` package, which works whether
67
+ psycopg was compiled from source or installed via a binary wheel. If psycopg
68
+ is not available on your system, install whichever driver works:
69
+
70
+ ```bash
71
+ pip install psycopg || pip install psycopg2
72
+ ```
73
+
74
+ To explicitly install the legacy psycopg2 driver:
75
+
76
+ ```bash
77
+ pip install "rda_python_common[psycopg2]" # build from source
78
+ pip install "rda_python_common[psycopg2-binary]" # pre-built wheel
79
+ ```
80
+
81
+ ## Configuration: COMMONUSER and ADMINUSER
82
+
83
+ `PGLOG['COMMONUSER']` is the shared common user that setuid-wrapped programs
84
+ execute as (default `gdexdata`), and `PGLOG['ADMINUSER']` is the admin
85
+ specialist user that receives email notifications and is permitted to invoke
86
+ `pgstart_<user>` (default `zji`).
87
+
88
+ Both values are initialized via the `SETPGLOG(key, default)` helper, which
89
+ reads the environment variable `PG<KEY>` and falls back to the supplied
90
+ default when the variable is unset:
91
+
92
+ ```python
93
+ # pg_log.py (class-based)
94
+ self.SETPGLOG("COMMONUSER", "gdexdata") # reads $PGCOMMONUSER
95
+ self.SETPGLOG("ADMINUSER", "zji") # reads $PGADMINUSER
96
+
97
+ # PgLOG.py (module-level) exposes the same helper as a function
98
+ SETPGLOG("COMMONUSER", "gdexdata")
99
+ SETPGLOG("ADMINUSER", "zji")
100
+ ```
101
+
102
+ To override the defaults per environment **once** so the values persist
103
+ across `pip install --upgrade`, set the environment variables:
104
+
105
+ ```bash
106
+ export PGCOMMONUSER=gdexdata # overrides PGLOG['COMMONUSER']
107
+ export PGADMINUSER=zji # overrides PGLOG['ADMINUSER']
108
+ ```
109
+
110
+ Place these `export` lines in `$ENVHOME/bin/activate` (venv), or set them as
111
+ conda environment variables so they are applied whenever the environment is
112
+ activated:
113
+
114
+ ```bash
115
+ conda env config vars set PGCOMMONUSER=gdexdata PGADMINUSER=zji
116
+ conda activate $ENVHOME # reactivate to pick up the values
117
+ ```
118
+
119
+ If the variables are unset, the built-in defaults (`gdexdata` / `zji`) are
120
+ used, preserving existing behavior.
121
+
122
+ ## Using rda-python-common in another RDA python repo
123
+
124
+ `rda-python-common` is the foundation that every other `rda-python-*` repo
125
+ builds on. Once it is installed in the active environment, consuming it from
126
+ a new or existing repo takes three short steps.
127
+
128
+ ### 1. Declare it as a dependency in your project
37
129
 
38
130
  Add `rda_python_common` to the `dependencies` list of your project's
39
131
  `pyproject.toml` so that downstream installs pull it in automatically:
@@ -52,9 +144,10 @@ This is the same pattern used by `rda-python-dsarch`, `rda-python-dsupdt`,
52
144
  `rda-python-dsrqst`, `rda-python-dscheck`, `rda-python-metrics`, and
53
145
  `rda-python-miscs`.
54
146
 
55
- ### 3. Import the modules you need
147
+ ### 2. Import the modules you need
56
148
 
57
- Two import styles are supported (see [Usage examples](#usage-examples) below):
149
+ Two import styles are supported (see [Usage examples](#usage-examples) below
150
+ for fuller patterns):
58
151
 
59
152
  ```python
60
153
  # Preferred for new code -- import the class from the lower-case module
@@ -66,26 +159,26 @@ from rda_python_common import PgLOG, PgDBI
66
159
  PgLOG.pglog("hello", PgLOG.LOGWRN)
67
160
  ```
68
161
 
69
- ### 4. Verify the install
162
+ ### 3. Verify the install
70
163
 
71
164
  ```bash
72
165
  python -c "import rda_python_common; print(rda_python_common.__version__)"
73
166
  ```
74
167
 
75
- You should see the installed version (currently `2.1.10`). If the import
168
+ You should see the installed version (currently `3.0.0`). If the import
76
169
  fails, double-check that the active Python environment is the one where you
77
170
  ran `pip install`.
78
171
 
79
172
  ## Modules
80
173
 
81
- All shared functionality lives under `src/rda_python_common/` and is organised as
82
- a single-inheritance class hierarchy. Each module defines exactly one class;
83
- later classes extend earlier ones, so an application that instantiates the
84
- top-of-chain class (typically `PgOPT` or `PgCMD`) gets every helper through one
85
- object.
174
+ All shared functionality lives under `src/rda_python_common/` and is organised
175
+ as a (mostly) single-inheritance class hierarchy. Each module defines exactly
176
+ one class; later classes extend earlier ones, so an application that
177
+ instantiates the top-of-chain class (typically `PgOPT` or `PgCMD`) gets every
178
+ helper through one object.
86
179
 
87
- Inheritance tree (top-down; multi-inheritance shown as two arrows
88
- converging on the same child):
180
+ The inheritance tree below is read top-down; the two multi-inheritance joins
181
+ are shown as two arrows converging on the same child:
89
182
 
90
183
  ```
91
184
  PgLOG
@@ -122,6 +215,8 @@ The tree is single inheritance everywhere except at two join points:
122
215
  operations (`PgDBI`) it needs to keep the shared `wfile` table and the
123
216
  per-dataset `wfile_<dsid>` partitions in sync.
124
217
 
218
+ Each class lives in its own module. Walking the tree from the root:
219
+
125
220
  - **`pg_log.py`** — `PgLOG`. Root of the hierarchy. Provides the central
126
221
  logging facility (bit-mask `logact` flags such as `MSGLOG`, `WARNLG`,
127
222
  `ERRLOG`, `EXITLG`), e-mail dispatch, system-command execution, process
@@ -145,7 +240,8 @@ The tree is single inheritance everywhere except at two join points:
145
240
  long-running batch jobs coordinate cleanly.
146
241
 
147
242
  - **`pg_dbi.py`** — `PgDBI(PgLOG)`. PostgreSQL database interface built on
148
- `psycopg2`. Wraps connection management, batch `INSERT`/`SELECT`/
243
+ `psycopg` (v3 by default, with `psycopg2` as an opt-in fallback). Wraps
244
+ connection management, batch `INSERT`/`SELECT`/
149
245
  `UPDATE`/`DELETE`, transaction control, and credential lookup from
150
246
  `.pgpass` or OpenBao. All RDA tools talk to the `rdadb` database through
151
247
  this class.
@@ -179,9 +275,9 @@ The tree is single inheritance everywhere except at two join points:
179
275
 
180
276
  ## Usage examples
181
277
 
182
- Each class lives in its own submodule. Import the class you need, then
183
- either instantiate it directly or subclass it to add application-specific
184
- state and methods.
278
+ The patterns below show the typical ways the classes above are used in
279
+ practice. Import the class you need, then either instantiate it directly or
280
+ subclass it to add application-specific state and methods.
185
281
 
186
282
  ### 1. Direct instantiation — use the helpers as-is
187
283
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rda_python_common"
7
- version = "2.1.10"
7
+ version = "3.0.0"
8
8
  authors = [
9
9
  { name="Zaihua Ji", email="zji@ucar.edu" },
10
10
  ]
@@ -18,13 +18,20 @@ classifiers = [
18
18
  "Development Status :: 5 - Production/Stable",
19
19
  ]
20
20
  dependencies = [
21
- "hvac",
22
- "psycopg2==2.9.10",
21
+ "psycopg",
22
+ "psutil",
23
23
  "rda-python-globus",
24
24
  "unidecode",
25
25
  "hvac"
26
26
  ]
27
27
 
28
+ [project.optional-dependencies]
29
+ # Allow opting in to the legacy psycopg2 driver instead of psycopg (v3).
30
+ # pg_dbi.py auto-detects which driver is installed and prefers psycopg (v3)
31
+ # when both are available.
32
+ psycopg2 = ["psycopg2"] # psycopg2 built from source
33
+ psycopg2-binary = ["psycopg2-binary"] # psycopg2 pre-built C extension
34
+
28
35
  [project.urls]
29
36
  "Homepage" = "https://github.com/NCAR/rda-python-common"
30
37
 
@@ -17,12 +17,53 @@ import re
17
17
  import time
18
18
  import hvac
19
19
  from datetime import datetime
20
- import psycopg2 as PgSQL
21
- from psycopg2.extras import execute_values
22
- from psycopg2.extras import execute_batch
23
20
  from os import path as op
24
21
  from . import PgLOG
25
22
 
23
+ # Prefer psycopg (v3); fall back to psycopg2 if v3 is not installed.
24
+ try:
25
+ import psycopg as PgSQL
26
+ PG_DRIVER = 'psycopg3'
27
+
28
+ def execute_values(cursor, sql, argslist, page_size=100):
29
+ """Compatibility shim for psycopg2.extras.execute_values on psycopg3.
30
+
31
+ Rewrites ``VALUES %s`` placeholder to ``VALUES (%s, %s, ...)`` based on
32
+ the column count inferred from the first row, then dispatches to
33
+ psycopg3's ``executemany`` (which already batches efficiently).
34
+ """
35
+ if not argslist: return
36
+ ncol = len(argslist[0])
37
+ row_ph = '(' + ','.join(['%s'] * ncol) + ')'
38
+ new_sql = re.sub(r'(?i)\bVALUES\s+%s\b', 'VALUES ' + row_ph, sql, count=1)
39
+ cursor.executemany(new_sql, argslist)
40
+
41
+ def execute_batch(cursor, sql, argslist, page_size=100):
42
+ """Compatibility shim for psycopg2.extras.execute_batch on psycopg3."""
43
+ cursor.executemany(sql, argslist)
44
+
45
+ def get_pgcode(pgerr):
46
+ """Return SQLSTATE for a psycopg3 error (via err.diag.sqlstate)."""
47
+ diag = getattr(pgerr, 'diag', None)
48
+ return getattr(diag, 'sqlstate', None) if diag is not None else None
49
+
50
+ def get_pgerror(pgerr):
51
+ """Return primary error message for a psycopg3 error (via err.diag.message_primary)."""
52
+ diag = getattr(pgerr, 'diag', None)
53
+ return getattr(diag, 'message_primary', None) if diag is not None else None
54
+ except ImportError:
55
+ import psycopg2 as PgSQL
56
+ from psycopg2.extras import execute_values, execute_batch
57
+ PG_DRIVER = 'psycopg2'
58
+
59
+ def get_pgcode(pgerr):
60
+ """Return SQLSTATE for a psycopg2 error (via err.pgcode)."""
61
+ return getattr(pgerr, 'pgcode', None)
62
+
63
+ def get_pgerror(pgerr):
64
+ """Return primary error message for a psycopg2 error (via err.pgerror)."""
65
+ return getattr(pgerr, 'pgerror', None)
66
+
26
67
  pgdb = None # reference to a connected database object
27
68
  curtran = 0 # 0 - no transaction, 1 - in transaction
28
69
  NMISSES = [] # array of mising userno
@@ -439,8 +480,8 @@ def check_dberror(pgerr, pgcnt, sqlstr, ary, logact = PGDBI['ERRLOG']):
439
480
 
440
481
  ret = PgLOG.FAILURE
441
482
 
442
- pgcode = pgerr.pgcode
443
- pgerror = pgerr.pgerror
483
+ pgcode = get_pgcode(pgerr)
484
+ pgerror = get_pgerror(pgerr)
444
485
  dberror = "{} {}".format(pgcode, pgerror) if pgcode and pgerror else str(pgerr)
445
486
  if pgcnt < PgLOG.PGLOG['DBRETRY']:
446
487
  if not pgcode:
@@ -517,7 +558,7 @@ def pgconnect(reconnect = 0, pgcnt = 0, autocommit = True):
517
558
  reconnect = 0 # initial connection
518
559
 
519
560
  while True:
520
- config = {'database' : PGDBI['DBNAME'],
561
+ config = {'dbname' : PGDBI['DBNAME'],
521
562
  'user' : PGDBI['LNNAME']}
522
563
  if PGDBI['DBSHOST'] == PgLOG.PGLOG['HOSTNAME']:
523
564
  config['host'] = 'localhost'
@@ -526,7 +567,7 @@ def pgconnect(reconnect = 0, pgcnt = 0, autocommit = True):
526
567
  if not PGDBI['DBPORT']: PGDBI['DBPORT'] = get_dbport(PGDBI['DBNAME'])
527
568
  if PGDBI['DBPORT']: config['port'] = PGDBI['DBPORT']
528
569
  config['password'] = '***'
529
- sqlstr = "psycopg2.connect(**{})".format(config)
570
+ sqlstr = "{}.connect(**{})".format(PG_DRIVER, config)
530
571
  config['password'] = get_pgpass_password()
531
572
  if PgLOG.PGLOG['DBGLEVEL']: PgLOG.pgdbg(1000, sqlstr)
532
573
  try:
@@ -788,7 +788,7 @@ def delete_backup_file(file, endpoint = None, logact = 0):
788
788
  return PgLOG.FAILURE
789
789
 
790
790
  #
791
- # reset local file/directory information to make them writable for PgLOG.PGLOG['GDEXUSER']
791
+ # reset local file/directory information to make them writable for PgLOG.PGLOG['COMMONUSER']
792
792
  # file - file name (mandatory)
793
793
  # info - gathered file info with option 14, None means file not exists
794
794
  #