pyintake 0.0.1__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.
- pyintake-0.0.1/.github/workflows/publish.yml +70 -0
- pyintake-0.0.1/.github/workflows/test.yml +39 -0
- pyintake-0.0.1/Makefile +110 -0
- pyintake-0.0.1/PKG-INFO +47 -0
- pyintake-0.0.1/pyproject.toml +176 -0
- pyintake-0.0.1/setup.cfg +4 -0
- pyintake-0.0.1/src/pyintake/__init__.py +83 -0
- pyintake-0.0.1/src/pyintake/__pycache__/__init__.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/__pycache__/_protocols.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/__pycache__/_types.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/__pycache__/errors.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/__pycache__/event.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/_protocols.py +145 -0
- pyintake-0.0.1/src/pyintake/_types.py +135 -0
- pyintake-0.0.1/src/pyintake/adapters/__init__.py +6 -0
- pyintake-0.0.1/src/pyintake/api/__init__.py +3 -0
- pyintake-0.0.1/src/pyintake/api/routes/__init__.py +3 -0
- pyintake-0.0.1/src/pyintake/api/routes/v1/__init__.py +3 -0
- pyintake-0.0.1/src/pyintake/buffer/__init__.py +6 -0
- pyintake-0.0.1/src/pyintake/buffer/__pycache__/__init__.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/buffer/__pycache__/_dedup.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/buffer/__pycache__/_priority_buffer.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/buffer/_dedup.py +110 -0
- pyintake-0.0.1/src/pyintake/buffer/_priority_buffer.py +191 -0
- pyintake-0.0.1/src/pyintake/config/__init__.py +3 -0
- pyintake-0.0.1/src/pyintake/core/__init__.py +6 -0
- pyintake-0.0.1/src/pyintake/core/__pycache__/__init__.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/core/__pycache__/_drain.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/core/__pycache__/_engine.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/core/__pycache__/_rate_limiter.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/core/_drain.py +248 -0
- pyintake-0.0.1/src/pyintake/core/_engine.py +248 -0
- pyintake-0.0.1/src/pyintake/core/_rate_limiter.py +87 -0
- pyintake-0.0.1/src/pyintake/errors.py +205 -0
- pyintake-0.0.1/src/pyintake/event.py +244 -0
- pyintake-0.0.1/src/pyintake/metrics/__init__.py +7 -0
- pyintake-0.0.1/src/pyintake/metrics/__pycache__/__init__.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/metrics/__pycache__/_collector.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/metrics/__pycache__/_models.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/metrics/_collector.py +196 -0
- pyintake-0.0.1/src/pyintake/metrics/_models.py +85 -0
- pyintake-0.0.1/src/pyintake/py.typed +0 -0
- pyintake-0.0.1/src/pyintake/validation/__init__.py +6 -0
- pyintake-0.0.1/src/pyintake/validation/__pycache__/__init__.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/validation/__pycache__/_chain.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/validation/__pycache__/_validators.cpython-313.pyc +0 -0
- pyintake-0.0.1/src/pyintake/validation/_chain.py +82 -0
- pyintake-0.0.1/src/pyintake/validation/_validators.py +181 -0
- pyintake-0.0.1/src/pyintake.egg-info/PKG-INFO +47 -0
- pyintake-0.0.1/src/pyintake.egg-info/SOURCES.txt +77 -0
- pyintake-0.0.1/src/pyintake.egg-info/dependency_links.txt +1 -0
- pyintake-0.0.1/src/pyintake.egg-info/entry_points.txt +2 -0
- pyintake-0.0.1/src/pyintake.egg-info/requires.txt +33 -0
- pyintake-0.0.1/src/pyintake.egg-info/top_level.txt +1 -0
- pyintake-0.0.1/tests/__init__.py +0 -0
- pyintake-0.0.1/tests/__pycache__/__init__.cpython-313.pyc +0 -0
- pyintake-0.0.1/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc +0 -0
- pyintake-0.0.1/tests/conftest.py +116 -0
- pyintake-0.0.1/tests/integration/__init__.py +0 -0
- pyintake-0.0.1/tests/unit/__init__.py +0 -0
- pyintake-0.0.1/tests/unit/__pycache__/__init__.cpython-313.pyc +0 -0
- pyintake-0.0.1/tests/unit/__pycache__/test_dedup.cpython-313-pytest-9.0.2.pyc +0 -0
- pyintake-0.0.1/tests/unit/__pycache__/test_drain.cpython-313-pytest-9.0.2.pyc +0 -0
- pyintake-0.0.1/tests/unit/__pycache__/test_engine.cpython-313-pytest-9.0.2.pyc +0 -0
- pyintake-0.0.1/tests/unit/__pycache__/test_event.cpython-313-pytest-9.0.2.pyc +0 -0
- pyintake-0.0.1/tests/unit/__pycache__/test_metrics.cpython-313-pytest-9.0.2.pyc +0 -0
- pyintake-0.0.1/tests/unit/__pycache__/test_priority_buffer.cpython-313-pytest-9.0.2.pyc +0 -0
- pyintake-0.0.1/tests/unit/__pycache__/test_rate_limiter.cpython-313-pytest-9.0.2.pyc +0 -0
- pyintake-0.0.1/tests/unit/__pycache__/test_validation_chain.cpython-313-pytest-9.0.2.pyc +0 -0
- pyintake-0.0.1/tests/unit/__pycache__/test_validators.cpython-313-pytest-9.0.2.pyc +0 -0
- pyintake-0.0.1/tests/unit/test_dedup.py +135 -0
- pyintake-0.0.1/tests/unit/test_drain.py +338 -0
- pyintake-0.0.1/tests/unit/test_engine.py +228 -0
- pyintake-0.0.1/tests/unit/test_event.py +301 -0
- pyintake-0.0.1/tests/unit/test_metrics.py +215 -0
- pyintake-0.0.1/tests/unit/test_priority_buffer.py +224 -0
- pyintake-0.0.1/tests/unit/test_rate_limiter.py +103 -0
- pyintake-0.0.1/tests/unit/test_validation_chain.py +167 -0
- pyintake-0.0.1/tests/unit/test_validators.py +200 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Build and publish to PyPI when a version tag (v*) is pushed.
|
|
2
|
+
# Also supports manual run (workflow_dispatch).
|
|
3
|
+
# Version is derived from the git tag by setuptools-scm.
|
|
4
|
+
|
|
5
|
+
name: Build and Publish
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
tags: ['v*']
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write
|
|
14
|
+
contents: write
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
build-and-publish:
|
|
18
|
+
name: Build and Publish to PyPI
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- name: Checkout
|
|
22
|
+
uses: actions/checkout@v4
|
|
23
|
+
with:
|
|
24
|
+
fetch-depth: 0
|
|
25
|
+
ref: ${{ github.event_name == 'release' && github.event.release.tag_name || github.ref }}
|
|
26
|
+
|
|
27
|
+
- name: Set version from tag
|
|
28
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
29
|
+
run: echo "SETUPTOOLS_SCM_PRETEND_VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
|
|
30
|
+
|
|
31
|
+
- name: Set up Python
|
|
32
|
+
uses: actions/setup-python@v5
|
|
33
|
+
with:
|
|
34
|
+
python-version: '3.12'
|
|
35
|
+
cache: 'pip'
|
|
36
|
+
cache-dependency-path: pyproject.toml
|
|
37
|
+
|
|
38
|
+
- name: Install dependencies and run tests
|
|
39
|
+
run: |
|
|
40
|
+
python -m pip install --upgrade pip
|
|
41
|
+
pip install -e ".[dev]"
|
|
42
|
+
pytest
|
|
43
|
+
|
|
44
|
+
- name: Install build tools
|
|
45
|
+
run: pip install build
|
|
46
|
+
|
|
47
|
+
- name: Build package
|
|
48
|
+
run: python -m build
|
|
49
|
+
|
|
50
|
+
- name: Verify package
|
|
51
|
+
run: |
|
|
52
|
+
python -m zipfile -l dist/pyintake-*.whl | head -30
|
|
53
|
+
echo "Package built successfully"
|
|
54
|
+
|
|
55
|
+
# Only publish to PyPI when building from a tag (official release). Skip for workflow_dispatch so we don't publish dev versions as pre-release.
|
|
56
|
+
- name: Publish to PyPI
|
|
57
|
+
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'release'
|
|
58
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
59
|
+
with:
|
|
60
|
+
packages-dir: dist/
|
|
61
|
+
|
|
62
|
+
- name: Create GitHub Release
|
|
63
|
+
if: startsWith(github.ref, 'refs/tags/') && github.event_name != 'release'
|
|
64
|
+
uses: softprops/action-gh-release@v2.4.2
|
|
65
|
+
with:
|
|
66
|
+
tag_name: ${{ github.ref_name }}
|
|
67
|
+
name: ${{ github.ref_name }}
|
|
68
|
+
generate_release_notes: true
|
|
69
|
+
env:
|
|
70
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [main, develop]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ['3.11', '3.12', '3.13']
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
cache: 'pip'
|
|
23
|
+
cache-dependency-path: pyproject.toml
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: |
|
|
27
|
+
python -m pip install --upgrade pip
|
|
28
|
+
pip install -e ".[dev]"
|
|
29
|
+
|
|
30
|
+
- name: Run tests
|
|
31
|
+
run: pytest
|
|
32
|
+
|
|
33
|
+
- name: Run linting
|
|
34
|
+
run: |
|
|
35
|
+
ruff format --check src/pyintake tests
|
|
36
|
+
ruff check src/pyintake tests
|
|
37
|
+
|
|
38
|
+
- name: Run type checking
|
|
39
|
+
run: mypy src/pyintake || true
|
pyintake-0.0.1/Makefile
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
.PHONY: help install install-dev test test-verbose test-cov test-fast test-unit test-integration clean build format lint type-check check publish-test publish docs docs-serve
|
|
2
|
+
|
|
3
|
+
# Detect if venv exists and set PYTHON/PIP accordingly
|
|
4
|
+
ifeq ($(wildcard venv/bin/python),)
|
|
5
|
+
PYTHON := python3
|
|
6
|
+
PIP := pip3
|
|
7
|
+
PYTHON_CMD := python3 -m
|
|
8
|
+
else
|
|
9
|
+
PYTHON := venv/bin/python
|
|
10
|
+
PIP := venv/bin/pip
|
|
11
|
+
PYTHON_CMD := venv/bin/python -m
|
|
12
|
+
endif
|
|
13
|
+
|
|
14
|
+
help: ## Show this help message
|
|
15
|
+
@echo "Available commands:"
|
|
16
|
+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
|
|
17
|
+
@echo ""
|
|
18
|
+
@echo "Publishing:"
|
|
19
|
+
@echo " For production: Create a GitHub Release (automatically publishes via Trusted Publishing)"
|
|
20
|
+
|
|
21
|
+
install: ## Install the package in development mode
|
|
22
|
+
python3 -m venv venv
|
|
23
|
+
. venv/bin/activate && pip install --upgrade pip && pip install -e .
|
|
24
|
+
|
|
25
|
+
install-dev: ## Install package + all development dependencies
|
|
26
|
+
python3 -m venv venv
|
|
27
|
+
. venv/bin/activate && pip install --upgrade pip && pip install -e ".[dev]"
|
|
28
|
+
@echo "To activate: source venv/bin/activate"
|
|
29
|
+
|
|
30
|
+
test: ## Run tests
|
|
31
|
+
$(PYTHON_CMD) pytest
|
|
32
|
+
|
|
33
|
+
test-verbose: ## Run tests with verbose output
|
|
34
|
+
$(PYTHON_CMD) pytest -v
|
|
35
|
+
|
|
36
|
+
test-cov: ## Run tests with coverage
|
|
37
|
+
$(PYTHON_CMD) pytest --cov=pyintake --cov-report=html --cov-report=term
|
|
38
|
+
|
|
39
|
+
test-fast: ## Run fast tests only (exclude slow markers)
|
|
40
|
+
$(PYTHON_CMD) pytest -m "not slow"
|
|
41
|
+
|
|
42
|
+
test-unit: ## Run unit tests only
|
|
43
|
+
$(PYTHON_CMD) pytest -m "unit"
|
|
44
|
+
|
|
45
|
+
test-integration: ## Run integration tests only
|
|
46
|
+
$(PYTHON_CMD) pytest -m "integration"
|
|
47
|
+
|
|
48
|
+
clean: ## Clean build artifacts
|
|
49
|
+
rm -rf build/ dist/ *.egg-info src/*.egg-info src/pyintake.egg-info
|
|
50
|
+
rm -rf .pytest_cache .mypy_cache .ruff_cache htmlcov/ .coverage
|
|
51
|
+
find . -type d -name __pycache__ -exec rm -r {} + 2>/dev/null || true
|
|
52
|
+
find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
|
53
|
+
|
|
54
|
+
build: ## Build distribution packages
|
|
55
|
+
@if ! $(PYTHON) -c "import build" 2>/dev/null; then \
|
|
56
|
+
echo "Installing build..."; \
|
|
57
|
+
$(PIP) install build; \
|
|
58
|
+
fi
|
|
59
|
+
$(PYTHON_CMD) build
|
|
60
|
+
|
|
61
|
+
format: ## Format code with ruff
|
|
62
|
+
$(PYTHON_CMD) ruff format src/pyintake tests
|
|
63
|
+
$(PYTHON_CMD) ruff check --fix src/pyintake tests
|
|
64
|
+
|
|
65
|
+
lint: ## Run linting checks (same as CI)
|
|
66
|
+
$(PYTHON_CMD) ruff format --check src/pyintake tests
|
|
67
|
+
$(PYTHON_CMD) ruff check src/pyintake tests
|
|
68
|
+
|
|
69
|
+
type-check: ## Run type checking with mypy
|
|
70
|
+
$(PYTHON_CMD) mypy src/pyintake
|
|
71
|
+
|
|
72
|
+
check: format lint type-check test ## Run all checks (format, lint, type-check, test)
|
|
73
|
+
|
|
74
|
+
docs: ## Build documentation
|
|
75
|
+
$(PYTHON_CMD) mkdocs build
|
|
76
|
+
|
|
77
|
+
docs-serve: ## Serve documentation locally
|
|
78
|
+
$(PYTHON_CMD) mkdocs serve
|
|
79
|
+
|
|
80
|
+
publish-test: clean build ## Build and upload to TestPyPI (requires TWINE_PASSWORD)
|
|
81
|
+
@echo "Uploading to TestPyPI..."
|
|
82
|
+
@if ! $(PYTHON) -c "import twine" 2>/dev/null; then \
|
|
83
|
+
echo "Installing twine..."; \
|
|
84
|
+
$(PIP) install twine; \
|
|
85
|
+
fi
|
|
86
|
+
@if [ -z "$$TWINE_PASSWORD" ]; then \
|
|
87
|
+
echo "Warning: TWINE_PASSWORD not set. Set it with:"; \
|
|
88
|
+
echo " export TWINE_PASSWORD=your-testpypi-token"; \
|
|
89
|
+
exit 1; \
|
|
90
|
+
fi
|
|
91
|
+
$(PYTHON_CMD) twine upload --repository testpypi dist/*
|
|
92
|
+
@echo "Published to TestPyPI"
|
|
93
|
+
@echo "Test install: pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ pyintake"
|
|
94
|
+
|
|
95
|
+
publish: clean build ## Build and upload to PyPI (use GitHub Releases for production)
|
|
96
|
+
@echo "WARNING: This will publish to PyPI!"
|
|
97
|
+
@echo "For production releases, use GitHub Releases instead (automatic via Trusted Publishing)"
|
|
98
|
+
@echo "Press Ctrl+C to cancel, or Enter to continue..."
|
|
99
|
+
@read dummy
|
|
100
|
+
@if ! $(PYTHON) -c "import twine" 2>/dev/null; then \
|
|
101
|
+
echo "Installing twine..."; \
|
|
102
|
+
$(PIP) install twine; \
|
|
103
|
+
fi
|
|
104
|
+
@if [ -z "$$TWINE_PASSWORD" ]; then \
|
|
105
|
+
echo "Warning: TWINE_PASSWORD not set. Set it with:"; \
|
|
106
|
+
echo " export TWINE_PASSWORD=your-pypi-token"; \
|
|
107
|
+
exit 1; \
|
|
108
|
+
fi
|
|
109
|
+
$(PYTHON_CMD) twine upload dist/*
|
|
110
|
+
@echo "Published to PyPI"
|
pyintake-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyintake
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Transport-agnostic data ingestion system with priority buffering and backpressure
|
|
5
|
+
Author-email: StatFYI <contact@statfyi.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/optophi/pyintake
|
|
8
|
+
Project-URL: Repository, https://github.com/optophi/pyintake
|
|
9
|
+
Project-URL: Issues, https://github.com/optophi/pyintake/issues
|
|
10
|
+
Keywords: ingestion,data-pipeline,buffering,backpressure,event-driven
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Provides-Extra: http
|
|
23
|
+
Requires-Dist: fastapi>=0.104.0; extra == "http"
|
|
24
|
+
Requires-Dist: uvicorn[standard]>=0.24.0; extra == "http"
|
|
25
|
+
Requires-Dist: pydantic>=2.0.0; extra == "http"
|
|
26
|
+
Provides-Extra: kafka
|
|
27
|
+
Requires-Dist: aiokafka>=0.10.0; extra == "kafka"
|
|
28
|
+
Provides-Extra: websocket
|
|
29
|
+
Requires-Dist: websockets>=12.0; extra == "websocket"
|
|
30
|
+
Provides-Extra: api
|
|
31
|
+
Requires-Dist: pyintake[http]; extra == "api"
|
|
32
|
+
Requires-Dist: pydantic-settings>=2.0.0; extra == "api"
|
|
33
|
+
Provides-Extra: config
|
|
34
|
+
Requires-Dist: pyyaml>=6.0.0; extra == "config"
|
|
35
|
+
Requires-Dist: pydantic>=2.0.0; extra == "config"
|
|
36
|
+
Provides-Extra: all
|
|
37
|
+
Requires-Dist: pyintake[api,config,kafka,websocket]; extra == "all"
|
|
38
|
+
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: pyintake[all]; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
41
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
42
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
43
|
+
Requires-Dist: ruff>=0.9.0; extra == "dev"
|
|
44
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
45
|
+
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|
46
|
+
Requires-Dist: httpx>=0.24.0; extra == "dev"
|
|
47
|
+
Requires-Dist: build>=0.10.0; extra == "dev"
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel", "setuptools-scm>=8.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyintake"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Transport-agnostic data ingestion system with priority buffering and backpressure"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = []
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "StatFYI", email = "contact@statfyi.com" },
|
|
15
|
+
]
|
|
16
|
+
keywords = [
|
|
17
|
+
"ingestion",
|
|
18
|
+
"data-pipeline",
|
|
19
|
+
"buffering",
|
|
20
|
+
"backpressure",
|
|
21
|
+
"event-driven",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 3 - Alpha",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Programming Language :: Python :: 3.13",
|
|
31
|
+
"Typing :: Typed",
|
|
32
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/optophi/pyintake"
|
|
37
|
+
Repository = "https://github.com/optophi/pyintake"
|
|
38
|
+
Issues = "https://github.com/optophi/pyintake/issues"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools_scm]
|
|
41
|
+
local_scheme = "no-local-version"
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Install extras:
|
|
46
|
+
# pip install pyintake -> core library only (zero deps)
|
|
47
|
+
# pip install pyintake[http] -> + FastAPI HTTP adapter
|
|
48
|
+
# pip install pyintake[kafka] -> + aiokafka Kafka adapter
|
|
49
|
+
# pip install pyintake[websocket] -> + websockets adapter
|
|
50
|
+
# pip install pyintake[api] -> + REST API server
|
|
51
|
+
# pip install pyintake[all] -> api + kafka + websocket
|
|
52
|
+
# pip install pyintake[dev] -> all + testing & dev tools
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
http = [
|
|
56
|
+
"fastapi>=0.104.0",
|
|
57
|
+
"uvicorn[standard]>=0.24.0",
|
|
58
|
+
"pydantic>=2.0.0",
|
|
59
|
+
]
|
|
60
|
+
kafka = [
|
|
61
|
+
"aiokafka>=0.10.0",
|
|
62
|
+
]
|
|
63
|
+
websocket = [
|
|
64
|
+
"websockets>=12.0",
|
|
65
|
+
]
|
|
66
|
+
api = [
|
|
67
|
+
"pyintake[http]",
|
|
68
|
+
"pydantic-settings>=2.0.0",
|
|
69
|
+
]
|
|
70
|
+
config = [
|
|
71
|
+
"pyyaml>=6.0.0",
|
|
72
|
+
"pydantic>=2.0.0",
|
|
73
|
+
]
|
|
74
|
+
all = [
|
|
75
|
+
"pyintake[api,kafka,websocket,config]",
|
|
76
|
+
]
|
|
77
|
+
dev = [
|
|
78
|
+
"pyintake[all]",
|
|
79
|
+
"pytest>=7.0.0",
|
|
80
|
+
"pytest-asyncio>=0.21.0",
|
|
81
|
+
"pytest-cov>=4.0.0",
|
|
82
|
+
"ruff>=0.9.0",
|
|
83
|
+
"mypy>=1.0.0",
|
|
84
|
+
"pre-commit>=3.0.0",
|
|
85
|
+
"httpx>=0.24.0",
|
|
86
|
+
"build>=0.10.0",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
[tool.setuptools.packages.find]
|
|
90
|
+
where = ["src"]
|
|
91
|
+
include = ["pyintake*"]
|
|
92
|
+
|
|
93
|
+
[tool.setuptools.package-data]
|
|
94
|
+
pyintake = [
|
|
95
|
+
"py.typed",
|
|
96
|
+
"data/seed/*.yaml",
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
[project.scripts]
|
|
100
|
+
pyintake = "pyintake.cli:main"
|
|
101
|
+
|
|
102
|
+
# =============================================================================
|
|
103
|
+
# Tool Configuration
|
|
104
|
+
# =============================================================================
|
|
105
|
+
|
|
106
|
+
[tool.ruff]
|
|
107
|
+
target-version = "py311"
|
|
108
|
+
line-length = 88
|
|
109
|
+
src = ["src", "tests"]
|
|
110
|
+
|
|
111
|
+
[tool.ruff.lint]
|
|
112
|
+
select = ["E", "W", "F", "I", "B", "C4", "UP"]
|
|
113
|
+
ignore = [
|
|
114
|
+
"E501",
|
|
115
|
+
"B008",
|
|
116
|
+
"B904",
|
|
117
|
+
"B028",
|
|
118
|
+
"B007",
|
|
119
|
+
"B017",
|
|
120
|
+
"E402",
|
|
121
|
+
"E731",
|
|
122
|
+
"B027",
|
|
123
|
+
"B905",
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
[tool.ruff.lint.per-file-ignores]
|
|
127
|
+
"__init__.py" = ["F401"]
|
|
128
|
+
"tests/**" = ["B904", "B017", "E402"]
|
|
129
|
+
|
|
130
|
+
[tool.ruff.lint.isort]
|
|
131
|
+
known-first-party = ["pyintake"]
|
|
132
|
+
|
|
133
|
+
[tool.ruff.format]
|
|
134
|
+
quote-style = "double"
|
|
135
|
+
|
|
136
|
+
[tool.mypy]
|
|
137
|
+
python_version = "3.11"
|
|
138
|
+
warn_return_any = true
|
|
139
|
+
warn_unused_configs = true
|
|
140
|
+
disallow_untyped_defs = false
|
|
141
|
+
ignore_missing_imports = true
|
|
142
|
+
|
|
143
|
+
[tool.pytest.ini_options]
|
|
144
|
+
minversion = "7.0"
|
|
145
|
+
testpaths = ["tests"]
|
|
146
|
+
python_files = ["test_*.py"]
|
|
147
|
+
python_classes = ["Test*"]
|
|
148
|
+
python_functions = ["test_*"]
|
|
149
|
+
asyncio_mode = "auto"
|
|
150
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
151
|
+
addopts = [
|
|
152
|
+
"--strict-markers",
|
|
153
|
+
"--tb=short",
|
|
154
|
+
]
|
|
155
|
+
markers = [
|
|
156
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
157
|
+
"integration: marks tests as integration tests",
|
|
158
|
+
"unit: marks tests as unit tests",
|
|
159
|
+
"asyncio: mark test as async (pytest-asyncio)",
|
|
160
|
+
]
|
|
161
|
+
filterwarnings = [
|
|
162
|
+
"error",
|
|
163
|
+
"ignore::UserWarning",
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
[tool.coverage.run]
|
|
167
|
+
source = ["pyintake"]
|
|
168
|
+
|
|
169
|
+
[tool.coverage.report]
|
|
170
|
+
show_missing = true
|
|
171
|
+
skip_empty = true
|
|
172
|
+
exclude_lines = [
|
|
173
|
+
"pragma: no cover",
|
|
174
|
+
"if TYPE_CHECKING:",
|
|
175
|
+
"if __name__ == .__main__.",
|
|
176
|
+
]
|
pyintake-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""PyIntake — Transport-agnostic data ingestion system.
|
|
2
|
+
|
|
3
|
+
Collects, validates, deduplicates, and buffers events from any transport,
|
|
4
|
+
then drains them into pluggable downstream consumers with priority ordering
|
|
5
|
+
and backpressure signaling.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pyintake._protocols import Consumer, MetricsCollector, TransportAdapter, Validator
|
|
11
|
+
from pyintake._types import (
|
|
12
|
+
BackpressureSignal,
|
|
13
|
+
BufferEntry,
|
|
14
|
+
ConsumeResult,
|
|
15
|
+
IngestResult,
|
|
16
|
+
Priority,
|
|
17
|
+
ValidationOutcome,
|
|
18
|
+
)
|
|
19
|
+
from pyintake.buffer._dedup import SlidingWindowDedup
|
|
20
|
+
from pyintake.buffer._priority_buffer import PriorityBuffer
|
|
21
|
+
from pyintake.core._drain import AdaptiveDrainLoop
|
|
22
|
+
from pyintake.core._engine import IngestionEngine
|
|
23
|
+
from pyintake.core._rate_limiter import TokenBucketRateLimiter
|
|
24
|
+
from pyintake.errors import (
|
|
25
|
+
AdapterError,
|
|
26
|
+
BufferFullError,
|
|
27
|
+
ConfigError,
|
|
28
|
+
ConsumerError,
|
|
29
|
+
DrainError,
|
|
30
|
+
DuplicateEventError,
|
|
31
|
+
ErrorCode,
|
|
32
|
+
PyIntakeError,
|
|
33
|
+
RateLimitedError,
|
|
34
|
+
ShutdownTimeoutError,
|
|
35
|
+
ValidationError,
|
|
36
|
+
)
|
|
37
|
+
from pyintake.event import IngestEvent
|
|
38
|
+
from pyintake.metrics._collector import InMemoryMetricsCollector
|
|
39
|
+
from pyintake.metrics._models import MetricsSnapshot, SourceMetrics
|
|
40
|
+
from pyintake.validation._chain import ValidationChain
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
# Core event type
|
|
44
|
+
"IngestEvent",
|
|
45
|
+
# Value types and enums
|
|
46
|
+
"Priority",
|
|
47
|
+
"BackpressureSignal",
|
|
48
|
+
"IngestResult",
|
|
49
|
+
"ValidationOutcome",
|
|
50
|
+
"ConsumeResult",
|
|
51
|
+
"BufferEntry",
|
|
52
|
+
# Protocols
|
|
53
|
+
"TransportAdapter",
|
|
54
|
+
"Validator",
|
|
55
|
+
"Consumer",
|
|
56
|
+
"MetricsCollector",
|
|
57
|
+
# Engine and drain
|
|
58
|
+
"IngestionEngine",
|
|
59
|
+
"AdaptiveDrainLoop",
|
|
60
|
+
# Buffer
|
|
61
|
+
"PriorityBuffer",
|
|
62
|
+
"SlidingWindowDedup",
|
|
63
|
+
# Validation
|
|
64
|
+
"ValidationChain",
|
|
65
|
+
# Rate limiting
|
|
66
|
+
"TokenBucketRateLimiter",
|
|
67
|
+
# Metrics
|
|
68
|
+
"InMemoryMetricsCollector",
|
|
69
|
+
"MetricsSnapshot",
|
|
70
|
+
"SourceMetrics",
|
|
71
|
+
# Errors
|
|
72
|
+
"PyIntakeError",
|
|
73
|
+
"ErrorCode",
|
|
74
|
+
"BufferFullError",
|
|
75
|
+
"ValidationError",
|
|
76
|
+
"DuplicateEventError",
|
|
77
|
+
"RateLimitedError",
|
|
78
|
+
"AdapterError",
|
|
79
|
+
"DrainError",
|
|
80
|
+
"ConfigError",
|
|
81
|
+
"ConsumerError",
|
|
82
|
+
"ShutdownTimeoutError",
|
|
83
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Protocol definitions for PyIntake pluggable interfaces.
|
|
2
|
+
|
|
3
|
+
Protocols define the contracts for transport adapters, validators,
|
|
4
|
+
consumers, and metrics collectors. Implementations can be swapped
|
|
5
|
+
in via dependency injection without coupling to concrete classes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pyintake._types import ConsumeResult, ValidationOutcome
|
|
14
|
+
from pyintake.event import IngestEvent
|
|
15
|
+
from pyintake.metrics._models import MetricsSnapshot
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Transport adapter protocol
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@runtime_checkable
|
|
24
|
+
class TransportAdapter(Protocol):
|
|
25
|
+
"""Receives events from a transport and feeds them into IngestionEngine.
|
|
26
|
+
|
|
27
|
+
Implementations handle transport-specific concerns (Kafka consumer,
|
|
28
|
+
HTTP server, file watcher, etc.) and call engine.ingest() for each event.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
async def start(self, engine: Any) -> None:
|
|
32
|
+
"""Start the adapter and begin feeding events to the engine.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
engine: The IngestionEngine to feed events into.
|
|
36
|
+
"""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
async def stop(self) -> None:
|
|
40
|
+
"""Stop the adapter and release transport resources."""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Validator protocol
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@runtime_checkable
|
|
50
|
+
class Validator(Protocol):
|
|
51
|
+
"""Validates an IngestEvent before it enters the buffer.
|
|
52
|
+
|
|
53
|
+
Validators run in the ingestion path (before buffering), so they
|
|
54
|
+
must be fast and synchronous. Heavy validation should be done
|
|
55
|
+
downstream by consumers.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def name(self) -> str:
|
|
60
|
+
"""Human-readable name for metrics and logging."""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
def validate(self, event: IngestEvent) -> ValidationOutcome:
|
|
64
|
+
"""Validate a single event.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
event: The event to validate.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Outcome indicating whether the event is valid.
|
|
71
|
+
"""
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Consumer protocol
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@runtime_checkable
|
|
81
|
+
class Consumer(Protocol):
|
|
82
|
+
"""Downstream consumer that receives drained event batches.
|
|
83
|
+
|
|
84
|
+
Consumers are the output side of the ingestion pipeline. They
|
|
85
|
+
receive batches of validated, deduplicated events from the
|
|
86
|
+
drain loop. Examples: pystator orchestrator, pycharter validator,
|
|
87
|
+
file collector for offline analysis.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
async def consume(self, batch: list[IngestEvent]) -> ConsumeResult:
|
|
91
|
+
"""Process a batch of events.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
batch: List of events drained from the buffer.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Result indicating success/failure and events processed.
|
|
98
|
+
"""
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
async def close(self) -> None:
|
|
102
|
+
"""Release any resources held by the consumer."""
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
# Metrics collector protocol
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@runtime_checkable
|
|
112
|
+
class MetricsCollector(Protocol):
|
|
113
|
+
"""Collects ingestion metrics programmatically.
|
|
114
|
+
|
|
115
|
+
All methods are synchronous and must be fast — they are called
|
|
116
|
+
in the hot ingestion path.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def record_accepted(self, event: IngestEvent) -> None:
|
|
120
|
+
"""Record a successfully accepted event."""
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
def record_rejected(self, event: IngestEvent, reason: str) -> None:
|
|
124
|
+
"""Record a rejected event with the rejection reason."""
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
def record_drained(self, count: int, sources: list[str] | None = None) -> None:
|
|
128
|
+
"""Record that count events were drained to consumers."""
|
|
129
|
+
...
|
|
130
|
+
|
|
131
|
+
def record_deduplicated(self, event: IngestEvent) -> None:
|
|
132
|
+
"""Record a deduplicated (duplicate-rejected) event."""
|
|
133
|
+
...
|
|
134
|
+
|
|
135
|
+
def record_shed(self, event: IngestEvent) -> None:
|
|
136
|
+
"""Record an event shed due to buffer pressure."""
|
|
137
|
+
...
|
|
138
|
+
|
|
139
|
+
def record_dead_letter(self, event: IngestEvent) -> None:
|
|
140
|
+
"""Record an event that exceeded max retries."""
|
|
141
|
+
...
|
|
142
|
+
|
|
143
|
+
def snapshot(self) -> MetricsSnapshot:
|
|
144
|
+
"""Return a frozen point-in-time snapshot of all metrics."""
|
|
145
|
+
...
|