ping-dataexport 0.2.2__tar.gz → 0.3.5__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 (51) hide show
  1. {ping_dataexport-0.2.2/src/ping_dataexport.egg-info → ping_dataexport-0.3.5}/PKG-INFO +414 -399
  2. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/README.md +34 -19
  3. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/pyproject.toml +1 -1
  4. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/setup.cfg +4 -4
  5. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/__init__.py +1 -1
  6. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/cli.py +4 -3
  7. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/job/validator.py +3 -3
  8. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5/src/ping_dataexport.egg-info}/PKG-INFO +414 -399
  9. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/LICENSE +0 -0
  10. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/MANIFEST.in +0 -0
  11. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/config.sample.ini +0 -0
  12. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/job.sample.ini +0 -0
  13. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/__config__.py +0 -0
  14. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/__main__.py +0 -0
  15. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/api.py +0 -0
  16. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/applogger.py +0 -0
  17. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/config.py +0 -0
  18. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/db/__init__.py +0 -0
  19. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/db/connector.py +0 -0
  20. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/db/oracle_connector.py +0 -0
  21. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/db/pyodbc_connector.py +0 -0
  22. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/db/sqlite_connector.py +0 -0
  23. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/db/type_map.py +0 -0
  24. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/errors.py +0 -0
  25. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/export/__init__.py +0 -0
  26. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/export/engine.py +0 -0
  27. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/export/options.py +0 -0
  28. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/export/runner.py +0 -0
  29. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/export/util.py +0 -0
  30. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/job/__init__.py +0 -0
  31. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/job/executor.py +0 -0
  32. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/job/shared_context.py +0 -0
  33. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/job/spec.py +0 -0
  34. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/paths.py +0 -0
  35. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/planner/__init__.py +0 -0
  36. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/planner/base.py +0 -0
  37. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/planner/datelist.py +0 -0
  38. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/planner/groupby.py +0 -0
  39. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/planner/monthbydate.py +0 -0
  40. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/planner/single.py +0 -0
  41. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/planner/watermark.py +0 -0
  42. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/writer/__init__.py +0 -0
  43. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/writer/arrow_map.py +0 -0
  44. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/writer/base.py +0 -0
  45. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/writer/delimited.py +0 -0
  46. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport/writer/parquet.py +0 -0
  47. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport.egg-info/SOURCES.txt +0 -0
  48. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport.egg-info/dependency_links.txt +0 -0
  49. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport.egg-info/entry_points.txt +0 -0
  50. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport.egg-info/requires.txt +0 -0
  51. {ping_dataexport-0.2.2 → ping_dataexport-0.3.5}/src/ping_dataexport.egg-info/top_level.txt +0 -0
