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.
Files changed (65) hide show
  1. data_ingestor-0.2.0/.github/dependabot.yml +23 -0
  2. data_ingestor-0.2.0/.github/workflows/checks.yml +72 -0
  3. data_ingestor-0.2.0/.github/workflows/codeql.yml +30 -0
  4. data_ingestor-0.2.0/.github/workflows/publish.yml +73 -0
  5. data_ingestor-0.2.0/.github/workflows/tests.yml +32 -0
  6. data_ingestor-0.2.0/.gitignore +29 -0
  7. data_ingestor-0.2.0/.python-version +1 -0
  8. data_ingestor-0.2.0/.vscode/settings.json +39 -0
  9. data_ingestor-0.2.0/Makefile +65 -0
  10. data_ingestor-0.2.0/PKG-INFO +162 -0
  11. data_ingestor-0.2.0/README.md +150 -0
  12. data_ingestor-0.2.0/docker-compose.yml +24 -0
  13. data_ingestor-0.2.0/docs/adapters.md +94 -0
  14. data_ingestor-0.2.0/docs/architecture.md +60 -0
  15. data_ingestor-0.2.0/docs/diagrams/ingestor-architecture.html +13808 -0
  16. data_ingestor-0.2.0/docs/examples.md +73 -0
  17. data_ingestor-0.2.0/docs/getting-started.md +70 -0
  18. data_ingestor-0.2.0/docs/index.md +34 -0
  19. data_ingestor-0.2.0/docs/reference.md +58 -0
  20. data_ingestor-0.2.0/docs/streaming.md +118 -0
  21. data_ingestor-0.2.0/docs/stylesheets/extra.css +10 -0
  22. data_ingestor-0.2.0/examples/batch_file.py +42 -0
  23. data_ingestor-0.2.0/examples/bitcoin_stream.py +53 -0
  24. data_ingestor-0.2.0/examples/consume_btc_ticks.py +35 -0
  25. data_ingestor-0.2.0/examples/custom_adapter.py +56 -0
  26. data_ingestor-0.2.0/examples/micro_batch.py +64 -0
  27. data_ingestor-0.2.0/examples/stream_to_kafka.py +59 -0
  28. data_ingestor-0.2.0/examples/wikimedia_stream.py +50 -0
  29. data_ingestor-0.2.0/mkdocs.yml +104 -0
  30. data_ingestor-0.2.0/pyproject.toml +77 -0
  31. data_ingestor-0.2.0/src/ingestor/__init__.py +15 -0
  32. data_ingestor-0.2.0/src/ingestor/__main__.py +6 -0
  33. data_ingestor-0.2.0/src/ingestor/adapters/__init__.py +0 -0
  34. data_ingestor-0.2.0/src/ingestor/adapters/batch/__init__.py +5 -0
  35. data_ingestor-0.2.0/src/ingestor/adapters/batch/file_source.py +43 -0
  36. data_ingestor-0.2.0/src/ingestor/adapters/console/__init__.py +4 -0
  37. data_ingestor-0.2.0/src/ingestor/adapters/console/json_sink.py +24 -0
  38. data_ingestor-0.2.0/src/ingestor/adapters/console/sink.py +28 -0
  39. data_ingestor-0.2.0/src/ingestor/adapters/kafka/__init__.py +14 -0
  40. data_ingestor-0.2.0/src/ingestor/adapters/kafka/connection.py +81 -0
  41. data_ingestor-0.2.0/src/ingestor/adapters/kafka/info.py +37 -0
  42. data_ingestor-0.2.0/src/ingestor/adapters/kafka/sink.py +48 -0
  43. data_ingestor-0.2.0/src/ingestor/adapters/kafka/source.py +80 -0
  44. data_ingestor-0.2.0/src/ingestor/adapters/sse/__init__.py +5 -0
  45. data_ingestor-0.2.0/src/ingestor/adapters/sse/source.py +106 -0
  46. data_ingestor-0.2.0/src/ingestor/adapters/tee/__init__.py +5 -0
  47. data_ingestor-0.2.0/src/ingestor/adapters/tee/sink.py +21 -0
  48. data_ingestor-0.2.0/src/ingestor/adapters/ws/__init__.py +5 -0
  49. data_ingestor-0.2.0/src/ingestor/adapters/ws/source.py +99 -0
  50. data_ingestor-0.2.0/src/ingestor/app.py +32 -0
  51. data_ingestor-0.2.0/src/ingestor/cli.py +52 -0
  52. data_ingestor-0.2.0/src/ingestor/ports/__init__.py +8 -0
  53. data_ingestor-0.2.0/src/ingestor/ports/connection.py +36 -0
  54. data_ingestor-0.2.0/src/ingestor/ports/record.py +21 -0
  55. data_ingestor-0.2.0/src/ingestor/ports/sink.py +24 -0
  56. data_ingestor-0.2.0/src/ingestor/ports/source.py +26 -0
  57. data_ingestor-0.2.0/src/ingestor/testing.py +52 -0
  58. data_ingestor-0.2.0/tests/__init__.py +0 -0
  59. data_ingestor-0.2.0/tests/test_cli.py +103 -0
  60. data_ingestor-0.2.0/tests/test_console_tee.py +54 -0
  61. data_ingestor-0.2.0/tests/test_ingestor.py +104 -0
  62. data_ingestor-0.2.0/tests/test_kafka.py +202 -0
  63. data_ingestor-0.2.0/tests/test_sse.py +107 -0
  64. data_ingestor-0.2.0/tests/test_ws.py +100 -0
  65. 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 |