data-ingestor 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data_ingestor-0.2.0/.github/dependabot.yml +23 -0
- data_ingestor-0.2.0/.github/workflows/checks.yml +72 -0
- data_ingestor-0.2.0/.github/workflows/codeql.yml +30 -0
- data_ingestor-0.2.0/.github/workflows/publish.yml +73 -0
- data_ingestor-0.2.0/.github/workflows/tests.yml +32 -0
- data_ingestor-0.2.0/.gitignore +29 -0
- data_ingestor-0.2.0/.python-version +1 -0
- data_ingestor-0.2.0/.vscode/settings.json +39 -0
- data_ingestor-0.2.0/Makefile +65 -0
- data_ingestor-0.2.0/PKG-INFO +162 -0
- data_ingestor-0.2.0/README.md +150 -0
- data_ingestor-0.2.0/docker-compose.yml +24 -0
- data_ingestor-0.2.0/docs/adapters.md +94 -0
- data_ingestor-0.2.0/docs/architecture.md +60 -0
- data_ingestor-0.2.0/docs/diagrams/ingestor-architecture.html +13808 -0
- data_ingestor-0.2.0/docs/examples.md +73 -0
- data_ingestor-0.2.0/docs/getting-started.md +70 -0
- data_ingestor-0.2.0/docs/index.md +34 -0
- data_ingestor-0.2.0/docs/reference.md +58 -0
- data_ingestor-0.2.0/docs/streaming.md +118 -0
- data_ingestor-0.2.0/docs/stylesheets/extra.css +10 -0
- data_ingestor-0.2.0/examples/batch_file.py +42 -0
- data_ingestor-0.2.0/examples/bitcoin_stream.py +53 -0
- data_ingestor-0.2.0/examples/consume_btc_ticks.py +35 -0
- data_ingestor-0.2.0/examples/custom_adapter.py +56 -0
- data_ingestor-0.2.0/examples/micro_batch.py +64 -0
- data_ingestor-0.2.0/examples/stream_to_kafka.py +59 -0
- data_ingestor-0.2.0/examples/wikimedia_stream.py +50 -0
- data_ingestor-0.2.0/mkdocs.yml +104 -0
- data_ingestor-0.2.0/pyproject.toml +77 -0
- data_ingestor-0.2.0/src/ingestor/__init__.py +15 -0
- data_ingestor-0.2.0/src/ingestor/__main__.py +6 -0
- data_ingestor-0.2.0/src/ingestor/adapters/__init__.py +0 -0
- data_ingestor-0.2.0/src/ingestor/adapters/batch/__init__.py +5 -0
- data_ingestor-0.2.0/src/ingestor/adapters/batch/file_source.py +43 -0
- data_ingestor-0.2.0/src/ingestor/adapters/console/__init__.py +4 -0
- data_ingestor-0.2.0/src/ingestor/adapters/console/json_sink.py +24 -0
- data_ingestor-0.2.0/src/ingestor/adapters/console/sink.py +28 -0
- data_ingestor-0.2.0/src/ingestor/adapters/kafka/__init__.py +14 -0
- data_ingestor-0.2.0/src/ingestor/adapters/kafka/connection.py +81 -0
- data_ingestor-0.2.0/src/ingestor/adapters/kafka/info.py +37 -0
- data_ingestor-0.2.0/src/ingestor/adapters/kafka/sink.py +48 -0
- data_ingestor-0.2.0/src/ingestor/adapters/kafka/source.py +80 -0
- data_ingestor-0.2.0/src/ingestor/adapters/sse/__init__.py +5 -0
- data_ingestor-0.2.0/src/ingestor/adapters/sse/source.py +106 -0
- data_ingestor-0.2.0/src/ingestor/adapters/tee/__init__.py +5 -0
- data_ingestor-0.2.0/src/ingestor/adapters/tee/sink.py +21 -0
- data_ingestor-0.2.0/src/ingestor/adapters/ws/__init__.py +5 -0
- data_ingestor-0.2.0/src/ingestor/adapters/ws/source.py +99 -0
- data_ingestor-0.2.0/src/ingestor/app.py +32 -0
- data_ingestor-0.2.0/src/ingestor/cli.py +52 -0
- data_ingestor-0.2.0/src/ingestor/ports/__init__.py +8 -0
- data_ingestor-0.2.0/src/ingestor/ports/connection.py +36 -0
- data_ingestor-0.2.0/src/ingestor/ports/record.py +21 -0
- data_ingestor-0.2.0/src/ingestor/ports/sink.py +24 -0
- data_ingestor-0.2.0/src/ingestor/ports/source.py +26 -0
- data_ingestor-0.2.0/src/ingestor/testing.py +52 -0
- data_ingestor-0.2.0/tests/__init__.py +0 -0
- data_ingestor-0.2.0/tests/test_cli.py +103 -0
- data_ingestor-0.2.0/tests/test_console_tee.py +54 -0
- data_ingestor-0.2.0/tests/test_ingestor.py +104 -0
- data_ingestor-0.2.0/tests/test_kafka.py +202 -0
- data_ingestor-0.2.0/tests/test_sse.py +107 -0
- data_ingestor-0.2.0/tests/test_ws.py +100 -0
- data_ingestor-0.2.0/uv.lock +1098 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: "uv"
|
|
4
|
+
directory: "/"
|
|
5
|
+
target-branch: "develop"
|
|
6
|
+
schedule:
|
|
7
|
+
interval: "weekly"
|
|
8
|
+
commit-message:
|
|
9
|
+
prefix: "chore"
|
|
10
|
+
groups:
|
|
11
|
+
python:
|
|
12
|
+
patterns: ["*"]
|
|
13
|
+
|
|
14
|
+
- package-ecosystem: "github-actions"
|
|
15
|
+
directory: "/"
|
|
16
|
+
target-branch: "develop"
|
|
17
|
+
schedule:
|
|
18
|
+
interval: "weekly"
|
|
19
|
+
commit-message:
|
|
20
|
+
prefix: "ci"
|
|
21
|
+
groups:
|
|
22
|
+
actions:
|
|
23
|
+
patterns: ["*"]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
name: Checks
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, develop, "v*"]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: checks-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
lint:
|
|
15
|
+
name: lint
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
|
22
|
+
with:
|
|
23
|
+
enable-cache: true
|
|
24
|
+
- run: uv python install 3.14
|
|
25
|
+
- name: Ruff lint
|
|
26
|
+
run: uv run --group dev ruff check .
|
|
27
|
+
- name: Ruff format
|
|
28
|
+
run: uv run --group dev ruff format --check .
|
|
29
|
+
|
|
30
|
+
types:
|
|
31
|
+
name: types
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
35
|
+
with:
|
|
36
|
+
fetch-depth: 0
|
|
37
|
+
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
|
38
|
+
with:
|
|
39
|
+
enable-cache: true
|
|
40
|
+
- run: uv python install 3.14
|
|
41
|
+
- name: Mypy
|
|
42
|
+
run: uv run --group dev --extra sse --extra ws mypy src/ingestor
|
|
43
|
+
|
|
44
|
+
build:
|
|
45
|
+
name: build
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
49
|
+
with:
|
|
50
|
+
fetch-depth: 0
|
|
51
|
+
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
|
52
|
+
with:
|
|
53
|
+
enable-cache: true
|
|
54
|
+
- run: uv python install 3.14
|
|
55
|
+
- name: Lock is in sync
|
|
56
|
+
run: uv lock --check
|
|
57
|
+
- name: Build sdist and wheel
|
|
58
|
+
run: uv build
|
|
59
|
+
|
|
60
|
+
docs:
|
|
61
|
+
name: docs
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
65
|
+
with:
|
|
66
|
+
fetch-depth: 0
|
|
67
|
+
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
|
68
|
+
with:
|
|
69
|
+
enable-cache: true
|
|
70
|
+
- run: uv python install 3.14
|
|
71
|
+
- name: MkDocs strict build
|
|
72
|
+
run: uv run --group docs mkdocs build --strict
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: CodeQL
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, develop]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, develop]
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: "0 3 * * 1"
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: codeql-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
analyze:
|
|
17
|
+
name: CodeQL
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
permissions:
|
|
20
|
+
actions: read
|
|
21
|
+
contents: read
|
|
22
|
+
security-events: write
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
25
|
+
- name: Initialize CodeQL
|
|
26
|
+
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4
|
|
27
|
+
with:
|
|
28
|
+
languages: python
|
|
29
|
+
- name: Analyze
|
|
30
|
+
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch: {} # manual runs publish to TestPyPI only
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: publish-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: false
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
name: build
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 0 # full history and tags so hatch-vcs derives the version
|
|
20
|
+
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
- run: uv python install 3.14
|
|
24
|
+
- name: Lint
|
|
25
|
+
run: |
|
|
26
|
+
uv run --group dev ruff check .
|
|
27
|
+
uv run --group dev ruff format --check .
|
|
28
|
+
- name: Types
|
|
29
|
+
run: uv run --group dev --extra sse --extra ws mypy src/ingestor
|
|
30
|
+
- name: Tests
|
|
31
|
+
run: uv run --group test --extra sse --extra ws pytest
|
|
32
|
+
- name: Build
|
|
33
|
+
run: uv build
|
|
34
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
|
35
|
+
with:
|
|
36
|
+
name: dist
|
|
37
|
+
path: dist/
|
|
38
|
+
|
|
39
|
+
pypi:
|
|
40
|
+
name: pypi
|
|
41
|
+
needs: build
|
|
42
|
+
if: github.event_name == 'release'
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
environment:
|
|
45
|
+
name: pypi
|
|
46
|
+
url: https://pypi.org/p/data-ingestor
|
|
47
|
+
permissions:
|
|
48
|
+
id-token: write # OIDC token for PyPI trusted publishing
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
|
51
|
+
with:
|
|
52
|
+
name: dist
|
|
53
|
+
path: dist/
|
|
54
|
+
- uses: pypa/gh-action-pypi-publish@release/v1 # Docker action: pin by release tag, not SHA
|
|
55
|
+
|
|
56
|
+
testpypi:
|
|
57
|
+
name: testpypi
|
|
58
|
+
needs: build
|
|
59
|
+
if: github.event_name == 'workflow_dispatch'
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
environment:
|
|
62
|
+
name: testpypi
|
|
63
|
+
url: https://test.pypi.org/p/data-ingestor
|
|
64
|
+
permissions:
|
|
65
|
+
id-token: write
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
|
68
|
+
with:
|
|
69
|
+
name: dist
|
|
70
|
+
path: dist/
|
|
71
|
+
- uses: pypa/gh-action-pypi-publish@release/v1 # Docker action: pin by release tag, not SHA
|
|
72
|
+
with:
|
|
73
|
+
repository-url: https://test.pypi.org/legacy/
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, develop, "v*"]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: tests-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
tests:
|
|
15
|
+
name: pytest + coverage
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout
|
|
19
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
20
|
+
with:
|
|
21
|
+
fetch-depth: 0
|
|
22
|
+
|
|
23
|
+
- name: Set up uv
|
|
24
|
+
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
|
25
|
+
with:
|
|
26
|
+
enable-cache: true
|
|
27
|
+
|
|
28
|
+
- name: Install Python 3.14
|
|
29
|
+
run: uv python install 3.14
|
|
30
|
+
|
|
31
|
+
- name: Run tests with coverage (fails under 80%)
|
|
32
|
+
run: uv run --group test --extra sse --extra ws pytest
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python Files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual Environment
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# Environment Variables
|
|
13
|
+
.env
|
|
14
|
+
|
|
15
|
+
# Archify Working Folders
|
|
16
|
+
.archify/
|
|
17
|
+
|
|
18
|
+
# MkDocs Build Output
|
|
19
|
+
site/
|
|
20
|
+
|
|
21
|
+
# Tests and coverage
|
|
22
|
+
.coverage
|
|
23
|
+
.coverage.*
|
|
24
|
+
htmlcov/
|
|
25
|
+
.pytest_cache/
|
|
26
|
+
|
|
27
|
+
# Tool caches
|
|
28
|
+
.mypy_cache/
|
|
29
|
+
.ruff_cache/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"editor.fontFamily": "'Fira Code', 'JetBrains Mono', Menlo, Monaco, 'Courier New', monospace",
|
|
3
|
+
"editor.fontLigatures": true,
|
|
4
|
+
"explorer.compactFolders": false,
|
|
5
|
+
"makefile.configureOnOpen": false,
|
|
6
|
+
"cSpell.words": [
|
|
7
|
+
"Ccircle",
|
|
8
|
+
"Cpath",
|
|
9
|
+
"Crect",
|
|
10
|
+
"Csvg",
|
|
11
|
+
"enwiktionary",
|
|
12
|
+
"Etampes",
|
|
13
|
+
"explorable",
|
|
14
|
+
"frwiki",
|
|
15
|
+
"fsrc",
|
|
16
|
+
"glightbox",
|
|
17
|
+
"httpx",
|
|
18
|
+
"inlinehilite",
|
|
19
|
+
"kout",
|
|
20
|
+
"ksink",
|
|
21
|
+
"ksrc",
|
|
22
|
+
"messagebus",
|
|
23
|
+
"mkdocstrings",
|
|
24
|
+
"neighbouring",
|
|
25
|
+
"pannable",
|
|
26
|
+
"Pudox",
|
|
27
|
+
"rasterizes",
|
|
28
|
+
"rescoping",
|
|
29
|
+
"sandboxed",
|
|
30
|
+
"SASL",
|
|
31
|
+
"Songti",
|
|
32
|
+
"sublabel",
|
|
33
|
+
"tasklist",
|
|
34
|
+
"timeslice",
|
|
35
|
+
"unimportable",
|
|
36
|
+
"Victar",
|
|
37
|
+
"wikidatawiki"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
.DEFAULT_GOAL := help
|
|
2
|
+
.PHONY: help install sync reinstall run test up stop logs ps wiki btc batch custom micro-batch bridge consume-btc docs-serve docs-build docs-deploy clean
|
|
3
|
+
|
|
4
|
+
help:
|
|
5
|
+
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) \
|
|
6
|
+
| awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}'
|
|
7
|
+
|
|
8
|
+
install:
|
|
9
|
+
uv sync
|
|
10
|
+
|
|
11
|
+
sync: install
|
|
12
|
+
|
|
13
|
+
reinstall:
|
|
14
|
+
uv sync --reinstall-package data-ingestor
|
|
15
|
+
|
|
16
|
+
run:
|
|
17
|
+
PYTHONPATH=src uv run python -m ingestor
|
|
18
|
+
|
|
19
|
+
test:
|
|
20
|
+
PYTHONPATH=src uv run python -m unittest discover -v
|
|
21
|
+
|
|
22
|
+
up:
|
|
23
|
+
docker compose up -d
|
|
24
|
+
|
|
25
|
+
stop:
|
|
26
|
+
docker compose stop
|
|
27
|
+
|
|
28
|
+
logs:
|
|
29
|
+
docker compose logs -f kafka
|
|
30
|
+
|
|
31
|
+
ps:
|
|
32
|
+
docker compose ps
|
|
33
|
+
|
|
34
|
+
wiki:
|
|
35
|
+
PYTHONPATH=src uv run --extra sse python examples/wikimedia_stream.py
|
|
36
|
+
|
|
37
|
+
btc:
|
|
38
|
+
PYTHONPATH=src uv run --extra ws python examples/bitcoin_stream.py
|
|
39
|
+
|
|
40
|
+
batch:
|
|
41
|
+
PYTHONPATH=src uv run python examples/batch_file.py $(FILE)
|
|
42
|
+
|
|
43
|
+
custom:
|
|
44
|
+
PYTHONPATH=src uv run python examples/custom_adapter.py
|
|
45
|
+
|
|
46
|
+
micro-batch:
|
|
47
|
+
PYTHONPATH=src uv run python examples/micro_batch.py
|
|
48
|
+
|
|
49
|
+
bridge:
|
|
50
|
+
PYTHONPATH=src uv run --extra ws python examples/stream_to_kafka.py $(N)
|
|
51
|
+
|
|
52
|
+
consume-btc:
|
|
53
|
+
PYTHONPATH=src uv run python examples/consume_btc_ticks.py
|
|
54
|
+
|
|
55
|
+
docs-serve:
|
|
56
|
+
uv run --group docs mkdocs serve
|
|
57
|
+
|
|
58
|
+
docs-build:
|
|
59
|
+
uv run --group docs mkdocs build --strict
|
|
60
|
+
|
|
61
|
+
docs-deploy:
|
|
62
|
+
uv run --group docs mkdocs gh-deploy --force
|
|
63
|
+
|
|
64
|
+
clean:
|
|
65
|
+
find . -type d -name __pycache__ -prune -exec rm -rf {} + 2>/dev/null || true
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: data-ingestor
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Data ingestion (streaming, batch, micro-batch) with a ports-and-adapters architecture
|
|
5
|
+
Requires-Python: >=3.14
|
|
6
|
+
Requires-Dist: confluent-kafka>=2.15.0
|
|
7
|
+
Provides-Extra: sse
|
|
8
|
+
Requires-Dist: httpx>=0.27; extra == 'sse'
|
|
9
|
+
Provides-Extra: ws
|
|
10
|
+
Requires-Dist: websocket-client>=1.8; extra == 'ws'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Ingestor
|
|
14
|
+
|
|
15
|
+
A Python **data ingestion** app that reads records from a source (**Source**) and writes them to a destination (**Sink**). The same logic serves several modes: **streaming** (Kafka), **batch** (file) and **micro-batch** (by limit/window).
|
|
16
|
+
|
|
17
|
+
The architecture is **ports and adapters**: the logic depends only on interfaces (`ingestor.ports`), and each tool lives isolated in its own adapter (`ingestor.adapters.*`). Swapping source or destination - or using in-memory fakes in tests - does not touch the logic.
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Python 3.14 or newer.
|
|
22
|
+
- `uv` installed to manage dependencies.
|
|
23
|
+
- Docker with Docker Compose and Docker Desktop running (only for the Kafka mode).
|
|
24
|
+
- Port `9092` free for local Kafka.
|
|
25
|
+
|
|
26
|
+
## How to run
|
|
27
|
+
|
|
28
|
+
### 1. Clone the project
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/rikemorais/ingestor.git
|
|
32
|
+
cd ingestor
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Install dependencies
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv sync
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. Start Kafka
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
docker compose up -d
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Wait for the broker to start. To follow the logs:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
docker compose logs -f kafka
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Press `Ctrl+C` to leave the logs. Kafka keeps running.
|
|
54
|
+
|
|
55
|
+
### 4. Run the app
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv run ingestor
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
> `ingestor` is the command defined in `pyproject.toml` (`[project.scripts]`). Equivalent alternative: `uv run python -m ingestor`.
|
|
62
|
+
|
|
63
|
+
The demo seeds a few records into the topic and then **ingests from Kafka to the console**. With local Kafka available, the expected output is:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
Connected to Kafka: 1 broker(s).
|
|
67
|
+
Ingested: origin=demo-messages key=1 value=Hello Kafka #1
|
|
68
|
+
Ingested: origin=demo-messages key=2 value=Hello Kafka #2
|
|
69
|
+
Ingested: origin=demo-messages key=3 value=Hello Kafka #3
|
|
70
|
+
Ingested: origin=demo-messages key=4 value=Hello Kafka #4
|
|
71
|
+
Ingested: origin=demo-messages key=5 value=Hello Kafka #5
|
|
72
|
+
Ingested 5 records.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The connection uses `localhost:9092` by default and waits up to 10 seconds for a response.
|
|
76
|
+
|
|
77
|
+
> Note: `KafkaSource` consumes in the `ingestor` group and, after a few consecutive empty polls (`max_empty_polls`), stops - that's why the demo terminates instead of waiting forever. Running again with the same group won't re-read already-read messages (the offset is saved). To re-consume everything, change the group in `src/ingestor/adapters/kafka/connection.py` or recreate the topic.
|
|
78
|
+
|
|
79
|
+
## Use another address
|
|
80
|
+
|
|
81
|
+
Set `KAFKA_BOOTSTRAP_SERVERS` when running:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
KAFKA_BOOTSTRAP_SERVERS=server:9092 uv run ingestor
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Replace `server:9092` with your broker's address. The current setup uses an unauthenticated connection; SASL/TLS clusters need adjustments in the adapter.
|
|
88
|
+
|
|
89
|
+
## Run the tests (no Kafka)
|
|
90
|
+
|
|
91
|
+
The tests use in-memory implementations (fakes) and a file source, so they **need neither Docker nor a broker**:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
uv run python -m unittest discover -v
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
They exercise the same `ingest` function with a `FakeConnection` and with a `FileSource` (batch) - proving in practice that the logic does not depend on the Kafka lib.
|
|
98
|
+
|
|
99
|
+
> Tip: when adding/moving files under `src/`, if `uv run` complains about a module not found, run once `uv sync --reinstall-package data-ingestor`.
|
|
100
|
+
|
|
101
|
+
## Stop Kafka
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
docker compose stop
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
To start the same container again:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
docker compose start
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## If the connection fails
|
|
114
|
+
|
|
115
|
+
Check that Docker is running and inspect the broker's state and logs:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
docker compose ps
|
|
119
|
+
docker compose logs --tail=50 kafka
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
If Kafka is still starting up, wait and run again. Also check the address and that port `9092` is free. On failure, the app prints the error and exits with code `1`.
|
|
123
|
+
|
|
124
|
+
## Structure
|
|
125
|
+
|
|
126
|
+
`src/` layout, with the code in an `ingestor` package organized as ports × adapters:
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
ingestor/
|
|
130
|
+
├── src/
|
|
131
|
+
│ └── ingestor/
|
|
132
|
+
│ ├── __init__.py
|
|
133
|
+
│ ├── app.py # use case: ingest(source, sink, limit)
|
|
134
|
+
│ ├── cli.py # Composition Root: picks and injects the adapters
|
|
135
|
+
│ ├── __main__.py # enables `python -m ingestor`
|
|
136
|
+
│ ├── ports/ # interfaces + domain (tool-agnostic)
|
|
137
|
+
│ │ ├── record.py # Record (value, key, origin)
|
|
138
|
+
│ │ ├── source.py # Source: read() -> Iterator[Record]
|
|
139
|
+
│ │ ├── sink.py # Sink: write(record)
|
|
140
|
+
│ │ └── connection.py # Connection: source() / sink()
|
|
141
|
+
│ └── adapters/ # concrete implementations (one folder per tool)
|
|
142
|
+
│ ├── kafka/ # streaming: KafkaSource, KafkaSink, KafkaConnection, KafkaInfoCluster
|
|
143
|
+
│ ├── console/ # generic destination: ConsoleSink
|
|
144
|
+
│ └── batch/ # batch: FileSource (reads a file line by line)
|
|
145
|
+
├── tests/
|
|
146
|
+
│ ├── __init__.py
|
|
147
|
+
│ ├── fakes.py # ListSource, MemorySink, FakeConnection
|
|
148
|
+
│ └── test_ingestor.py # unittest tests
|
|
149
|
+
├── docker-compose.yml # local Kafka
|
|
150
|
+
├── pyproject.toml # dependencies, build and the `ingestor` command
|
|
151
|
+
└── uv.lock
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**How the layers relate:** `app.py` depends only on `ingestor.ports` (Source, Sink, Connection); the adapters in `adapters/` implement those ports; and only `cli.py` knows the concrete classes, wiring everything together. This way no specific lib (like `confluent_kafka`) "leaks" into the logic or the tests.
|
|
155
|
+
|
|
156
|
+
**The three modes, the same logic:**
|
|
157
|
+
|
|
158
|
+
| Mode | Source | Does `ingest` change? |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| Streaming | `KafkaSource` | no |
|
|
161
|
+
| Batch | `FileSource` | no |
|
|
162
|
+
| Micro-batch | any Source + `limit` | no |
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Ingestor
|
|
2
|
+
|
|
3
|
+
A Python **data ingestion** app that reads records from a source (**Source**) and writes them to a destination (**Sink**). The same logic serves several modes: **streaming** (Kafka), **batch** (file) and **micro-batch** (by limit/window).
|
|
4
|
+
|
|
5
|
+
The architecture is **ports and adapters**: the logic depends only on interfaces (`ingestor.ports`), and each tool lives isolated in its own adapter (`ingestor.adapters.*`). Swapping source or destination - or using in-memory fakes in tests - does not touch the logic.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Python 3.14 or newer.
|
|
10
|
+
- `uv` installed to manage dependencies.
|
|
11
|
+
- Docker with Docker Compose and Docker Desktop running (only for the Kafka mode).
|
|
12
|
+
- Port `9092` free for local Kafka.
|
|
13
|
+
|
|
14
|
+
## How to run
|
|
15
|
+
|
|
16
|
+
### 1. Clone the project
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
git clone https://github.com/rikemorais/ingestor.git
|
|
20
|
+
cd ingestor
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 2. Install dependencies
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv sync
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 3. Start Kafka
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
docker compose up -d
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Wait for the broker to start. To follow the logs:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
docker compose logs -f kafka
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Press `Ctrl+C` to leave the logs. Kafka keeps running.
|
|
42
|
+
|
|
43
|
+
### 4. Run the app
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv run ingestor
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> `ingestor` is the command defined in `pyproject.toml` (`[project.scripts]`). Equivalent alternative: `uv run python -m ingestor`.
|
|
50
|
+
|
|
51
|
+
The demo seeds a few records into the topic and then **ingests from Kafka to the console**. With local Kafka available, the expected output is:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
Connected to Kafka: 1 broker(s).
|
|
55
|
+
Ingested: origin=demo-messages key=1 value=Hello Kafka #1
|
|
56
|
+
Ingested: origin=demo-messages key=2 value=Hello Kafka #2
|
|
57
|
+
Ingested: origin=demo-messages key=3 value=Hello Kafka #3
|
|
58
|
+
Ingested: origin=demo-messages key=4 value=Hello Kafka #4
|
|
59
|
+
Ingested: origin=demo-messages key=5 value=Hello Kafka #5
|
|
60
|
+
Ingested 5 records.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The connection uses `localhost:9092` by default and waits up to 10 seconds for a response.
|
|
64
|
+
|
|
65
|
+
> Note: `KafkaSource` consumes in the `ingestor` group and, after a few consecutive empty polls (`max_empty_polls`), stops - that's why the demo terminates instead of waiting forever. Running again with the same group won't re-read already-read messages (the offset is saved). To re-consume everything, change the group in `src/ingestor/adapters/kafka/connection.py` or recreate the topic.
|
|
66
|
+
|
|
67
|
+
## Use another address
|
|
68
|
+
|
|
69
|
+
Set `KAFKA_BOOTSTRAP_SERVERS` when running:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
KAFKA_BOOTSTRAP_SERVERS=server:9092 uv run ingestor
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Replace `server:9092` with your broker's address. The current setup uses an unauthenticated connection; SASL/TLS clusters need adjustments in the adapter.
|
|
76
|
+
|
|
77
|
+
## Run the tests (no Kafka)
|
|
78
|
+
|
|
79
|
+
The tests use in-memory implementations (fakes) and a file source, so they **need neither Docker nor a broker**:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
uv run python -m unittest discover -v
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
They exercise the same `ingest` function with a `FakeConnection` and with a `FileSource` (batch) - proving in practice that the logic does not depend on the Kafka lib.
|
|
86
|
+
|
|
87
|
+
> Tip: when adding/moving files under `src/`, if `uv run` complains about a module not found, run once `uv sync --reinstall-package data-ingestor`.
|
|
88
|
+
|
|
89
|
+
## Stop Kafka
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
docker compose stop
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
To start the same container again:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
docker compose start
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## If the connection fails
|
|
102
|
+
|
|
103
|
+
Check that Docker is running and inspect the broker's state and logs:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
docker compose ps
|
|
107
|
+
docker compose logs --tail=50 kafka
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
If Kafka is still starting up, wait and run again. Also check the address and that port `9092` is free. On failure, the app prints the error and exits with code `1`.
|
|
111
|
+
|
|
112
|
+
## Structure
|
|
113
|
+
|
|
114
|
+
`src/` layout, with the code in an `ingestor` package organized as ports × adapters:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
ingestor/
|
|
118
|
+
├── src/
|
|
119
|
+
│ └── ingestor/
|
|
120
|
+
│ ├── __init__.py
|
|
121
|
+
│ ├── app.py # use case: ingest(source, sink, limit)
|
|
122
|
+
│ ├── cli.py # Composition Root: picks and injects the adapters
|
|
123
|
+
│ ├── __main__.py # enables `python -m ingestor`
|
|
124
|
+
│ ├── ports/ # interfaces + domain (tool-agnostic)
|
|
125
|
+
│ │ ├── record.py # Record (value, key, origin)
|
|
126
|
+
│ │ ├── source.py # Source: read() -> Iterator[Record]
|
|
127
|
+
│ │ ├── sink.py # Sink: write(record)
|
|
128
|
+
│ │ └── connection.py # Connection: source() / sink()
|
|
129
|
+
│ └── adapters/ # concrete implementations (one folder per tool)
|
|
130
|
+
│ ├── kafka/ # streaming: KafkaSource, KafkaSink, KafkaConnection, KafkaInfoCluster
|
|
131
|
+
│ ├── console/ # generic destination: ConsoleSink
|
|
132
|
+
│ └── batch/ # batch: FileSource (reads a file line by line)
|
|
133
|
+
├── tests/
|
|
134
|
+
│ ├── __init__.py
|
|
135
|
+
│ ├── fakes.py # ListSource, MemorySink, FakeConnection
|
|
136
|
+
│ └── test_ingestor.py # unittest tests
|
|
137
|
+
├── docker-compose.yml # local Kafka
|
|
138
|
+
├── pyproject.toml # dependencies, build and the `ingestor` command
|
|
139
|
+
└── uv.lock
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**How the layers relate:** `app.py` depends only on `ingestor.ports` (Source, Sink, Connection); the adapters in `adapters/` implement those ports; and only `cli.py` knows the concrete classes, wiring everything together. This way no specific lib (like `confluent_kafka`) "leaks" into the logic or the tests.
|
|
143
|
+
|
|
144
|
+
**The three modes, the same logic:**
|
|
145
|
+
|
|
146
|
+
| Mode | Source | Does `ingest` change? |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| Streaming | `KafkaSource` | no |
|
|
149
|
+
| Batch | `FileSource` | no |
|
|
150
|
+
| Micro-batch | any Source + `limit` | no |
|