@@ -1,399 +1,414 @@
1
- Metadata-Version: 2.4
2
- Name: ping-dataexport
3
- Version: 0.2.2
4
- Summary: PING's Data Export Tool - To Parquet/CSV/TEXT as Accelerator for Data & AI Project
5
- Author: vorapol
6
- License-Expression: Apache-2.0
7
- Keywords: export,database,csv,parquet,etl,data-platform,airflow
8
- Classifier: Development Status :: 4 - Beta
9
- Classifier: Environment :: Console
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Operating System :: OS Independent
12
- Classifier: Programming Language :: Python :: 3
13
- Classifier: Topic :: Database
14
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
- Requires-Python: >=3.9
16
- Description-Content-Type: text/markdown
17
- License-File: LICENSE
18
- Requires-Dist: pyarrow>=18
19
- Provides-Extra: odbc
20
- Requires-Dist: pyodbc>=5; extra == "odbc"
21
- Provides-Extra: oracle
22
- Requires-Dist: oracledb>=2; extra == "oracle"
23
- Provides-Extra: drivers
24
- Requires-Dist: pyodbc>=5; extra == "drivers"
25
- Requires-Dist: oracledb>=2; extra == "drivers"
26
- Provides-Extra: dev
27
- Requires-Dist: pytest>=8; extra == "dev"
28
- Dynamic: license-file
29
-
30
- # ping-dataexport
31
-
32
- ## PING (ping-godhand)'s Universal Data Exporter
33
-
34
- > **Provided as-is. No support, no warranty, no roadmap commitments, use at
35
- > your own risk.**
36
-
37
- ## คำอธิบาย (ภาษาไทย)
38
-
39
- โปรแกรมช่วย Export ข้อมูลอย่างง่าย: เชื่อมต่อฐานข้อมูล รันคำสั่ง SQL
40
- แล้ว Export ผลลัพธ์ออกเป็นไฟล์ **CSV / TXT / Parquet** สร้างมาเพื่องาน Data Platform
41
- โดยเฉพาะ ส่งออกข้อมูลจากฐานข้อมูล ให้เป็นไฟล์ที่ Data Lake
42
- หรือ Data Warehouse นำไปใช้ต่อได้ทันที
43
-
44
- - **ความถูกต้องของชนิดข้อมูลมาก่อนเสมอ** ข้อมูลจาก cursor writer
45
- ทีละ Row ด้วย Data Type ดั้งเดิมเสมอเช่น: `DECIMAL(p,s)` ลงใน Parquet เป็น
46
- `decimal128(p,s)`, คอลัมน์ int ที่มี NULL ยังคงเป็น int, วันที่ยังคงเป็นวันที่
47
- และไม่ใช้ pandas เด็ดขาด
48
- - **ฐานข้อมูลที่รองรับ** SQLite (Library มาตรฐาน ไม่ต้องติดตั้งอะไรเพิ่ม),
49
- SQL Server (MS ODBC / FreeTDS), Oracle (โหมด thin แบบเ Native หรือ ODBC)
50
- และแหล่งข้อมูล ODBC ใดก็ได้
51
- - **ใช้งานได้สองทาง** — ผ่าน CLI `ping-dataexport` หรือผ่าน Library API (`ping_dataexport.api`)
52
- ที่ออกแบบมาสำหรับ Airflow
53
-
54
- ## Description (English)
55
-
56
- Universal database data export: connect to a database, run a SQL query, stream
57
- the result to **CSV / TXT / Parquet**. Built for data-platform work — exporting
58
- legacy databases into files a data lake or warehouse can ingest.
59
-
60
- - **Type fidelity first** rows stream cursor → writer with native driver
61
- types. `DECIMAL(p,s)` lands in Parquet as `decimal128(p,s)`, ints with NULLs
62
- stay ints, dates stay dates. No pandas, ever.
63
- - **Databases** SQLite (stdlib, zero setup), SQL Server (MS ODBC / FreeTDS),
64
- Oracle (native thin mode or ODBC), any ODBC source.
65
- - **Two front doors** a CLI and a library API (`ping_dataexport.api`) built
66
- for Airflow: exceptions instead of exit codes, results as objects.
67
-
68
- ## Install
69
-
70
- ```bash
71
- pip install ping-dataexport # base (pyarrow) sqlite works out of the box
72
- pip install ping-dataexport[odbc] # + pyodbc (SQL Server, FreeTDS, ODBC)
73
- pip install ping-dataexport[oracle] # + oracledb (Oracle native)
74
- pip install ping-dataexport[drivers] # + both
75
- ```
76
-
77
- ## 1. Create a config.ini
78
-
79
- One `[Main]` section for defaults, one section per database source:
80
-
81
- ```ini
82
- [Main]
83
- ExportFolder=./output ; relative output/log paths resolve under this
84
- FetchSize=10000
85
- ForceMakeDir=Yes
86
- RowIDColumn=row_id ; optional audit column names
87
- DataDateColumn=data_date ; optional audit data extract date
88
-
89
- [SQLITE01]
90
- Type=sqlite
91
- Database=./demo.sqlite
92
-
93
- [DB01]
94
- Type=mssql-odbc
95
- Host=host
96
- Port=1433
97
- Database=DB
98
- Username=user
99
- Password=secret
100
-
101
- [ORA01]
102
- Type=oracle
103
- Host=orahost
104
- Port=1521
105
- Service=ORCLPDB
106
- Username=scott
107
- Password=tiger
108
- ```
109
-
110
- A full sample ships as `config.sample.ini` in the source distribution.
111
-
112
- ## 2. Config keys — type / possible values
113
-
114
- `[Main]` (all optional; a missing `[Main]` yields the defaults):
115
-
116
- | Key | Type | Possible values / default |
117
- |-----|------|---------------------------|
118
- | `ExportFolder` | path | base folder for relative output/log paths; default: current directory |
119
- | `TempFolder` | path | reserved for a future release |
120
- | `FetchSize` | int > 0 | rows per batch; default `50000` |
121
- | `PreviewRow` | int > 0 | rows shown in console/test preview; default `10` |
122
- | `ForceMakeDir` | bool | `Yes`/`No`, `True`/`False`, `1`/`0`, `on`/`off`; default `Yes` |
123
- | `RowIDColumn` | string | audit column name (running row number); absent = no column |
124
- | `DataDateColumn` | string | audit column name (data extract date); absent = no column |
125
- | `LogFile` | path | default `{ExportFolder}/logs/log_{jobname}.txt` |
126
-
127
- DB section (one per source; section name is what `-s` refers to):
128
-
129
- | Key | Type | Possible values / notes |
130
- |-----|------|--------------------------|
131
- | `Type` | enum | `sqlite` \| `mssql-odbc` \| `freetds` \| `oracle` \| `oracle-odbc` \| `odbc`; omitted with `ConnectionString`/`DSN` present ⇒ `odbc` |
132
- | `Name` | string | display name in logs; default: the section name |
133
- | `ConnectionString` | string | raw ODBC connection string (alternative to Host/Port/Database) |
134
- | `DSN` | string | ODBC data source name |
135
- | `Host` / `Port` | string / int | server address |
136
- | `Database` | string | database name; for `sqlite`: the file path |
137
- | `Service` / `SID` | string | Oracle service name or SID |
138
- | `Username` | string | login user |
139
- | `Password` | string | plain-text password |
140
- | `PasswordBase64` | string | base64-encoded password (obfuscation, not encryption); when both are set, `Password` wins |
141
- | `Encoding` | string | e.g. `utf-8`, `tis-620` |
142
- | `Driver` | string | ODBC driver name override, e.g. `ODBC Driver 18 for SQL Server` |
143
-
144
- ```ini
145
- PasswordBase64=c2VjcmV0 ; base64("secret")
146
- ```
147
-
148
- ## 3. Run an export (CLI)
149
-
150
- `ping-dataexport` and `python -m ping_dataexport` are equivalent. The config
151
- file defaults to `./config.ini` (`-cf` to point elsewhere).
152
-
153
- ```bash
154
- # preview to the console (no -o/-od): prints the first rows
155
- ping-dataexport -s SQLITE01 -q "SELECT * FROM sales"
156
-
157
- # single file format inferred from the extension (.csv/.txt/.parquet)
158
- ping-dataexport -s SQLITE01 -q "SELECT * FROM sales" -o sales.parquet
159
- ping-dataexport -s SQLITE01 -q "SELECT * FROM sales" -o sales.csv -gz # gzip
160
-
161
- # validate + preview only, write nothing
162
- ping-dataexport -s SQLITE01 -q "SELECT * FROM sales" -o sales.csv -t
163
- ```
164
-
165
- All options:
166
-
167
- | Option | Full name | Meaning |
168
- |--------|-----------|---------|
169
- | `-s` | `--source` | DB config section name (e.g. `DB01`) |
170
- | `-q` | `--query` | SQL query |
171
- | `-o` | `--output` | single output file (format from extension, or `-f`) |
172
- | `-od` | `--outputdir` | output directory (required for `-m` modes) |
173
- | `-bn` | `--basename` | base filename for mode exports |
174
- | `-f` | `--format` | `csv`, `txt`, `parquet` |
175
- | `-gz` | `--gzip` | gzip the output (CSV/TXT only) |
176
- | `-sp` | `--sep` | field separator (default `,`; `\t` maps to tab) |
177
- | `-quo` | `--quote` | quote character (default `"`) |
178
- | `-stnl` | `--stripnewline` | newline handling in text columns: `space` \| `blank` \| `escape` \| `doubleescape` |
179
- | `-stmc` | `--stripmetachar` | strip invisible/control characters from text columns |
180
- | `-fs` | `--fetchsize` | fetch size (rows per batch) |
181
- | `-m` | `--mode` | export mode: `groupby` \| `date` \| `relativedate` \| `monthbydate` \| `splitrow` \| `watermark` — see section 4 |
182
- | `-col` | `--column` | column the mode partitions on |
183
- | `-row` | `--row` | rows per file (`-m splitrow`) |
184
- | `-df` | `--datefrom` | start / anchor date `yyyy-MM-dd`; omitted with `-dr`/`-mr` ⇒ today (system date) |
185
- | `-dt` | `--dateto` | end date `yyyy-MM-dd` (`-m date`) |
186
- | `-dr` | `--daysrelative` | days back from `-df` (`-m relativedate`) |
187
- | `-mr` | `--monthsrelative` | months back from `-df` (`-m monthbydate`) |
188
- | `-rm` | `--removeexisting` | `Yes`: clear the output directory before exporting |
189
- | `-j` | `--job` | run one job.ini section — see section 5 |
190
- | `-jf` | `--jobfile` | job file path (default `./job.ini`) |
191
- | `-p` | `--parallel` | run ALL job sections in parallel with N workers (default: CPU count); `-pl` still works as a legacy alias |
192
- | `-n` | `--name` | job name shown in logs and the default log filename |
193
- | `-cf` | `--configfile` | config file path (default `./config.ini`) |
194
- | `-lf` | `--logfile` | log file path |
195
- | `-t` | `--test` | test mode: validate + preview, no files |
196
-
197
- ## 4. Export modes (partitioned outputs)
198
-
199
- Modes split one query into multiple files. All need `-od` (output directory)
200
- + `-bn` (base filename); partition values are always bound SQL parameters.
201
-
202
- **`groupby` one file per distinct value of a column:**
203
-
204
- ```bash
205
- ping-dataexport -s DB01 -q "SELECT * FROM sales" -m groupby -col region -od by_region -bn sales
206
- ```
207
- ```
208
- by_region/
209
- ├── sales_@@NULL@@.csv <- rows where region IS NULL
210
- ├── sales_MY.csv
211
- ├── sales_SG.csv
212
- ├── sales_TH.csv
213
- └── sales_VN.csv
214
- ```
215
-
216
- **`date` — one file per day in a date range (inclusive):**
217
-
218
- ```bash
219
- ping-dataexport -s DB01 -q "SELECT * FROM sales" -m date -col txn_date \
220
- -df 2026-01-01 -dt 2026-01-05 -od daily -bn sales
221
- ```
222
- ```
223
- daily/
224
- ├── sales_2026-01-01.csv
225
- ├── sales_2026-01-02.csv
226
- ├── sales_2026-01-03.csv
227
- ├── sales_2026-01-04.csv
228
- └── sales_2026-01-05.csv
229
- ```
230
-
231
- **`relativedate` — N days back from an anchor date, anchor included
232
- (omit `-df` to anchor on today):**
233
-
234
- ```bash
235
- ping-dataexport -s DB01 -q "SELECT * FROM sales" -m relativedate -col txn_date \
236
- -df 2026-01-10 -dr 3 -od rel -bn sales
237
- ```
238
- ```
239
- rel/
240
- ├── sales_2026-01-07.csv
241
- ├── sales_2026-01-08.csv
242
- ├── sales_2026-01-09.csv
243
- └── sales_2026-01-10.csv
244
- ```
245
-
246
- **`monthbydate` — N months back plus the anchor month (the anchor month stops
247
- at the anchor date; omit `-df` to anchor on today):**
248
-
249
- ```bash
250
- ping-dataexport -s DB01 -q "SELECT * FROM sales" -m monthbydate -col txn_date \
251
- -df 2026-03-15 -mr 3 -od monthly -bn sales
252
- ```
253
- ```
254
- monthly/
255
- ├── sales_202512.csv
256
- ├── sales_202601.csv
257
- ├── sales_202602.csv
258
- └── sales_202603.csv <- 2026-03-01 .. 2026-03-15 only
259
- ```
260
-
261
- **`splitrow` — rotate to a new file every N rows:**
262
-
263
- ```bash
264
- ping-dataexport -s DB01 -q "SELECT * FROM sales ORDER BY id" -m splitrow -row 100000 -od split -bn part
265
- ```
266
- ```
267
- split/
268
- ├── part_1.csv <- 100,000 rows each
269
- ├── part_2.csv
270
- └── part_3.csv <- remainder
271
- ```
272
-
273
- **`watermark` incremental: only rows newer than the last run:**
274
-
275
- ```bash
276
- ping-dataexport -s DB01 -q "SELECT * FROM sales" -m watermark -col txn_date -od incr -bn sales
277
- ```
278
- ```
279
- incr/
280
- ├── sales_20260831_120000.csv <- filename = run timestamp
281
- └── __watermark__.ini <- state file: highest txn_date exported so far
282
- ```
283
-
284
- Run the watermark job again: no new rows → no new file; new rows → one more
285
- timestamped file. Delete `__watermark__.ini` to start over from a full export.
286
-
287
- Every run also prints structured log lines to the console (and to the log
288
- filesee *Output and logs* below):
289
-
290
- ```
291
- 2026-08-31 11:24:41 | JOB_20260831_112441 | INFO | CONFIG | JOB | source=DB01 mode=groupby
292
- 2026-08-31 11:24:41 | JOB_20260831_112441 | INFO | EXPORT | PARTITION | region = 'TH'
293
- 2026-08-31 11:24:41 | JOB_20260831_112441 | INFO | EXPORT | FILE | ./output/by_region/sales_TH.csv
294
- 2026-08-31 11:24:41 | JOB_20260831_112441 | INFO | EXPORT | PROGRESS | 200 rows (finished)
295
- 2026-08-31 11:24:41 | JOB_20260831_112441 | INFO | RESULT | COMPLETED | 0.2 seconds
296
- ```
297
-
298
- ## 5. Job files (job.ini)
299
-
300
- Put recurring exports in a job file so you never retype long commands. Each
301
- `[Section]` is **one job**: run one with `-j SectionName`, or all of them at
302
- once with `-p`. Every key is simply a CLI option written as a config key:
303
-
304
- ```ini
305
- [DailySales]
306
- Source=DB01
307
- Query=SELECT id, txn_date, amount FROM sales
308
- WHERE region = 'TH'
309
- Mode=date
310
- Column=txn_date
311
- DateFrom=2026-01-01
312
- DateTo=2026-01-31
313
- OutputDir=out/daily_sales
314
- BaseName=sales
315
- Format=csv
316
-
317
- [FullDump]
318
- Source=DB01
319
- Query=SELECT * FROM sales
320
- Output=out/full_dump.parquet
321
- ```
322
-
323
- ```bash
324
- ping-dataexport -j DailySales # one section (job.ini by default, -jf elsewhere)
325
- ping-dataexport -j DailySales -od other_dir # CLI options override the section
326
- ping-dataexport -p 4 # ALL sections in parallel, 4 workers
327
- ```
328
-
329
- Every job.ini key and the CLI option it mirrors (keys are case-insensitive):
330
-
331
- | Job key | CLI option | Meaning |
332
- |---------|------------|---------|
333
- | `Source` | `-s` / `--source` | which config.ini DB section to export from |
334
- | `Query` | `-q` / `--query` | the SQL to run; indent continuation lines for multi-line queries |
335
- | `Output` | `-o` / `--output` | single output file path |
336
- | `OutputDir` | `-od` / `--outputdir` | output directory (needed when `Mode=` is set) |
337
- | `BaseName` | `-bn` / `--basename` | base filename for mode exports |
338
- | `Format` | `-f` / `--format` | `csv` / `txt` / `parquet` (else inferred from `Output` extension) |
339
- | `Gzip` | `-gz` / `--gzip` | `Yes`/`No` gzip the output (CSV/TXT only) |
340
- | `Sep` | `-sp` / `--sep` | field separator (default `,`; `\t` = tab) |
341
- | `Quote` | `-quo` / `--quote` | quote character (default `"`) |
342
- | `StripNewLine` | `-stnl` / `--stripnewline` | `space` \| `blank` \| `escape` \| `doubleescape` |
343
- | `StripMetaChar` | `-stmc` / `--stripmetachar` | `Yes`/`No` — strip invisible/control characters |
344
- | `FetchSize` | `-fs` / `--fetchsize` | rows per batch |
345
- | `Mode` | `-m` / `--mode` | `groupby` / `date` / `relativedate` / `monthbydate` / `splitrow` / `watermark` |
346
- | `Column` | `-col` / `--column` | the column the mode partitions on |
347
- | `Row` | `-row` / `--row` | rows per file (`Mode=splitrow`) |
348
- | `DateFrom` | `-df` / `--datefrom` | start / anchor date, `yyyy-MM-dd` |
349
- | `DateTo` | `-dt` / `--dateto` | end date, `yyyy-MM-dd` (`Mode=date`) |
350
- | `DaysRelative` | `-dr` / `--daysrelative` | days back from `DateFrom` (`Mode=relativedate`) |
351
- | `MonthsRelative` | `-mr` / `--monthsrelative` | months back from `DateFrom` (`Mode=monthbydate`) |
352
- | `RemoveExisting` | `-rm` / `--removeexisting` | `Yes`: clear `OutputDir` before exporting |
353
- | `Name` | `-n` / `--name` | job name shown in logs; default: the section name |
354
-
355
- A full sample ships as `job.sample.ini` in the source distribution.
356
-
357
- ## 6. Use as a library (Python / Airflow)
358
-
359
- ```python
360
- from ping_dataexport.api import run_export, run_job, run_jobfile
361
-
362
- # ad-hoc any CLI option works as a keyword argument
363
- result = run_export("DB01", "SELECT * FROM sales",
364
- configfile="config.ini", output="sales.parquet")
365
- print(result.status, result.rows, result.files)
366
-
367
- # one job.ini section / every section in parallel
368
- run_job("DailySales", jobfile="job.ini", configfile="config.ini")
369
- results = run_jobfile(jobfile="job.ini", configfile="config.ini", workers=4)
370
- ```
371
-
372
- Failures raise exceptions (`ConfigError`, `JobValidationError`, `ExportError`)
373
- — never `sys.exit` — so an Airflow task fails cleanly. `run_jobfile` is the
374
- exception: one job's failure never stops the others; check each
375
- `ExportResult.status`.
376
-
377
- The package also works with **no pip install at all**: copy the
378
- `ping_dataexport/` folder next to your code and import it — relative imports
379
- only, no metadata lookups.
380
-
381
- ## Output and logs
382
-
383
- - Relative output paths resolve under `ExportFolder`; `ForceMakeDir=Yes`
384
- creates missing directories.
385
- - Every run logs structured lines to stdout and to a log file:
386
- `-lf path` > `LogFile=` in config.ini > default
387
- `{ExportFolder}/logs/log_{jobname}.txt`.
388
- - CLI exit codes: `0` success, `1` failure (in `-p` parallel mode: `1` if any
389
- job failed).
390
-
391
- ## ODBC prerequisites (ODBC sources only)
392
-
393
- The Python drivers install via pip, but ODBC drivers are OS-level installs:
394
- Microsoft ODBC Driver 17/18 for SQL Server, FreeTDS (`apt install tdsodbc`), or
395
- your Oracle ODBC driver. `Type=sqlite` and `Type=oracle` (thin mode) need none.
396
-
397
- ## License
398
-
399
- Apache-2.0.
1
+ Metadata-Version: 2.4
2
+ Name: ping-dataexport
3
+ Version: 0.3.5
4
+ Summary: PING's Data Export Tool - To Parquet/CSV/TEXT as Accelerator for Data & AI Project
5
+ Author: vorapol
6
+ License-Expression: Apache-2.0
7
+ Keywords: export,database,csv,parquet,etl,data-platform,airflow
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Database
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: pyarrow>=18
19
+ Provides-Extra: odbc
20
+ Requires-Dist: pyodbc>=5; extra == "odbc"
21
+ Provides-Extra: oracle
22
+ Requires-Dist: oracledb>=2; extra == "oracle"
23
+ Provides-Extra: drivers
24
+ Requires-Dist: pyodbc>=5; extra == "drivers"
25
+ Requires-Dist: oracledb>=2; extra == "drivers"
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # ping-dataexport
31
+
32
+ ## PING (ping-godhand)'s Universal Data Exporter
33
+
34
+ > **Provided as-is. No support, no warranty, no roadmap commitments, use at your own risk.**
35
+
36
+ ## คำอธิบาย (ภาษาไทย)
37
+
38
+ โปรแกรมช่วย Export ข้อมูลอย่างง่าย: เชื่อมต่อฐานข้อมูล รันคำสั่ง SQL
39
+ แล้ว Export ผลลัพธ์ออกเป็นไฟล์ **CSV / TXT / Parquet** สร้างมาเพื่องาน Data Platform
40
+ โดยเฉพาะ ส่งออกข้อมูลจากฐานข้อมูล ให้เป็นไฟล์ที่ Data Lake
41
+ หรือ Data Warehouse นำไปใช้ต่อได้ทันที
42
+
43
+ - **ความถูกต้องของชนิดข้อมูลมาก่อนเสมอ** — ข้อมูลจาก cursor → writer
44
+ ทีละ Row ด้วย Data Type ดั้งเดิมเสมอเช่น: `DECIMAL(p,s)` ลงใน Parquet เป็น
45
+ `decimal128(p,s)`, คอลัมน์ int ที่มี NULL ยังคงเป็น int, วันที่ยังคงเป็นวันที่
46
+ และไม่ใช้ pandas เด็ดขาด
47
+ - **ฐานข้อมูลที่รองรับ** — SQLite (Library มาตรฐาน ไม่ต้องติดตั้งอะไรเพิ่ม),
48
+ SQL Server (MS ODBC / FreeTDS), Oracle (โหมด thin แบบ Native หรือ ODBC)
49
+ และแหล่งข้อมูล ODBC ใดก็ได้
50
+ - **ใช้งานได้สองทาง** — ผ่าน CLI `ping-dataexport` หรือผ่าน Library API (`ping_dataexport.api`)
51
+ ที่ออกแบบมาสำหรับ Airflow
52
+
53
+ ## Description (English)
54
+
55
+ Universal database data export: connect to a database, run a SQL query, stream
56
+ the result to **CSV / TXT / Parquet**. Built for data-platform work — exporting
57
+ legacy databases into files a data lake or warehouse can ingest.
58
+
59
+ - **Type fidelity first** — rows stream cursor → writer with native driver
60
+ types. `DECIMAL(p,s)` lands in Parquet as `decimal128(p,s)`, ints with NULLs
61
+ stay ints, dates stay dates. No pandas, ever.
62
+ - **Databases** — SQLite (stdlib, zero setup), SQL Server (MS ODBC / FreeTDS),
63
+ Oracle (native thin mode or ODBC), any ODBC source.
64
+ - **Two front doors** a CLI and a library API (`ping_dataexport.api`) built
65
+ for Airflow: exceptions instead of exit codes, results as objects.
66
+
67
+ ## Install
68
+
69
+ ```bash
70
+ pip install ping-dataexport # base (pyarrow) — sqlite works out of the box
71
+ pip install ping-dataexport[odbc] # + pyodbc (SQL Server, FreeTDS, ODBC)
72
+ pip install ping-dataexport[oracle] # + oracledb (Oracle native)
73
+ pip install ping-dataexport[drivers] # + both
74
+ ```
75
+
76
+ ## 1. Create a config.ini
77
+
78
+ One `[Main]` section for defaults, one section per database source:
79
+
80
+ ```ini
81
+ [Main]
82
+ ExportFolder=./output ; relative output/log paths resolve under this
83
+ FetchSize=10000
84
+ ForceMakeDir=Yes
85
+ RowIDColumn=row_id ; optional audit column names
86
+ DataDateColumn=data_date ; optional audit data extract date
87
+
88
+ [SQLITE01]
89
+ Type=sqlite
90
+ Database=./demo.sqlite
91
+
92
+ [DB01]
93
+ Type=mssql-odbc
94
+ Host=host
95
+ Port=1433
96
+ Database=DB
97
+ Username=user
98
+ Password=secret
99
+
100
+ [ORA01]
101
+ Type=oracle
102
+ Host=orahost
103
+ Port=1521
104
+ Service=ORCLPDB
105
+ Username=scott
106
+ Password=tiger
107
+ ```
108
+
109
+ A full sample ships as `config.sample.ini` in the source distribution.
110
+
111
+ ## 2. Config keys — type / possible values
112
+
113
+ `[Main]` (all optional; a missing `[Main]` yields the defaults):
114
+
115
+ | Key | Type | Possible values / default |
116
+ |-----|------|---------------------------|
117
+ | `ExportFolder` | path | base folder for relative output/log paths; default: current directory |
118
+ | `TempFolder` | path | reserved for a future release |
119
+ | `FetchSize` | int > 0 | rows per batch; default `50000` |
120
+ | `PreviewRow` | int > 0 | rows shown in console/test preview; default `10` |
121
+ | `ForceMakeDir` | bool | `Yes`/`No`, `True`/`False`, `1`/`0`, `on`/`off`; default `Yes` |
122
+ | `RowIDColumn` | string | audit column name (running row number); absent = no column |
123
+ | `DataDateColumn` | string | audit column name (data extract date); absent = no column |
124
+ | `LogFile` | path | default `{ExportFolder}/logs/log_{jobname}.txt` |
125
+
126
+ DB section (one per source; section name is what `-s` refers to):
127
+
128
+ | Key | Type | Possible values / notes |
129
+ |-----|------|--------------------------|
130
+ | `Type` | enum | `sqlite` \| `mssql-odbc` \| `freetds` \| `oracle` \| `oracle-odbc` \| `odbc`; omitted with `ConnectionString`/`DSN` present ⇒ `odbc` |
131
+ | `Name` | string | display name in logs; default: the section name |
132
+ | `ConnectionString` | string | raw ODBC connection string (alternative to Host/Port/Database) |
133
+ | `DSN` | string | ODBC data source name |
134
+ | `Host` / `Port` | string / int | server address |
135
+ | `Database` | string | database name; for `sqlite`: the file path |
136
+ | `Service` / `SID` | string | Oracle service name or SID |
137
+ | `Username` | string | login user |
138
+ | `Password` | string | plain-text password |
139
+ | `PasswordBase64` | string | base64-encoded password (obfuscation, not encryption); when both are set, `Password` wins |
140
+ | `Encoding` | string | e.g. `utf-8`, `tis-620` |
141
+ | `Driver` | string | ODBC driver name override, e.g. `ODBC Driver 18 for SQL Server` |
142
+
143
+ ```ini
144
+ PasswordBase64=c2VjcmV0 ; base64("secret")
145
+ ```
146
+
147
+ ## 3. Run an export (CLI)
148
+
149
+ `ping-dataexport` and `python -m ping_dataexport` are equivalent. The config
150
+ file defaults to `./config.ini` (`-cf` to point elsewhere).
151
+
152
+ ```bash
153
+ # preview to the console (no -o/-dir): prints the first rows
154
+ ping-dataexport -s SQLITE01 -q "SELECT * FROM sales"
155
+
156
+ # single file — format inferred from the extension (.csv/.txt/.parquet)
157
+ ping-dataexport -s SQLITE01 -q "SELECT * FROM sales" -o sales.parquet
158
+ ping-dataexport -s SQLITE01 -q "SELECT * FROM sales" -o sales.csv -gz # gzip
159
+
160
+ # validate + preview only, write nothing
161
+ ping-dataexport -s SQLITE01 -q "SELECT * FROM sales" -o sales.csv -t
162
+ ```
163
+
164
+ All options:
165
+
166
+ | Option | Full name | Meaning |
167
+ |--------|-----------|---------|
168
+ | `-s` | `--source` | DB config section name (e.g. `DB01`) |
169
+ | `-q` | `--query` | SQL query |
170
+ | `-o` | `--output` | single output file (format from extension, or `-f`) |
171
+ | `-dir` | `--outputdir` | output directory (required for `-m` modes); `-od` still works as a legacy alias |
172
+ | `-pf` | `--prefix` | output filename prefix for mode exports; `-bn`/`--basename` still work as legacy aliases |
173
+ | `-f` | `--format` | `csv`, `txt`, `parquet` |
174
+ | `-gz` | `--gzip` | gzip the output (CSV/TXT only) |
175
+ | `-sep` | `--sep` | field separator (default `,`; `\t` maps to tab) |
176
+ | `-quo` | `--quote` | quote character (default `"`) |
177
+ | `-stnl` | `--stripnewline` | newline handling in text columns: `space` \| `blank` \| `escape` \| `doubleescape` |
178
+ | `-stmc` | `--stripmetachar` | strip invisible/control characters from text columns |
179
+ | `-fs` | `--fetchsize` | fetch size (rows per batch) |
180
+ | `-m` | `--mode` | export mode: `groupby` \| `date` \| `relativedate` \| `monthbydate` \| `splitrow` \| `watermark` — see section 4 |
181
+ | `-col` | `--column` | column the mode partitions on |
182
+ | `-row` | `--row` | rows per file (`-m splitrow`) |
183
+ | `-df` | `--datefrom` | start / anchor date `yyyy-MM-dd`; omitted with `-dr`/`-mr` ⇒ today (system date) |
184
+ | `-dt` | `--dateto` | end date `yyyy-MM-dd` (`-m date`) |
185
+ | `-dr` | `--daysrelative` | days back from `-df` (`-m relativedate`) |
186
+ | `-mr` | `--monthsrelative` | months back from `-df` (`-m monthbydate`) |
187
+ | `-rm` | `--removeexisting` | `Yes`: clear the output directory before exporting |
188
+ | `-j` | `--job` | run one job.ini section see section 5 |
189
+ | `-jf` | `--jobfile` | job file path (default `./job.ini`) |
190
+ | `-p` | `--parallel` | run ALL job sections in parallel with N workers (default: CPU count); `-pl` still works as a legacy alias |
191
+ | `-n` | `--name` | job name shown in logs and the default log filename |
192
+ | `-cf` | `--configfile` | config file path (default `./config.ini`) |
193
+ | `-lf` | `--logfile` | log file path |
194
+ | `-t` | `--test` | test mode: validate + preview, no files |
195
+
196
+ ### Which options must go together
197
+
198
+ | If you use… | You must also have… | Notes |
199
+ |-------------|--------------------|-------|
200
+ | (anything) | `-s` + `-q` | the two basics always required, unless `-j` supplies them from a job file |
201
+ | `-o` | nothing else | single file; **cannot** be combined with `-dir`/`-pf` |
202
+ | `-m` (any mode) | `-dir` + `-pf` | every mode writes multiple files, so it needs a directory + filename prefix |
203
+ | `-dir` or `-pf` | `-m` | they only make sense for mode exports |
204
+ | `-m groupby` / `watermark` | `-col` | the column to group / track |
205
+ | `-m date` | `-col` + `-df` + `-dt` | the date column and an explicit range |
206
+ | `-m relativedate` | `-col` + `-dr` | `-df` optional (defaults to today) |
207
+ | `-m monthbydate` | `-col` + `-mr` | `-df` optional (defaults to today) |
208
+ | `-m splitrow` | `-row` | no `-col` needed |
209
+ | `-gz` | CSV/TXT output | rejected with parquet (it compresses itself) |
210
+ | no `-o` and no `-dir` | — | console preview only (first `PreviewRow` rows, nothing written) |
211
+
212
+ ## 4. Export modes (partitioned outputs)
213
+
214
+ Modes split one query into multiple files. All need `-dir` (output directory)
215
+ + `-pf` (filename prefix); partition values are always bound SQL parameters.
216
+
217
+ **`groupby` — one file per distinct value of a column:**
218
+
219
+ ```bash
220
+ ping-dataexport -s DB01 -q "SELECT * FROM sales" -m groupby -col region -dir by_region -pf sales
221
+ ```
222
+ ```
223
+ by_region/
224
+ ├── sales_@@NULL@@.csv <- rows where region IS NULL
225
+ ├── sales_MY.csv
226
+ ├── sales_SG.csv
227
+ ├── sales_TH.csv
228
+ └── sales_VN.csv
229
+ ```
230
+
231
+ **`date` — one file per day in a date range (inclusive):**
232
+
233
+ ```bash
234
+ ping-dataexport -s DB01 -q "SELECT * FROM sales" -m date -col txn_date \
235
+ -df 2026-01-01 -dt 2026-01-05 -dir daily -pf sales
236
+ ```
237
+ ```
238
+ daily/
239
+ ├── sales_2026-01-01.csv
240
+ ├── sales_2026-01-02.csv
241
+ ├── sales_2026-01-03.csv
242
+ ├── sales_2026-01-04.csv
243
+ └── sales_2026-01-05.csv
244
+ ```
245
+
246
+ **`relativedate` — N days back from an anchor date, anchor included
247
+ (omit `-df` to anchor on today):**
248
+
249
+ ```bash
250
+ ping-dataexport -s DB01 -q "SELECT * FROM sales" -m relativedate -col txn_date \
251
+ -df 2026-01-10 -dr 3 -dir rel -pf sales
252
+ ```
253
+ ```
254
+ rel/
255
+ ├── sales_2026-01-07.csv
256
+ ├── sales_2026-01-08.csv
257
+ ├── sales_2026-01-09.csv
258
+ └── sales_2026-01-10.csv
259
+ ```
260
+
261
+ **`monthbydate` — N months back plus the anchor month (the anchor month stops
262
+ at the anchor date; omit `-df` to anchor on today):**
263
+
264
+ ```bash
265
+ ping-dataexport -s DB01 -q "SELECT * FROM sales" -m monthbydate -col txn_date \
266
+ -df 2026-03-15 -mr 3 -dir monthly -pf sales
267
+ ```
268
+ ```
269
+ monthly/
270
+ ├── sales_202512.csv
271
+ ├── sales_202601.csv
272
+ ├── sales_202602.csv
273
+ └── sales_202603.csv <- 2026-03-01 .. 2026-03-15 only
274
+ ```
275
+
276
+ **`splitrow` rotate to a new file every N rows:**
277
+
278
+ ```bash
279
+ ping-dataexport -s DB01 -q "SELECT * FROM sales ORDER BY id" -m splitrow -row 100000 -dir split -pf part
280
+ ```
281
+ ```
282
+ split/
283
+ ├── part_1.csv <- 100,000 rows each
284
+ ├── part_2.csv
285
+ └── part_3.csv <- remainder
286
+ ```
287
+
288
+ **`watermark`incremental: only rows newer than the last run:**
289
+
290
+ ```bash
291
+ ping-dataexport -s DB01 -q "SELECT * FROM sales" -m watermark -col txn_date -dir incr -pf sales
292
+ ```
293
+ ```
294
+ incr/
295
+ ├── sales_20260831_120000.csv <- filename = run timestamp
296
+ └── __watermark__.ini <- state file: highest txn_date exported so far
297
+ ```
298
+
299
+ Run the watermark job again: no new rows → no new file; new rows → one more
300
+ timestamped file. Delete `__watermark__.ini` to start over from a full export.
301
+
302
+ Every run also prints structured log lines to the console (and to the log
303
+ file — see *Output and logs* below):
304
+
305
+ ```
306
+ 2026-08-31 11:24:41 | JOB_20260831_112441 | INFO | CONFIG | JOB | source=DB01 mode=groupby
307
+ 2026-08-31 11:24:41 | JOB_20260831_112441 | INFO | EXPORT | PARTITION | region = 'TH'
308
+ 2026-08-31 11:24:41 | JOB_20260831_112441 | INFO | EXPORT | FILE | ./output/by_region/sales_TH.csv
309
+ 2026-08-31 11:24:41 | JOB_20260831_112441 | INFO | EXPORT | PROGRESS | 200 rows (finished)
310
+ 2026-08-31 11:24:41 | JOB_20260831_112441 | INFO | RESULT | COMPLETED | 0.2 seconds
311
+ ```
312
+
313
+ ## 5. Job files (job.ini)
314
+
315
+ Put recurring exports in a job file so you never retype long commands. Each
316
+ `[Section]` is **one job**: run one with `-j SectionName`, or all of them at
317
+ once with `-p`. Every key is simply a CLI option written as a config key:
318
+
319
+ ```ini
320
+ [DailySales]
321
+ Source=DB01
322
+ Query=SELECT id, txn_date, amount FROM sales
323
+ WHERE region = 'TH'
324
+ Mode=date
325
+ Column=txn_date
326
+ DateFrom=2026-01-01
327
+ DateTo=2026-01-31
328
+ OutputDir=out/daily_sales
329
+ BaseName=sales
330
+ Format=csv
331
+
332
+ [FullDump]
333
+ Source=DB01
334
+ Query=SELECT * FROM sales
335
+ Output=out/full_dump.parquet
336
+ ```
337
+
338
+ ```bash
339
+ ping-dataexport -j DailySales # one section (job.ini by default, -jf elsewhere)
340
+ ping-dataexport -j DailySales -dir other_dir # CLI options override the section
341
+ ping-dataexport -p 4 # ALL sections in parallel, 4 workers
342
+ ```
343
+
344
+ Every job.ini key and the CLI option it mirrors (keys are case-insensitive):
345
+
346
+ | Job key | CLI option | Meaning |
347
+ |---------|------------|---------|
348
+ | `Source` | `-s` / `--source` | which config.ini DB section to export from |
349
+ | `Query` | `-q` / `--query` | the SQL to run; indent continuation lines for multi-line queries |
350
+ | `Output` | `-o` / `--output` | single output file path |
351
+ | `OutputDir` | `-dir` / `--outputdir` | output directory (needed when `Mode=` is set) |
352
+ | `BaseName` | `-pf` / `--prefix` | output filename prefix for mode exports |
353
+ | `Format` | `-f` / `--format` | `csv` / `txt` / `parquet` (else inferred from `Output` extension) |
354
+ | `Gzip` | `-gz` / `--gzip` | `Yes`/`No` — gzip the output (CSV/TXT only) |
355
+ | `Sep` | `-sep` / `--sep` | field separator (default `,`; `\t` = tab) |
356
+ | `Quote` | `-quo` / `--quote` | quote character (default `"`) |
357
+ | `StripNewLine` | `-stnl` / `--stripnewline` | `space` \| `blank` \| `escape` \| `doubleescape` |
358
+ | `StripMetaChar` | `-stmc` / `--stripmetachar` | `Yes`/`No` — strip invisible/control characters |
359
+ | `FetchSize` | `-fs` / `--fetchsize` | rows per batch |
360
+ | `Mode` | `-m` / `--mode` | `groupby` / `date` / `relativedate` / `monthbydate` / `splitrow` / `watermark` |
361
+ | `Column` | `-col` / `--column` | the column the mode partitions on |
362
+ | `Row` | `-row` / `--row` | rows per file (`Mode=splitrow`) |
363
+ | `DateFrom` | `-df` / `--datefrom` | start / anchor date, `yyyy-MM-dd` |
364
+ | `DateTo` | `-dt` / `--dateto` | end date, `yyyy-MM-dd` (`Mode=date`) |
365
+ | `DaysRelative` | `-dr` / `--daysrelative` | days back from `DateFrom` (`Mode=relativedate`) |
366
+ | `MonthsRelative` | `-mr` / `--monthsrelative` | months back from `DateFrom` (`Mode=monthbydate`) |
367
+ | `RemoveExisting` | `-rm` / `--removeexisting` | `Yes`: clear `OutputDir` before exporting |
368
+ | `Name` | `-n` / `--name` | job name shown in logs; default: the section name |
369
+
370
+ A full sample ships as `job.sample.ini` in the source distribution.
371
+
372
+ ## 6. Use as a library (Python / Airflow)
373
+
374
+ ```python
375
+ from ping_dataexport.api import run_export, run_job, run_jobfile
376
+
377
+ # ad-hoc any CLI option works as a keyword argument
378
+ result = run_export("DB01", "SELECT * FROM sales",
379
+ configfile="config.ini", output="sales.parquet")
380
+ print(result.status, result.rows, result.files)
381
+
382
+ # one job.ini section / every section in parallel
383
+ run_job("DailySales", jobfile="job.ini", configfile="config.ini")
384
+ results = run_jobfile(jobfile="job.ini", configfile="config.ini", workers=4)
385
+ ```
386
+
387
+ Failures raise exceptions (`ConfigError`, `JobValidationError`, `ExportError`)
388
+ never `sys.exit` so an Airflow task fails cleanly. `run_jobfile` is the
389
+ exception: one job's failure never stops the others; check each
390
+ `ExportResult.status`.
391
+
392
+ The package also works with **no pip install at all**: copy the
393
+ `ping_dataexport/` folder next to your code and import it relative imports
394
+ only, no metadata lookups.
395
+
396
+ ## Output and logs
397
+
398
+ - Relative output paths resolve under `ExportFolder`; `ForceMakeDir=Yes`
399
+ creates missing directories.
400
+ - Every run logs structured lines to stdout and to a log file:
401
+ `-lf path` > `LogFile=` in config.ini > default
402
+ `{ExportFolder}/logs/log_{jobname}.txt`.
403
+ - CLI exit codes: `0` success, `1` failure (in `-p` parallel mode: `1` if any
404
+ job failed).
405
+
406
+ ## ODBC prerequisites (ODBC sources only)
407
+
408
+ The Python drivers install via pip, but ODBC drivers are OS-level installs:
409
+ Microsoft ODBC Driver 17/18 for SQL Server, FreeTDS (`apt install tdsodbc`), or
410
+ your Oracle ODBC driver. `Type=sqlite` and `Type=oracle` (thin mode) need none.
411
+
412
+ ## License
413
+
414
+ Apache-2.0.