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.
- {rda_python_common-2.1.10/src/rda_python_common.egg-info → rda_python_common-3.0.0}/PKG-INFO +125 -25
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/README.md +118 -22
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/pyproject.toml +10 -3
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgDBI.py +48 -7
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgFile.py +1 -1
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgLOG.py +37 -22
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgOPT.py +2 -2
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgSIG.py +2 -2
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/__init__.py +1 -1
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_dbi.py +77 -21
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_file.py +19 -10
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_log.py +54 -33
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_opt.py +2 -2
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_sig.py +96 -81
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_util.py +36 -89
- {rda_python_common-2.1.10 → rda_python_common-3.0.0/src/rda_python_common.egg-info}/PKG-INFO +125 -25
- rda_python_common-3.0.0/src/rda_python_common.egg-info/requires.txt +11 -0
- rda_python_common-2.1.10/src/rda_python_common.egg-info/requires.txt +0 -5
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/LICENSE +0 -0
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/setup.cfg +0 -0
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgCMD.py +0 -0
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgLock.py +0 -0
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgSplit.py +0 -0
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/PgUtil.py +0 -0
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_cmd.py +0 -0
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_lock.py +0 -0
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_password.py +0 -0
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pg_split.py +0 -0
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pgpassword.py +0 -0
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common/pgpassword.usg +0 -0
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common.egg-info/SOURCES.txt +0 -0
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common.egg-info/dependency_links.txt +0 -0
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common.egg-info/entry_points.txt +0 -0
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/src/rda_python_common.egg-info/top_level.txt +0 -0
- {rda_python_common-2.1.10 → rda_python_common-3.0.0}/test/test_common.py +0 -0
{rda_python_common-2.1.10/src/rda_python_common.egg-info → rda_python_common-3.0.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rda_python_common
|
|
3
|
-
Version:
|
|
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:
|
|
15
|
-
Requires-Dist:
|
|
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
|
-
##
|
|
29
|
+
## Environment setup
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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 `
|
|
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
|
|
102
|
-
a single-inheritance class hierarchy. Each module defines exactly
|
|
103
|
-
later classes extend earlier ones, so an application that
|
|
104
|
-
top-of-chain class (typically `PgOPT` or `PgCMD`) gets every
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
203
|
-
either instantiate it directly or
|
|
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
|
-
##
|
|
5
|
+
## Environment setup
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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 `
|
|
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
|
|
82
|
-
a single-inheritance class hierarchy. Each module defines exactly
|
|
83
|
-
later classes extend earlier ones, so an application that
|
|
84
|
-
top-of-chain class (typically `PgOPT` or `PgCMD`) gets every
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
183
|
-
either instantiate it directly or
|
|
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 = "
|
|
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
|
-
"
|
|
22
|
-
"
|
|
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
|
|
443
|
-
pgerror = pgerr
|
|
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 = {'
|
|
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 = "
|
|
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['
|
|
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
|
#
|