etlplus 0.3.5__tar.gz → 0.3.17__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.
- etlplus-0.3.17/.coveragerc +21 -0
- etlplus-0.3.17/.github/actions/python-bootstrap/action.yml +42 -0
- etlplus-0.3.17/.github/workflows/ci.yml +160 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/PKG-INFO +1 -1
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus.egg-info/PKG-INFO +1 -1
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus.egg-info/SOURCES.txt +6 -0
- etlplus-0.3.17/tests/conftest.py +210 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/integration/conftest.py +105 -16
- etlplus-0.3.17/tests/integration/test_i_cli.py +244 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/integration/test_i_examples_data_parity.py +5 -0
- etlplus-0.3.17/tests/integration/test_i_pagination_strategy.py +551 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/integration/test_i_pipeline_smoke.py +41 -36
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/integration/test_i_pipeline_yaml_load.py +6 -0
- etlplus-0.3.17/tests/integration/test_i_run.py +69 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/integration/test_i_run_profile_pagination_defaults.py +11 -7
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/integration/test_i_run_profile_rate_limit_defaults.py +6 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/api/conftest.py +42 -15
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/api/test_u_auth.py +113 -123
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/api/test_u_config.py +60 -16
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/api/test_u_endpoint_client.py +447 -268
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/api/test_u_pagination_client.py +5 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/api/test_u_pagination_config.py +5 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/api/test_u_paginator.py +5 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/api/test_u_rate_limit_config.py +5 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/api/test_u_rate_limiter.py +5 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/api/test_u_request_manager.py +3 -2
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/api/test_u_retry_manager.py +6 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/api/test_u_transport.py +5 -0
- etlplus-0.3.17/tests/unit/config/test_u_connector.py +119 -0
- etlplus-0.3.17/tests/unit/config/test_u_pipeline.py +285 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/conftest.py +30 -30
- etlplus-0.3.17/tests/unit/test_u_cli.py +185 -0
- etlplus-0.3.17/tests/unit/test_u_enums.py +135 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/test_u_extract.py +212 -0
- etlplus-0.3.17/tests/unit/test_u_file.py +297 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/test_u_load.py +201 -0
- etlplus-0.3.17/tests/unit/test_u_mixins.py +47 -0
- etlplus-0.3.17/tests/unit/test_u_run.py +288 -0
- etlplus-0.3.17/tests/unit/test_u_run_helpers.py +385 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/test_u_transform.py +80 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/test_u_utils.py +92 -3
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/test_u_validate.py +39 -0
- etlplus-0.3.5/.github/workflows/ci.yml +0 -87
- etlplus-0.3.5/tests/conftest.py +0 -11
- etlplus-0.3.5/tests/integration/test_i_cli.py +0 -348
- etlplus-0.3.5/tests/integration/test_i_pagination_strategy.py +0 -452
- etlplus-0.3.5/tests/integration/test_i_run.py +0 -133
- etlplus-0.3.5/tests/unit/config/test_u_connector.py +0 -54
- etlplus-0.3.5/tests/unit/config/test_u_pipeline.py +0 -194
- etlplus-0.3.5/tests/unit/test_u_cli.py +0 -124
- etlplus-0.3.5/tests/unit/test_u_file.py +0 -100
- {etlplus-0.3.5 → etlplus-0.3.17}/.editorconfig +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/.gitattributes +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/.gitignore +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/.pre-commit-config.yaml +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/.ruff.toml +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/CODE_OF_CONDUCT.md +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/CONTRIBUTING.md +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/DEMO.md +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/LICENSE +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/Makefile +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/README.md +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/REFERENCES.md +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/docs/pipeline-guide.md +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/docs/snippets/installation_version.md +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/__init__.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/__main__.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/__version__.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/api/README.md +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/api/__init__.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/api/auth.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/api/config.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/api/endpoint_client.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/api/errors.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/api/pagination/__init__.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/api/pagination/client.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/api/pagination/config.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/api/pagination/paginator.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/api/rate_limiting/__init__.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/api/rate_limiting/config.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/api/rate_limiting/rate_limiter.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/api/request_manager.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/api/retry_manager.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/api/transport.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/api/types.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/cli.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/config/__init__.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/config/connector.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/config/jobs.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/config/pipeline.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/config/profile.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/config/types.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/config/utils.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/enums.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/extract.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/file.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/load.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/mixins.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/py.typed +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/run.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/run_helpers.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/transform.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/types.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/utils.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/validate.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/validation/__init__.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus/validation/utils.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus.egg-info/dependency_links.txt +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus.egg-info/entry_points.txt +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus.egg-info/requires.txt +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/etlplus.egg-info/top_level.txt +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/examples/README.md +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/examples/configs/pipeline.yml +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/examples/data/sample.csv +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/examples/data/sample.json +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/examples/data/sample.xml +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/examples/data/sample.xsd +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/examples/data/sample.yaml +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/examples/quickstart_python.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/pyproject.toml +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/pytest.ini +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/setup.cfg +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/setup.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/__init__.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/api/test_u_mocks.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tests/unit/validation/test_u_validation_utils.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tools/run_pipeline.py +0 -0
- {etlplus-0.3.5 → etlplus-0.3.17}/tools/update_demo_snippets.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# .coveragerc
|
|
2
|
+
# ETLPlus
|
|
3
|
+
#
|
|
4
|
+
# Copyright © 2025 Dagitali LLC. All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# An optional pytest-cov configuration file. Limits coverage measurement to the
|
|
7
|
+
# ETLPlus package and ignore test modules.
|
|
8
|
+
#
|
|
9
|
+
# See:
|
|
10
|
+
# 1. https://pytest-cov.readthedocs.io/en/latest/config.html
|
|
11
|
+
|
|
12
|
+
[run]
|
|
13
|
+
source = etlplus
|
|
14
|
+
branch = true
|
|
15
|
+
omit =
|
|
16
|
+
tests/*
|
|
17
|
+
*/tests/*
|
|
18
|
+
|
|
19
|
+
[report]
|
|
20
|
+
skip_covered = true
|
|
21
|
+
show_missing = true
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# action.yml
|
|
2
|
+
# ETLPlus
|
|
3
|
+
#
|
|
4
|
+
# Copyright © 2025 Dagitali LLC. All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# A GitHub Actions action to set up Python and install Python package
|
|
7
|
+
# dependencies.
|
|
8
|
+
#
|
|
9
|
+
# Notes
|
|
10
|
+
# - External GitHub Actions action references are each pinned to specific SHA
|
|
11
|
+
# to limit supply-chain risk.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
name: Setup Python and Install Dependencies
|
|
16
|
+
|
|
17
|
+
description: >-
|
|
18
|
+
Sets up the requested Python runtime, upgrades pip, and installs the provided
|
|
19
|
+
dependency list to keep workflow jobs consistent.
|
|
20
|
+
|
|
21
|
+
inputs:
|
|
22
|
+
python-version:
|
|
23
|
+
description: Python version to install via actions/setup-python
|
|
24
|
+
required: true
|
|
25
|
+
python-bootstrap:
|
|
26
|
+
description: Arguments passed to "pip install"
|
|
27
|
+
required: true
|
|
28
|
+
|
|
29
|
+
runs:
|
|
30
|
+
using: composite
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # Pinned v5
|
|
33
|
+
with:
|
|
34
|
+
python-version: ${{ inputs.python-version }}
|
|
35
|
+
|
|
36
|
+
- name: Upgrade pip
|
|
37
|
+
shell: bash
|
|
38
|
+
run: python -m pip install --upgrade pip
|
|
39
|
+
|
|
40
|
+
- name: Install dependencies
|
|
41
|
+
shell: bash
|
|
42
|
+
run: pip install ${{ inputs.python-bootstrap }}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# ci.yml
|
|
2
|
+
# ETLPlus
|
|
3
|
+
#
|
|
4
|
+
# Copyright © 2025 Dagitali LLC. All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# A GitHub Actions workflow configuration file for Continuous Integration (CI).
|
|
7
|
+
#
|
|
8
|
+
# Notes
|
|
9
|
+
# - The workflow includes jobs for linting, testing, building distributions,
|
|
10
|
+
# and publishing releases to GitHub and PyPI.
|
|
11
|
+
# - The workflow is hardened by removing global permissions and setting per-job
|
|
12
|
+
# scopes.
|
|
13
|
+
# - To harden workflow security, global permissions are absent, and per-job
|
|
14
|
+
# permissions are set as needed.
|
|
15
|
+
# - External GitHub Actions action references are each pinned to specific SHA
|
|
16
|
+
# to limit supply-chain risk.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
name: CI
|
|
21
|
+
|
|
22
|
+
on:
|
|
23
|
+
push:
|
|
24
|
+
branches:
|
|
25
|
+
# GitFlow branches
|
|
26
|
+
- main
|
|
27
|
+
- develop
|
|
28
|
+
- '**/feature/**'
|
|
29
|
+
- '**/bugfix/**'
|
|
30
|
+
- '**/release/**'
|
|
31
|
+
- '**/hotfix/**'
|
|
32
|
+
|
|
33
|
+
# Extended branches
|
|
34
|
+
- '**/chore/**'
|
|
35
|
+
- '**/ci/**'
|
|
36
|
+
- '**/docs/**'
|
|
37
|
+
tags: [ 'v*.*.*' ]
|
|
38
|
+
pull_request:
|
|
39
|
+
branches: [ main, develop ]
|
|
40
|
+
|
|
41
|
+
permissions: {}
|
|
42
|
+
|
|
43
|
+
jobs:
|
|
44
|
+
lint:
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
strategy: &python-matrix
|
|
47
|
+
matrix:
|
|
48
|
+
python-version: ['3.13', '3.14']
|
|
49
|
+
permissions: &permissions_read
|
|
50
|
+
contents: read
|
|
51
|
+
steps:
|
|
52
|
+
- &checkout_step
|
|
53
|
+
name: Checkout repository
|
|
54
|
+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # Pinned v4
|
|
55
|
+
with:
|
|
56
|
+
fetch-depth: 0
|
|
57
|
+
- uses: ./.github/actions/python-bootstrap
|
|
58
|
+
with:
|
|
59
|
+
python-version: ${{ matrix.python-version }}
|
|
60
|
+
python-bootstrap: ".[dev]"
|
|
61
|
+
- name: Ruff check
|
|
62
|
+
run: |
|
|
63
|
+
ruff version
|
|
64
|
+
ruff check .
|
|
65
|
+
ruff format --check .
|
|
66
|
+
|
|
67
|
+
test:
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
strategy: *python-matrix
|
|
70
|
+
permissions: *permissions_read
|
|
71
|
+
steps:
|
|
72
|
+
- *checkout_step
|
|
73
|
+
- uses: ./.github/actions/python-bootstrap
|
|
74
|
+
with:
|
|
75
|
+
python-version: ${{ matrix.python-version }}
|
|
76
|
+
python-bootstrap: "-e .[dev,yaml]"
|
|
77
|
+
- name: Run tests (with coverage)
|
|
78
|
+
run: |
|
|
79
|
+
pytest -q \
|
|
80
|
+
--cov \
|
|
81
|
+
--cov-branch \
|
|
82
|
+
--cov-config=.coveragerc \
|
|
83
|
+
--cov-report=term-missing \
|
|
84
|
+
--cov-report=xml \
|
|
85
|
+
tests/
|
|
86
|
+
|
|
87
|
+
- name: Upload coverage reports to Codecov
|
|
88
|
+
if: matrix.python-version == '3.13'
|
|
89
|
+
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # Pinned v5.5.2
|
|
90
|
+
with:
|
|
91
|
+
fail_ci_if_error: true
|
|
92
|
+
files: coverage.xml
|
|
93
|
+
flags: unit
|
|
94
|
+
name: etlplus
|
|
95
|
+
token: ${{ secrets.CODECOV_TOKEN }} # Omit for public repo
|
|
96
|
+
verbose: true
|
|
97
|
+
|
|
98
|
+
build:
|
|
99
|
+
name: Build distributions
|
|
100
|
+
runs-on: ubuntu-latest
|
|
101
|
+
if: &release_tag_condition >
|
|
102
|
+
startsWith(github.ref, 'refs/tags/v') ||
|
|
103
|
+
startsWith(github.ref, 'refs/tags/rc')
|
|
104
|
+
needs: [lint, test]
|
|
105
|
+
permissions: *permissions_read
|
|
106
|
+
steps:
|
|
107
|
+
- *checkout_step
|
|
108
|
+
- uses: ./.github/actions/python-bootstrap
|
|
109
|
+
with:
|
|
110
|
+
python-version: '3.13'
|
|
111
|
+
python-bootstrap: build
|
|
112
|
+
- name: Build distributions
|
|
113
|
+
run: |
|
|
114
|
+
python -m build
|
|
115
|
+
- name: Upload distributions
|
|
116
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # Pinned v4
|
|
117
|
+
with:
|
|
118
|
+
name: dist-artifacts
|
|
119
|
+
path: dist/*
|
|
120
|
+
if-no-files-found: error
|
|
121
|
+
|
|
122
|
+
release:
|
|
123
|
+
name: Publish GitHub Release
|
|
124
|
+
runs-on: ubuntu-latest
|
|
125
|
+
if: *release_tag_condition
|
|
126
|
+
needs: build
|
|
127
|
+
permissions:
|
|
128
|
+
contents: write
|
|
129
|
+
steps:
|
|
130
|
+
- *checkout_step
|
|
131
|
+
- &download_dist_step
|
|
132
|
+
name: Download distributions
|
|
133
|
+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # Pinned v4
|
|
134
|
+
with:
|
|
135
|
+
name: dist-artifacts
|
|
136
|
+
path: dist
|
|
137
|
+
- name: Publish GitHub release
|
|
138
|
+
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # Pinned v2
|
|
139
|
+
with:
|
|
140
|
+
files: dist/*
|
|
141
|
+
generate_release_notes: true
|
|
142
|
+
|
|
143
|
+
publish:
|
|
144
|
+
name: Publish to PyPI
|
|
145
|
+
runs-on: ubuntu-latest
|
|
146
|
+
if: *release_tag_condition
|
|
147
|
+
needs: build
|
|
148
|
+
permissions:
|
|
149
|
+
contents: read
|
|
150
|
+
id-token: write
|
|
151
|
+
environment:
|
|
152
|
+
name: pypi
|
|
153
|
+
url: https://pypi.org/project/etlplus/
|
|
154
|
+
steps:
|
|
155
|
+
- *checkout_step
|
|
156
|
+
- *download_dist_step
|
|
157
|
+
- name: Publish to PyPI
|
|
158
|
+
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # Pinned v1
|
|
159
|
+
with:
|
|
160
|
+
verbose: true
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
.coveragerc
|
|
1
2
|
.editorconfig
|
|
2
3
|
.gitattributes
|
|
3
4
|
.gitignore
|
|
@@ -13,6 +14,7 @@ REFERENCES.md
|
|
|
13
14
|
pyproject.toml
|
|
14
15
|
pytest.ini
|
|
15
16
|
setup.py
|
|
17
|
+
.github/actions/python-bootstrap/action.yml
|
|
16
18
|
.github/workflows/ci.yml
|
|
17
19
|
docs/pipeline-guide.md
|
|
18
20
|
docs/snippets/installation_version.md
|
|
@@ -85,9 +87,13 @@ tests/integration/test_i_run_profile_pagination_defaults.py
|
|
|
85
87
|
tests/integration/test_i_run_profile_rate_limit_defaults.py
|
|
86
88
|
tests/unit/conftest.py
|
|
87
89
|
tests/unit/test_u_cli.py
|
|
90
|
+
tests/unit/test_u_enums.py
|
|
88
91
|
tests/unit/test_u_extract.py
|
|
89
92
|
tests/unit/test_u_file.py
|
|
90
93
|
tests/unit/test_u_load.py
|
|
94
|
+
tests/unit/test_u_mixins.py
|
|
95
|
+
tests/unit/test_u_run.py
|
|
96
|
+
tests/unit/test_u_run_helpers.py
|
|
91
97
|
tests/unit/test_u_transform.py
|
|
92
98
|
tests/unit/test_u_utils.py
|
|
93
99
|
tests/unit/test_u_validate.py
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
:mod:`tests.conftest` module.
|
|
3
|
+
|
|
4
|
+
Global pytest fixtures shared across unit, integration, and end-to-end tests.
|
|
5
|
+
|
|
6
|
+
Notes
|
|
7
|
+
-----
|
|
8
|
+
- Provides CLI helpers so tests no longer need to monkeypatch ``sys.argv``
|
|
9
|
+
inline.
|
|
10
|
+
- Supplies JSON file factories that rely on ``tmp_path`` for automatic
|
|
11
|
+
cleanup.
|
|
12
|
+
- Keeps docstrings NumPy-formatted to satisfy numpydoc linting.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
from typing import Protocol
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
from requests import PreparedRequest # type: ignore[import]
|
|
26
|
+
|
|
27
|
+
from etlplus.cli import main
|
|
28
|
+
|
|
29
|
+
# SECTION: HELPERS ========================================================== #
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _coerce_cli_args(
|
|
33
|
+
cli_args: tuple[str | Sequence[str], ...],
|
|
34
|
+
) -> tuple[str, ...]:
|
|
35
|
+
"""
|
|
36
|
+
Normalize CLI arguments into a ``tuple[str, ...]``.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
cli_args : tuple[str | Sequence[str], ...]
|
|
41
|
+
Arguments provided to ``cli_runner``/``cli_invoke``.
|
|
42
|
+
|
|
43
|
+
Returns
|
|
44
|
+
-------
|
|
45
|
+
tuple[str, ...]
|
|
46
|
+
Normalized argument tuple safe to concatenate with ``sys.argv``.
|
|
47
|
+
"""
|
|
48
|
+
if (
|
|
49
|
+
len(cli_args) == 1
|
|
50
|
+
and isinstance(cli_args[0], Sequence)
|
|
51
|
+
and not isinstance(cli_args[0], (str, bytes))
|
|
52
|
+
):
|
|
53
|
+
return tuple(str(part) for part in cli_args[0])
|
|
54
|
+
return tuple(str(part) for part in cli_args)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CliInvoke(Protocol):
|
|
58
|
+
"""Protocol describing the :func:`cli_invoke` fixture."""
|
|
59
|
+
|
|
60
|
+
def __call__(
|
|
61
|
+
self,
|
|
62
|
+
*cli_args: str | Sequence[str],
|
|
63
|
+
) -> tuple[int, str, str]: ...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CliRunner(Protocol):
|
|
67
|
+
"""Protocol describing the ``cli_runner`` fixture."""
|
|
68
|
+
|
|
69
|
+
def __call__(self, *cli_args: str | Sequence[str]) -> int: ...
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class JsonFactory(Protocol):
|
|
73
|
+
"""Protocol describing the :func:`json_file_factory` fixture."""
|
|
74
|
+
|
|
75
|
+
def __call__(
|
|
76
|
+
self,
|
|
77
|
+
payload: Any,
|
|
78
|
+
*,
|
|
79
|
+
filename: str | None = None,
|
|
80
|
+
ensure_ascii: bool = False,
|
|
81
|
+
) -> Path: ...
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class RequestFactory(Protocol):
|
|
85
|
+
"""Protocol describing prepared-request factories."""
|
|
86
|
+
|
|
87
|
+
def __call__(
|
|
88
|
+
self,
|
|
89
|
+
url: str | None = None,
|
|
90
|
+
) -> PreparedRequest: ...
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# SECTION: FIXTURES ========================================================= #
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@pytest.fixture(name='base_url')
|
|
97
|
+
def base_url_fixture() -> str:
|
|
98
|
+
"""Return the canonical base URL shared across tests."""
|
|
99
|
+
|
|
100
|
+
return 'https://api.example.com'
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.fixture(name='json_file_factory')
|
|
104
|
+
def json_file_factory_fixture(
|
|
105
|
+
tmp_path: Path,
|
|
106
|
+
) -> JsonFactory:
|
|
107
|
+
"""
|
|
108
|
+
Create JSON files under ``tmp_path`` and return their paths.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
tmp_path : Path
|
|
113
|
+
Temporary directory managed by pytest.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
JsonFactory
|
|
118
|
+
Factory that persists the provided payload as JSON and returns the
|
|
119
|
+
resulting path.
|
|
120
|
+
|
|
121
|
+
Examples
|
|
122
|
+
--------
|
|
123
|
+
>>> path = json_file_factory({'name': 'Ada'})
|
|
124
|
+
>>> path.exists()
|
|
125
|
+
True
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def _create(
|
|
129
|
+
payload: Any,
|
|
130
|
+
*,
|
|
131
|
+
filename: str | None = None,
|
|
132
|
+
ensure_ascii: bool = False,
|
|
133
|
+
) -> Path:
|
|
134
|
+
target = tmp_path / (filename or 'payload.json')
|
|
135
|
+
data = (
|
|
136
|
+
payload
|
|
137
|
+
if isinstance(payload, str)
|
|
138
|
+
else json.dumps(payload, indent=2, ensure_ascii=ensure_ascii)
|
|
139
|
+
)
|
|
140
|
+
target.write_text(data)
|
|
141
|
+
return target
|
|
142
|
+
|
|
143
|
+
return _create
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@pytest.fixture(name='cli_runner')
|
|
147
|
+
def cli_runner_fixture(
|
|
148
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
149
|
+
) -> CliRunner:
|
|
150
|
+
"""
|
|
151
|
+
Invoke ``etlplus`` CLI commands with isolated ``sys.argv`` state.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
monkeypatch : pytest.MonkeyPatch
|
|
156
|
+
Built-in pytest fixture used to patch ``sys.argv``.
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
-------
|
|
160
|
+
CliRunner
|
|
161
|
+
Helper that accepts CLI arguments, runs :func:`etlplus.cli.main`, and
|
|
162
|
+
returns the exit code.
|
|
163
|
+
|
|
164
|
+
Examples
|
|
165
|
+
--------
|
|
166
|
+
>>> cli_runner(('extract', 'file', 'data.json'))
|
|
167
|
+
0
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def _run(*cli_args: str | Sequence[str]) -> int:
|
|
171
|
+
args = _coerce_cli_args(cli_args)
|
|
172
|
+
monkeypatch.setattr(sys, 'argv', ['etlplus', *args])
|
|
173
|
+
return main()
|
|
174
|
+
|
|
175
|
+
return _run
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@pytest.fixture
|
|
179
|
+
def cli_invoke(
|
|
180
|
+
cli_runner: CliRunner,
|
|
181
|
+
capsys: pytest.CaptureFixture[str],
|
|
182
|
+
) -> CliInvoke:
|
|
183
|
+
"""
|
|
184
|
+
Run CLI commands and return exit code, stdout, and stderr.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
cli_runner : CliRunner
|
|
189
|
+
Helper fixture defined above.
|
|
190
|
+
capsys : pytest.CaptureFixture[str]
|
|
191
|
+
Pytest fixture for capturing stdout/stderr.
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
CliInvoke
|
|
196
|
+
Helper that yields ``(exit_code, stdout, stderr)`` tuples.
|
|
197
|
+
|
|
198
|
+
Examples
|
|
199
|
+
--------
|
|
200
|
+
>>> code, out, err = cli_invoke(('extract', 'file', 'data.json'))
|
|
201
|
+
>>> code
|
|
202
|
+
0
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
def _invoke(*cli_args: str | Sequence[str]) -> tuple[int, str, str]:
|
|
206
|
+
exit_code = cli_runner(*cli_args)
|
|
207
|
+
captured = capsys.readouterr()
|
|
208
|
+
return exit_code, captured.out, captured.err
|
|
209
|
+
|
|
210
|
+
return _invoke
|
|
@@ -11,6 +11,7 @@ Notes
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import importlib
|
|
14
|
+
import json
|
|
14
15
|
import pathlib
|
|
15
16
|
from collections.abc import Callable
|
|
16
17
|
from typing import Any
|
|
@@ -34,6 +35,12 @@ from etlplus.config import PipelineConfig
|
|
|
34
35
|
# SECTION: HELPERS ========================================================== #
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
pytestmark = pytest.mark.integration
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# SECTION: HELPERS ========================================================== #
|
|
42
|
+
|
|
43
|
+
|
|
37
44
|
# Directory-level marker for integration tests.
|
|
38
45
|
pytestmark = pytest.mark.integration
|
|
39
46
|
|
|
@@ -55,8 +62,8 @@ class FakeEndpointClientProtocol(Protocol):
|
|
|
55
62
|
# SECTION: FIXTURES ========================================================= #
|
|
56
63
|
|
|
57
64
|
|
|
58
|
-
@pytest.fixture
|
|
59
|
-
def
|
|
65
|
+
@pytest.fixture(name='capture_load_to_api')
|
|
66
|
+
def capture_load_to_api_fixture(
|
|
60
67
|
monkeypatch: pytest.MonkeyPatch,
|
|
61
68
|
) -> dict[str, Any]:
|
|
62
69
|
"""
|
|
@@ -112,8 +119,8 @@ def capture_load_to_api(
|
|
|
112
119
|
return seen
|
|
113
120
|
|
|
114
121
|
|
|
115
|
-
@pytest.fixture
|
|
116
|
-
def
|
|
122
|
+
@pytest.fixture(name='fake_endpoint_client')
|
|
123
|
+
def fake_endpoint_client_fixture() -> tuple[
|
|
117
124
|
type[FakeEndpointClientProtocol],
|
|
118
125
|
list[FakeEndpointClientProtocol],
|
|
119
126
|
]: # noqa: ANN201
|
|
@@ -170,9 +177,83 @@ def fake_endpoint_client() -> tuple[
|
|
|
170
177
|
return FakeClient, created
|
|
171
178
|
|
|
172
179
|
|
|
173
|
-
@pytest.fixture
|
|
174
|
-
def
|
|
180
|
+
@pytest.fixture(name='file_to_api_pipeline_factory')
|
|
181
|
+
def file_to_api_pipeline_factory_fixture(
|
|
182
|
+
tmp_path: pathlib.Path,
|
|
183
|
+
base_url: str,
|
|
184
|
+
) -> Callable[..., PipelineConfig]:
|
|
185
|
+
"""Build a pipeline wiring a JSON file source to an API target."""
|
|
186
|
+
|
|
187
|
+
def _make(
|
|
188
|
+
*,
|
|
189
|
+
payload: Any | None = None,
|
|
190
|
+
base_url: str = base_url,
|
|
191
|
+
base_path: str | None = '/v1',
|
|
192
|
+
endpoint_path: str = '/ingest',
|
|
193
|
+
endpoint_name: str = 'ingest',
|
|
194
|
+
method: str = 'post',
|
|
195
|
+
headers: dict[str, str] | None = None,
|
|
196
|
+
job_name: str = 'send',
|
|
197
|
+
target_name: str = 'ingest_out',
|
|
198
|
+
) -> PipelineConfig:
|
|
199
|
+
source_path = tmp_path / f'{job_name}_input.json'
|
|
200
|
+
effective_payload = payload if payload is not None else {'ok': True}
|
|
201
|
+
text = (
|
|
202
|
+
effective_payload
|
|
203
|
+
if isinstance(effective_payload, str)
|
|
204
|
+
else json.dumps(effective_payload)
|
|
205
|
+
)
|
|
206
|
+
source_path.write_text(text, encoding='utf-8')
|
|
207
|
+
|
|
208
|
+
profile = ApiProfileConfig(
|
|
209
|
+
base_url=base_url,
|
|
210
|
+
headers={},
|
|
211
|
+
base_path=base_path or '',
|
|
212
|
+
auth={},
|
|
213
|
+
rate_limit_defaults=None,
|
|
214
|
+
pagination_defaults=None,
|
|
215
|
+
)
|
|
216
|
+
api = ApiConfig(
|
|
217
|
+
base_url=base_url,
|
|
218
|
+
profiles={'default': profile},
|
|
219
|
+
endpoints={endpoint_name: EndpointConfig(path=endpoint_path)},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
src = ConnectorFile(
|
|
223
|
+
name='file_src',
|
|
224
|
+
type='file',
|
|
225
|
+
format='json',
|
|
226
|
+
path=str(source_path),
|
|
227
|
+
)
|
|
228
|
+
tgt = ConnectorApi(
|
|
229
|
+
name=target_name,
|
|
230
|
+
type='api',
|
|
231
|
+
api='svc',
|
|
232
|
+
endpoint=endpoint_name,
|
|
233
|
+
method=method,
|
|
234
|
+
headers=headers or {},
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return PipelineConfig(
|
|
238
|
+
apis={'svc': api},
|
|
239
|
+
sources=[src],
|
|
240
|
+
targets=[tgt],
|
|
241
|
+
jobs=[
|
|
242
|
+
JobConfig(
|
|
243
|
+
name=job_name,
|
|
244
|
+
extract=ExtractRef(source='file_src'),
|
|
245
|
+
load=LoadRef(target=target_name),
|
|
246
|
+
),
|
|
247
|
+
],
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
return _make
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@pytest.fixture(name='pipeline_cfg_factory')
|
|
254
|
+
def pipeline_cfg_factory_fixture(
|
|
175
255
|
tmp_path: pathlib.Path,
|
|
256
|
+
base_url: str,
|
|
176
257
|
) -> Callable[..., PipelineConfig]:
|
|
177
258
|
"""
|
|
178
259
|
Factory to build a minimal PipelineConfig for runner tests.
|
|
@@ -185,6 +266,8 @@ def pipeline_cfg_factory(
|
|
|
185
266
|
----------
|
|
186
267
|
tmp_path : pathlib.Path
|
|
187
268
|
The pytest temporary path fixture.
|
|
269
|
+
base_url : str
|
|
270
|
+
Common base URL used across tests.
|
|
188
271
|
|
|
189
272
|
Returns
|
|
190
273
|
-------
|
|
@@ -196,9 +279,10 @@ def pipeline_cfg_factory(
|
|
|
196
279
|
*,
|
|
197
280
|
pagination_defaults: PaginationConfig | None = None,
|
|
198
281
|
rate_limit_defaults: RateLimitConfig | None = None,
|
|
282
|
+
extract_options: dict[str, Any] | None = None,
|
|
199
283
|
) -> PipelineConfig:
|
|
200
284
|
prof = ApiProfileConfig(
|
|
201
|
-
base_url=
|
|
285
|
+
base_url=base_url,
|
|
202
286
|
headers={},
|
|
203
287
|
base_path='/v1',
|
|
204
288
|
auth={},
|
|
@@ -219,24 +303,29 @@ def pipeline_cfg_factory(
|
|
|
219
303
|
format='json',
|
|
220
304
|
path=str(out_path),
|
|
221
305
|
)
|
|
306
|
+
job = JobConfig(
|
|
307
|
+
name='job',
|
|
308
|
+
extract=ExtractRef(source='s'),
|
|
309
|
+
load=LoadRef(target='t'),
|
|
310
|
+
)
|
|
311
|
+
if extract_options is not None:
|
|
312
|
+
if job.extract is None:
|
|
313
|
+
msg = 'job.extract is None; cannot set options'
|
|
314
|
+
raise ValueError(msg)
|
|
315
|
+
job.extract.options = extract_options
|
|
316
|
+
|
|
222
317
|
return PipelineConfig(
|
|
223
318
|
apis={'svc': api},
|
|
224
319
|
sources=[src],
|
|
225
320
|
targets=[tgt],
|
|
226
|
-
jobs=[
|
|
227
|
-
JobConfig(
|
|
228
|
-
name='job',
|
|
229
|
-
extract=ExtractRef(source='s'),
|
|
230
|
-
load=LoadRef(target='t'),
|
|
231
|
-
),
|
|
232
|
-
],
|
|
321
|
+
jobs=[job],
|
|
233
322
|
)
|
|
234
323
|
|
|
235
324
|
return _make
|
|
236
325
|
|
|
237
326
|
|
|
238
|
-
@pytest.fixture
|
|
239
|
-
def
|
|
327
|
+
@pytest.fixture(name='run_patched')
|
|
328
|
+
def run_patched_fixture(
|
|
240
329
|
monkeypatch: pytest.MonkeyPatch,
|
|
241
330
|
) -> Callable[..., dict[str, Any]]:
|
|
242
331
|
"""
|