ping-dataexport 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. ping_dataexport/__init__.py +11 -0
  2. ping_dataexport/__main__.py +54 -0
  3. ping_dataexport/api.py +164 -0
  4. ping_dataexport/applogger.py +104 -0
  5. ping_dataexport/cli.py +101 -0
  6. ping_dataexport/config.py +233 -0
  7. ping_dataexport/db/__init__.py +0 -0
  8. ping_dataexport/db/connector.py +99 -0
  9. ping_dataexport/db/oracle_connector.py +86 -0
  10. ping_dataexport/db/pyodbc_connector.py +82 -0
  11. ping_dataexport/db/sqlite_connector.py +61 -0
  12. ping_dataexport/db/type_map.py +118 -0
  13. ping_dataexport/errors.py +19 -0
  14. ping_dataexport/export/__init__.py +0 -0
  15. ping_dataexport/export/engine.py +170 -0
  16. ping_dataexport/export/options.py +48 -0
  17. ping_dataexport/export/runner.py +50 -0
  18. ping_dataexport/export/util.py +65 -0
  19. ping_dataexport/job/__init__.py +0 -0
  20. ping_dataexport/job/executor.py +107 -0
  21. ping_dataexport/job/shared_context.py +22 -0
  22. ping_dataexport/job/spec.py +251 -0
  23. ping_dataexport/job/validator.py +87 -0
  24. ping_dataexport/paths.py +71 -0
  25. ping_dataexport/planner/__init__.py +0 -0
  26. ping_dataexport/planner/base.py +71 -0
  27. ping_dataexport/planner/datelist.py +35 -0
  28. ping_dataexport/planner/groupby.py +71 -0
  29. ping_dataexport/planner/monthbydate.py +104 -0
  30. ping_dataexport/planner/single.py +18 -0
  31. ping_dataexport/planner/watermark.py +202 -0
  32. ping_dataexport/writer/__init__.py +0 -0
  33. ping_dataexport/writer/arrow_map.py +174 -0
  34. ping_dataexport/writer/base.py +37 -0
  35. ping_dataexport/writer/delimited.py +108 -0
  36. ping_dataexport/writer/parquet.py +84 -0
  37. ping_dataexport-0.1.0.dist-info/METADATA +228 -0
  38. ping_dataexport-0.1.0.dist-info/RECORD +42 -0
  39. ping_dataexport-0.1.0.dist-info/WHEEL +5 -0
  40. ping_dataexport-0.1.0.dist-info/entry_points.txt +2 -0
  41. ping_dataexport-0.1.0.dist-info/licenses/LICENSE +201 -0
  42. ping_dataexport-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,37 @@
1
+ """RowWriter interface — one implementation per output format."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from pathlib import Path
7
+ from typing import Any, List, Optional, Sequence
8
+
9
+ from ..db.connector import ColumnInfo
10
+
11
+
12
+ class RowWriter(ABC):
13
+ """open() -> write_row()* -> close(). abort() removes the partial file on failure."""
14
+
15
+ def __init__(self) -> None:
16
+ self._path: Optional[Path] = None
17
+
18
+ @abstractmethod
19
+ def open(self, path: Path, columns: List[ColumnInfo]) -> None:
20
+ """Create/truncate the target file for the given result columns."""
21
+
22
+ @abstractmethod
23
+ def write_row(self, row: Sequence[Any], row_id: int) -> None:
24
+ """Write one row. row_id is the engine's running counter (audit column)."""
25
+
26
+ @abstractmethod
27
+ def close(self) -> None:
28
+ """Flush and close; must be idempotent."""
29
+
30
+ def abort(self) -> None:
31
+ """Close and delete the partial output of a failed partition."""
32
+ try:
33
+ self.close()
34
+ finally:
35
+ if self._path is not None and self._path.exists():
36
+ self._path.unlink()
37
+ self._path = None
@@ -0,0 +1,108 @@
1
+ """DelimitedRowWriter — CSV/TXT with gzip, strip rules, audit columns.
2
+
3
+ Hand-rolled (NOT the csv module) so the quoting rules are exact and ours:
4
+ - csv: minimal quoting — a field is quoted only when it contains the separator,
5
+ the quote char, or a newline; embedded quotes are doubled.
6
+ - txt: tab separator forced, quoting disabled.
7
+ - NULL -> empty string; UTF-8; '\n' line endings; create/truncate.
8
+ - Value rendering preserves type fidelity: Decimal via str() (no float round-trip),
9
+ dates yyyy-MM-dd, timestamps yyyy-MM-dd HH:mm:ss[.ffffff], True/False booleans.
10
+ - Audit columns (RowID / DataDate) are appended unconditionally.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import datetime
16
+ import decimal
17
+ import gzip
18
+ import io
19
+ from pathlib import Path
20
+ from typing import Any, Callable, List, Optional, Sequence
21
+
22
+ from ..db.connector import STRING, ColumnInfo
23
+ from ..export.options import ExportOptions
24
+ from ..export.util import build_text_cleaner
25
+ from .base import RowWriter
26
+
27
+
28
+ def render_value(value: Any, clean_text: Callable[[str], str]) -> str:
29
+ """One DB value -> its delimited-text form. Strip rules apply to strings only."""
30
+ if value is None:
31
+ return ""
32
+ if isinstance(value, str):
33
+ return clean_text(value)
34
+ if isinstance(value, bool):
35
+ return "True" if value else "False"
36
+ if isinstance(value, decimal.Decimal):
37
+ return str(value)
38
+ if isinstance(value, datetime.datetime):
39
+ base = value.strftime("%Y-%m-%d %H:%M:%S")
40
+ return f"{base}.{value.microsecond:06d}" if value.microsecond else base
41
+ if isinstance(value, datetime.date):
42
+ return value.isoformat()
43
+ if isinstance(value, datetime.time):
44
+ return value.isoformat()
45
+ if isinstance(value, (bytes, bytearray, memoryview)):
46
+ return bytes(value).hex()
47
+ return str(value)
48
+
49
+
50
+ class DelimitedRowWriter(RowWriter):
51
+ def __init__(self, options: ExportOptions, data_date: str):
52
+ super().__init__()
53
+ if options.format == "txt":
54
+ self.sep = "\t"
55
+ self.quote: Optional[str] = None # txt: no quoting
56
+ else:
57
+ self.sep = options.sep
58
+ self.quote = options.quote
59
+ self.gzip = options.gzip
60
+ self.row_id_column = options.row_id_column
61
+ self.data_date_column = options.data_date_column
62
+ self.data_date = data_date
63
+ self._clean = build_text_cleaner(options.stripnewline, options.stripmetachar)
64
+ self._fh: Optional[io.TextIOBase] = None
65
+ self._is_string_col: List[bool] = []
66
+
67
+ def open(self, path: Path, columns: List[ColumnInfo]) -> None:
68
+ self._path = path
69
+ if self.gzip:
70
+ self._fh = gzip.open(path, "wt", encoding="utf-8", newline="\n")
71
+ else:
72
+ self._fh = open(path, "w", encoding="utf-8", newline="\n")
73
+ self._is_string_col = [c.kind == STRING for c in columns]
74
+ header = [c.name for c in columns]
75
+ if self.row_id_column:
76
+ header.append(self.row_id_column)
77
+ if self.data_date_column:
78
+ header.append(self.data_date_column)
79
+ self._fh.write(self.sep.join(self._quote_field(h) for h in header) + "\n")
80
+
81
+ def write_row(self, row: Sequence[Any], row_id: int) -> None:
82
+ fields = [
83
+ self._quote_field(render_value(v, self._clean if is_str else _identity))
84
+ for v, is_str in zip(row, self._is_string_col)
85
+ ]
86
+ if self.row_id_column:
87
+ fields.append(str(row_id))
88
+ if self.data_date_column:
89
+ fields.append(self._quote_field(self.data_date))
90
+ self._fh.write(self.sep.join(fields) + "\n")
91
+
92
+ def close(self) -> None:
93
+ if self._fh is not None:
94
+ try:
95
+ self._fh.close()
96
+ finally:
97
+ self._fh = None
98
+
99
+ def _quote_field(self, text: str) -> str:
100
+ if self.quote is None:
101
+ return text
102
+ if self.quote in text or self.sep in text or "\n" in text or "\r" in text:
103
+ return self.quote + text.replace(self.quote, self.quote * 2) + self.quote
104
+ return text
105
+
106
+
107
+ def _identity(value: str) -> str:
108
+ return value
@@ -0,0 +1,84 @@
1
+ """ParquetRowWriter — PyArrow ParquetWriter lifecycle; ArrowRowMapper does the mapping.
2
+
3
+ - Snappy compression; --gzip is rejected at validation (parquet compresses itself).
4
+ - Rows buffer per-column and flush as a typed RecordBatch every fetch_size rows,
5
+ so memory stays bounded regardless of result size.
6
+ - Audit columns: RowID -> int64, DataDate -> string; a clash with a query column
7
+ name (case-insensitive) skips that audit column.
8
+ - Split mode reuses the same schema across part files; row_id keeps counting
9
+ (the engine owns the counter).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+ from typing import Any, List, Optional, Sequence
16
+
17
+ from ..db.connector import BIGINT, STRING, ColumnInfo
18
+ from ..export.options import ExportOptions
19
+ from ..export.util import build_text_cleaner
20
+ from .arrow_map import ArrowRowMapper
21
+ from .base import RowWriter
22
+
23
+
24
+ class ParquetRowWriter(RowWriter):
25
+ def __init__(self, options: ExportOptions, data_date: str):
26
+ super().__init__()
27
+ self.fetch_size = options.fetch_size
28
+ self.data_date = data_date
29
+ self._clean = build_text_cleaner(options.stripnewline, options.stripmetachar)
30
+ self._row_id_column = options.row_id_column
31
+ self._data_date_column = options.data_date_column
32
+ self._writer: Optional[Any] = None
33
+ self._mapper: Optional[ArrowRowMapper] = None
34
+ self._buffers: List[List[Any]] = []
35
+ self._buffered = 0
36
+ self._audit_row_id = False
37
+ self._audit_data_date = False
38
+
39
+ def open(self, path: Path, columns: List[ColumnInfo]) -> None:
40
+ import pyarrow.parquet as pq
41
+
42
+ self._path = path
43
+ query_names = {c.name.lower() for c in columns}
44
+ effective = list(columns)
45
+ self._audit_row_id = bool(
46
+ self._row_id_column and self._row_id_column.lower() not in query_names
47
+ )
48
+ if self._audit_row_id:
49
+ effective.append(ColumnInfo(self._row_id_column, BIGINT))
50
+ self._audit_data_date = bool(
51
+ self._data_date_column and self._data_date_column.lower() not in query_names
52
+ )
53
+ if self._audit_data_date:
54
+ effective.append(ColumnInfo(self._data_date_column, STRING))
55
+
56
+ self._mapper = ArrowRowMapper(effective, self._clean)
57
+ self._writer = pq.ParquetWriter(str(path), self._mapper.schema, compression="snappy")
58
+ self._buffers = self._mapper.new_buffers()
59
+ self._buffered = 0
60
+
61
+ def write_row(self, row: Sequence[Any], row_id: int) -> None:
62
+ values = list(row)
63
+ if self._audit_row_id:
64
+ values.append(row_id)
65
+ if self._audit_data_date:
66
+ values.append(self.data_date)
67
+ self._mapper.append_row(self._buffers, values)
68
+ self._buffered += 1
69
+ if self._buffered >= self.fetch_size:
70
+ self._flush()
71
+
72
+ def close(self) -> None:
73
+ if self._writer is not None:
74
+ try:
75
+ if self._buffered:
76
+ self._flush()
77
+ finally:
78
+ self._writer.close()
79
+ self._writer = None
80
+
81
+ def _flush(self) -> None:
82
+ self._writer.write_batch(self._mapper.to_record_batch(self._buffers))
83
+ self._buffers = self._mapper.new_buffers()
84
+ self._buffered = 0
@@ -0,0 +1,228 @@
1
+ Metadata-Version: 2.4
2
+ Name: ping-dataexport
3
+ Version: 0.1.0
4
+ Summary: Universal database data export to CSV/TXT/Parquet — streaming, typed, no pandas
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
+ Universal database data export: connect to a database, run a SQL query, stream
33
+ the result to **CSV / TXT / Parquet**. Built for data-platform work — exporting
34
+ legacy databases into files a data lake or warehouse can ingest.
35
+
36
+ - **Type fidelity first** — rows stream cursor → writer with native driver
37
+ types. `DECIMAL(p,s)` lands in Parquet as `decimal128(p,s)`, ints with NULLs
38
+ stay ints, dates stay dates. No pandas, ever.
39
+ - **Databases** — SQLite (stdlib, zero setup), SQL Server (MS ODBC / FreeTDS),
40
+ Oracle (native thin mode or ODBC), any ODBC source.
41
+ - **Two front doors** — a CLI and a library API (`ping_dataexport.api`) built
42
+ for Airflow: exceptions instead of exit codes, results as objects.
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install ping-dataexport # base (pyarrow) — sqlite works out of the box
48
+ pip install ping-dataexport[odbc] # + pyodbc (SQL Server, FreeTDS, ODBC)
49
+ pip install ping-dataexport[oracle] # + oracledb (Oracle native)
50
+ pip install ping-dataexport[drivers] # + both
51
+ ```
52
+
53
+ ## 1. Create a config.ini
54
+
55
+ One `[Main]` section for defaults, one section per database source:
56
+
57
+ ```ini
58
+ [Main]
59
+ ExportFolder=./output ; relative output/log paths resolve under this
60
+ FetchSize=10000
61
+ ForceMakeDir=Yes
62
+ RowIDColumn=row_id ; optional audit column names
63
+ DataDateColumn=data_date
64
+
65
+ [SQLITE01]
66
+ Type=sqlite
67
+ Database=./demo.sqlite
68
+
69
+ [DB01]
70
+ Type=mssql-odbc
71
+ Host=host
72
+ Port=1433
73
+ Database=DB
74
+ Username=user
75
+ Password=secret
76
+
77
+ [ORA01]
78
+ Type=oracle
79
+ Host=orahost
80
+ Port=1521
81
+ Service=ORCLPDB
82
+ Username=scott
83
+ Password=tiger
84
+ ```
85
+
86
+ `Type` values: `sqlite`, `mssql-odbc`, `freetds`, `oracle`, `oracle-odbc`,
87
+ `odbc`. ODBC sources also accept a raw `ConnectionString=` instead of
88
+ Host/Port/Database. A full sample ships as `config.sample.ini` in the source
89
+ distribution.
90
+
91
+ Instead of `Password=` you can use `PasswordBase64=` with the base64-encoded
92
+ password (obfuscation, not encryption). If both are present, `Password` wins.
93
+
94
+ ```ini
95
+ PasswordBase64=c2VjcmV0 ; base64("secret")
96
+ ```
97
+
98
+ ## 2. Run an export (CLI)
99
+
100
+ `ping-dataexport` and `python -m ping_dataexport` are equivalent. The config
101
+ file defaults to `./config.ini` (`-cf` to point elsewhere).
102
+
103
+ ```bash
104
+ # preview to the console (no -o/-od): prints the first rows
105
+ ping-dataexport -s SQLITE01 -q "SELECT * FROM sales"
106
+
107
+ # single file — format inferred from the extension (.csv/.txt/.parquet)
108
+ ping-dataexport -s SQLITE01 -q "SELECT * FROM sales" -o sales.parquet
109
+ ping-dataexport -s SQLITE01 -q "SELECT * FROM sales" -o sales.csv -gz # gzip
110
+
111
+ # validate + preview only, write nothing
112
+ ping-dataexport -s SQLITE01 -q "SELECT * FROM sales" -o sales.csv -t
113
+ ```
114
+
115
+ Common options:
116
+
117
+ | Option | Meaning |
118
+ |--------|---------|
119
+ | `-s` / `-q` | source section name / SQL query |
120
+ | `-o` | single output file (format from extension, or `-f`) |
121
+ | `-od` / `-bn` | output directory + base filename (required for `-m` modes) |
122
+ | `-f` | `csv`, `txt`, `parquet` |
123
+ | `-gz` | gzip the output (CSV/TXT only) |
124
+ | `-sp` / `-quo` | field separator / quote character |
125
+ | `-stnl space\|blank`, `-stmc` | strip newlines / metacharacters inside values |
126
+ | `-fs` | fetch size (rows per batch) |
127
+ | `-rm Yes` | clear the output directory before exporting |
128
+ | `-cf` / `-lf` | config file path / log file path |
129
+ | `-t` | test mode: validate + preview, no files |
130
+
131
+ ## 3. Export modes (partitioned outputs)
132
+
133
+ Modes split one query into multiple files. All need `-od` + `-bn`; partition
134
+ values are always bound SQL parameters.
135
+
136
+ ```bash
137
+ # one file per distinct value of a column
138
+ ping-dataexport -s DB01 -q "SELECT * FROM sales" -m groupby -col region -od by_region -bn sales
139
+
140
+ # one file per day in a date range
141
+ ping-dataexport -s DB01 -q "SELECT * FROM sales" -m date -col txn_date \
142
+ -df 2026-01-01 -dt 2026-01-31 -od daily -bn sales
143
+
144
+ # N days back from an anchor date / N months of month-files
145
+ ping-dataexport -s DB01 -q "..." -m relativedate -col txn_date -df 2026-01-10 -dr 7 -od rel -bn r
146
+ ping-dataexport -s DB01 -q "..." -m monthbydate -col txn_date -df 2026-03-15 -mr 3 -od monthly -bn m
147
+
148
+ # rotate a new file every N rows
149
+ ping-dataexport -s DB01 -q "SELECT * FROM sales ORDER BY id" -m splitrow -row 100000 -od split -bn part
150
+
151
+ # incremental: exports only rows newer than the last run (state file in -od)
152
+ ping-dataexport -s DB01 -q "SELECT * FROM sales" -m watermark -col txn_date -od incr -bn sales
153
+ ```
154
+
155
+ ## 4. Job files (job.ini)
156
+
157
+ Put recurring exports in a job file — keys map 1:1 to the CLI options:
158
+
159
+ ```ini
160
+ [DailySales]
161
+ Source=DB01
162
+ Query=SELECT id, txn_date, amount FROM sales
163
+ WHERE region = 'TH'
164
+ Mode=date
165
+ Column=txn_date
166
+ DateFrom=2026-01-01
167
+ DateTo=2026-01-31
168
+ OutputDir=out/daily_sales
169
+ BaseName=sales
170
+ Format=csv
171
+
172
+ [FullDump]
173
+ Source=DB01
174
+ Query=SELECT * FROM sales
175
+ Output=out/full_dump.parquet
176
+ ```
177
+
178
+ ```bash
179
+ ping-dataexport -j DailySales # one section (job.ini by default, -jf elsewhere)
180
+ ping-dataexport -j DailySales -od other_dir # CLI options override the section
181
+ ping-dataexport -pl 4 # ALL sections in parallel, 4 workers
182
+ ```
183
+
184
+ A full sample ships as `job.sample.ini` in the source distribution.
185
+
186
+ ## 5. Use as a library (Python / Airflow)
187
+
188
+ ```python
189
+ from ping_dataexport.api import run_export, run_job, run_jobfile
190
+
191
+ # ad-hoc — any CLI option works as a keyword argument
192
+ result = run_export("DB01", "SELECT * FROM sales",
193
+ configfile="config.ini", output="sales.parquet")
194
+ print(result.status, result.rows, result.files)
195
+
196
+ # one job.ini section / every section in parallel
197
+ run_job("DailySales", jobfile="job.ini", configfile="config.ini")
198
+ results = run_jobfile(jobfile="job.ini", configfile="config.ini", workers=4)
199
+ ```
200
+
201
+ Failures raise exceptions (`ConfigError`, `JobValidationError`, `ExportError`)
202
+ — never `sys.exit` — so an Airflow task fails cleanly. `run_jobfile` is the
203
+ exception: one job's failure never stops the others; check each
204
+ `ExportResult.status`.
205
+
206
+ The package also works with **no pip install at all**: copy the
207
+ `ping_dataexport/` folder next to your code and import it — relative imports
208
+ only, no metadata lookups.
209
+
210
+ ## Output and logs
211
+
212
+ - Relative output paths resolve under `ExportFolder`; `ForceMakeDir=Yes`
213
+ creates missing directories.
214
+ - Every run logs structured lines to stdout and to a log file:
215
+ `-lf path` > `LogFile=` in config.ini > default
216
+ `{ExportFolder}/logs/log_{jobname}.txt`.
217
+ - CLI exit codes: `0` success, `1` failure (in `-pl` parallel mode: `1` if any
218
+ job failed).
219
+
220
+ ## ODBC prerequisites (ODBC sources only)
221
+
222
+ The Python drivers install via pip, but ODBC drivers are OS-level installs:
223
+ Microsoft ODBC Driver 17/18 for SQL Server, FreeTDS (`apt install tdsodbc`), or
224
+ your Oracle ODBC driver. `Type=sqlite` and `Type=oracle` (thin mode) need none.
225
+
226
+ ## License
227
+
228
+ Apache-2.0.
@@ -0,0 +1,42 @@
1
+ ping_dataexport/__init__.py,sha256=X7yvlh0O2PpianqHs6AoWVs-dqk96uXkAsakLj6oWBQ,452
2
+ ping_dataexport/__main__.py,sha256=yV5QogtDfzTy4gfX6eDyypddJqOpuBhVHSBm6w1CQ10,1530
3
+ ping_dataexport/api.py,sha256=0VAtlTJ1ht3cNSHvGkfoDlVjiEgTEJo1Vk4bEU0ktEA,5914
4
+ ping_dataexport/applogger.py,sha256=ELa-TkDJWPlQu21IvpvMz1PwWxqlKiK7oCpqyiVyRKA,3750
5
+ ping_dataexport/cli.py,sha256=Rg-e39zdUeyqRzZt2KQEAokFpMg9IHdQmd0ZGfXe4Lg,4468
6
+ ping_dataexport/config.py,sha256=Ui6OgVnanV7IsY-uBZWOY7oiM-5BkU6hx6s4iJHaeK8,9180
7
+ ping_dataexport/errors.py,sha256=gX-UNqcq374Znq9K-8LRmBvUtCGxhz1xo2tspMGImXQ,583
8
+ ping_dataexport/paths.py,sha256=G6whXV4RnRCnIEypRwpvSBnbnsNVkDU7H-Kn_I7L1iI,2684
9
+ ping_dataexport/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ ping_dataexport/db/connector.py,sha256=wHHryAk-k4QtUjrYUAyXnb9AoC6VgWE6o1OY2cOcwBw,3239
11
+ ping_dataexport/db/oracle_connector.py,sha256=RAAr-ABjXnDLzXikyTVjwlvWEGPjFtMAK1DvpMPAczU,3079
12
+ ping_dataexport/db/pyodbc_connector.py,sha256=lXdP8TareRIyWKzMLnY1kmSfMDuQrlUV_-2UbhmYgCw,3355
13
+ ping_dataexport/db/sqlite_connector.py,sha256=CVmnbWMD0epcVnQHagwN8Z1_MwYHRF8L5fIAcyesLnA,2354
14
+ ping_dataexport/db/type_map.py,sha256=nJ7xmyYhQG9nRbtp7jTwXc69tAFcLTB9eQfWsZlPutY,4286
15
+ ping_dataexport/export/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ ping_dataexport/export/engine.py,sha256=YFIH6QJDpzuHhPoKxmcMOzRQwF2SP61RTGjU7KsqZnQ,6144
17
+ ping_dataexport/export/options.py,sha256=qyMAt8Chk1z3DZRmbisNFLUqi1y1SEOFIARb2RNoRCg,1500
18
+ ping_dataexport/export/runner.py,sha256=ruwJQy0SOX_x0C2loI6ZwqTHaniay1io9MnJoN_qQUM,1750
19
+ ping_dataexport/export/util.py,sha256=US5Xx3xkQsUjDH4tpZtYFq7X0Ofau0sHsRdpPU02Ncw,2461
20
+ ping_dataexport/job/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ ping_dataexport/job/executor.py,sha256=7AWNk8thyeCtch9Uiny0vDTTs-6qKt_Ut8zBt6bvZ80,4161
22
+ ping_dataexport/job/shared_context.py,sha256=q3oBVLj0XmFbd-uQNsCWQLjDTy1VLxSOewUK6jQkR3A,719
23
+ ping_dataexport/job/spec.py,sha256=Zrnw0YOVqv9XNuucCFaxzF47cTMQBVKAFd-dHa8R-q8,8995
24
+ ping_dataexport/job/validator.py,sha256=bPLwzzD0SkNygW6rdriitQbranahaeSi2FcASjYA_ZA,3669
25
+ ping_dataexport/planner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ ping_dataexport/planner/base.py,sha256=qM9bPPms-E_I-vtjr15azbtvEJDimrv-HR9AekOxQNc,2334
27
+ ping_dataexport/planner/datelist.py,sha256=djYxFTiRtIcWDt90gBQYydiycSKUvHOd-P0SFLO_Tvw,1271
28
+ ping_dataexport/planner/groupby.py,sha256=Opo9r_Rh9xx8d_OP0J10cco_40qq7TVArojkRmpvw9U,2569
29
+ ping_dataexport/planner/monthbydate.py,sha256=M6GkWZc0bpMeVs107pBDmv6uyNMOQDx_-Li539D78Wc,4331
30
+ ping_dataexport/planner/single.py,sha256=kVb755hnjHHld16O7tUSkduzrubf9w2Quaf9nS5aHm8,622
31
+ ping_dataexport/planner/watermark.py,sha256=Rc1Z_aQJBcYeD57Mq-3Li27by_Nq1mlqTZCS0gX9NeU,7764
32
+ ping_dataexport/writer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ ping_dataexport/writer/arrow_map.py,sha256=KYeABbs_Dm34qT19CqNmAWTzwrjWQ8e8VZMfPTyhvjw,6148
34
+ ping_dataexport/writer/base.py,sha256=Pc16NVbXf9nnpHax52jim8wcWMRAZnLXvYgNG8Glf7M,1170
35
+ ping_dataexport/writer/delimited.py,sha256=zr4sX_4sDakLuaUZ-I43VnmhrAoZEzrZSqOGjpugSAg,4123
36
+ ping_dataexport/writer/parquet.py,sha256=Neu2OW2XXYFcCUEMVgnIUIlANkp4rSzcqfwr_biGuDM,3274
37
+ ping_dataexport-0.1.0.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
38
+ ping_dataexport-0.1.0.dist-info/METADATA,sha256=0CkxcMRl-ND5V299OFsgtimnvz-woMQsYW8xwkBUSWA,8016
39
+ ping_dataexport-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
40
+ ping_dataexport-0.1.0.dist-info/entry_points.txt,sha256=GLTn1c1dKXLLib0N7MOzLRnRTgvwZOwigRqxCPphenw,66
41
+ ping_dataexport-0.1.0.dist-info/top_level.txt,sha256=_-lMZI38na42QmuQQF-WscRvuXvYyD6QzpXR8BClBJg,16
42
+ ping_dataexport-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ping-dataexport = ping_dataexport.__main__:main