logreducer 3.5.1__tar.gz → 3.5.3__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 (25) hide show
  1. {logreducer-3.5.1 → logreducer-3.5.3}/PKG-INFO +19 -17
  2. {logreducer-3.5.1 → logreducer-3.5.3}/README.md +9 -5
  3. {logreducer-3.5.1 → logreducer-3.5.3}/pyproject.toml +35 -23
  4. {logreducer-3.5.1 → logreducer-3.5.3}/pyproject.toml.orig +47 -23
  5. {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/anomaly.py +6 -7
  6. {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/config.py +3 -1
  7. {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/core.py +65 -16
  8. logreducer-3.5.3/src/logreducer/logging_config.py +237 -0
  9. {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/memory.py +10 -12
  10. {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/patterns.py +32 -30
  11. {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/sinks.py +10 -1
  12. {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/sql.py +1 -1
  13. logreducer-3.5.3/src/logreducer/template_mining.py +453 -0
  14. {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/temporal.py +26 -42
  15. logreducer-3.5.1/src/logreducer/logging_config.py +0 -193
  16. {logreducer-3.5.1 → logreducer-3.5.3}/LICENSE +0 -0
  17. {logreducer-3.5.1 → logreducer-3.5.3}/NOTICE +0 -0
  18. {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/__init__.py +0 -0
  19. {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/cli.py +0 -0
  20. {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/clickhouse.py +0 -0
  21. {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/kafka.py +0 -0
  22. {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/py.typed +0 -0
  23. {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/sampling.py +0 -0
  24. {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/sources.py +0 -0
  25. {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/target.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: logreducer
3
- Version: 3.5.1
3
+ Version: 3.5.3
4
4
  Summary: Reduce GB-scale logs to a representative sample - streaming Python library and CLI with pattern mining and anomaly detection
5
5
  Keywords: log,logging,reduction,analysis,pattern-extraction,memory-efficient,streaming,anomaly-detection
6
6
  Author: HyperI Team
@@ -17,17 +17,15 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
17
  Classifier: Programming Language :: Python :: 3
18
18
  Classifier: Programming Language :: Python :: 3 :: Only
19
19
  Classifier: Operating System :: OS Independent
20
- Requires-Dist: drain3>=0.9.0
21
- Requires-Dist: psutil>=7.2.0
22
- Requires-Dist: loguru>=0.7.3
23
- Requires-Dist: numpy>=2.0.0
24
- Requires-Dist: scikit-learn>=1.5.0
25
- Requires-Dist: typer>=0.25.0
26
- Requires-Dist: clickhouse-connect>=1.0 ; extra == 'clickhouse'
27
- Requires-Dist: xxhash>=3.0.0 ; extra == 'enhanced'
28
- Requires-Dist: datasketch>=1.5.0 ; extra == 'enhanced'
29
- Requires-Dist: confluent-kafka>=2.14.0 ; extra == 'kafka'
30
- Requires-Dist: sqlalchemy>=2.0.30 ; extra == 'sql'
20
+ Requires-Dist: psutil>=7.2.2
21
+ Requires-Dist: numpy>=2.5.3
22
+ Requires-Dist: scikit-learn>=1.9.1
23
+ Requires-Dist: typer>=0.27.3
24
+ Requires-Dist: clickhouse-connect>=1.9.0 ; extra == 'clickhouse'
25
+ Requires-Dist: xxhash>=4.0.1 ; extra == 'enhanced'
26
+ Requires-Dist: datasketch>=2.0.0 ; extra == 'enhanced'
27
+ Requires-Dist: confluent-kafka>=2.15.1 ; extra == 'kafka'
28
+ Requires-Dist: sqlalchemy>=2.1.3 ; extra == 'sql'
31
29
  Maintainer: HyperI Team
32
30
  Maintainer-email: HyperI Team <dev@hyperi.io>
33
31
  Requires-Python: >=3.14
@@ -337,8 +335,9 @@ reducer = logreducer.LogReducer(mode="hybrid", config=cfg)
337
335
  # ...or let logreducer read your env cascade directly: prefixed beats bare.
338
336
  cfg = logreducer.BigDialConfig.from_env("MYAPP", "LOGREDUCER") # MYAPP_MAX_MEMORY_GB wins
339
337
 
340
- # 2. Logging: your app owns the loguru sinks; logreducer registers nothing and
341
- # its records flow through YOUR handlers, formatted by YOUR standard.
338
+ # 2. Logging: your app owns the stdlib logging handlers; logreducer attaches
339
+ # nothing and its records flow through YOUR handlers, at YOUR level,
340
+ # formatted by YOUR standard.
342
341
  logreducer.setup_logging(enable=True, own_sinks=False)
343
342
  ```
344
343
 
@@ -368,7 +367,7 @@ Logging is off by default. Enable it and pick a format via env vars:
368
367
  LOG_LEVEL=DEBUG LOG_FORMAT=json logreducer app.log --log
369
368
  ```
370
369
 
371
- `LOG_FORMAT=json` emits one JSON object per line for log aggregators; the default is human-readable (coloured in a terminal, plain in CI/containers). Embedding? See `own_sinks=False` above.
370
+ Logging is the standard library `logging` module, under the `logreducer` logger. `LOG_FORMAT=json` emits one JSON object per line for log aggregators (keys `timestamp`, `level`, `target`, `function`, `line_number`, `message`, `fields`); the default is human-readable (coloured in a terminal, plain in CI/containers). Embedding? See `own_sinks=False` above.
372
371
 
373
372
  ## Development
374
373
 
@@ -386,6 +385,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the toolchain, test layout (including
386
385
 
387
386
  Copyright 2026 HYPERI PTY LIMITED.
388
387
 
388
+ Credits: template mining (`src/logreducer/template_mining.py`) is derived from [Drain3](https://github.com/logpai/Drain3), Copyright (c) 2020-2022 International Business Machines and the Drain3 project contributors, MIT licence. Drain3 builds on the Drain parser in LogPAI's [logparser](https://github.com/logpai/logparser). The module header carries Drain3's licence text in full.
389
+
389
390
  ## Context
390
391
 
391
392
  Depth: [docs/architecture.md](docs/architecture.md).
@@ -406,6 +407,7 @@ it is given and drops the rest.
406
407
  | `src/logreducer/sources.py`, `sinks.py` | the two structural protocols that ARE the integration surface, plus `FileSource` and `FileSink` |
407
408
  | `src/logreducer/sql.py`, `clickhouse.py`, `kafka.py` | the optional adapters, one extra each |
408
409
  | `src/logreducer/patterns.py`, `anomaly.py`, `temporal.py` | Drain3 mining and fuzzy dedup, Isolation Forest, time-window grouping |
410
+ | `src/logreducer/template_mining.py` | the Drain template miner, derived from Drain3 (credited under License) |
409
411
  | `src/logreducer/memory.py` | `MemoryMonitor`, `BoundedDeduplicator`, the file read strategies |
410
412
  | `src/logreducer/sampling.py`, `target.py` | dialect-aware SQL sampling, and the `reduce_to_target` batch loop |
411
413
  | `src/logreducer/config.py` | `BigDialConfig`, the level presets, the `from_env` cascade |
@@ -438,7 +440,7 @@ Individually: `uv run ruff format`, `uv run ruff check --fix`,
438
440
  | Assume a reused `LogReducer` starts clean | Nothing -- `_reset_components()` rebuilds them per run | The dedup seen-set, both Drain3 miners and the fuzzy LSH accumulate per line. Carried into the next run they drop every line as already-seen and return empty (`core.py`) |
439
441
  | Share one deduplicator across hybrid's two passes | Rebuild it before the anomaly pass | The pattern pass consumes the seen-set, so the anomaly pass sees everything as seen and no anomalies survive. Hybrid silently degrades to pattern-only (`core.py`) |
440
442
  | Assume an unknown config keyword is ignored | Read the `ValueError` and fix the name | A silently dropped override means a user's tuning quietly does nothing (`core.py`) |
441
- | Read `LogPattern.template` as the final Drain3 template | Treat it as a first-seen snapshot until #5 lands | `extract_patterns` captures `cluster.get_template()` when a cluster FIRST appears, before later lines widen slots to `<*>`, so a varying cluster stores its raw first line. The `<*>`-count priority boost then runs on that stale snapshot (#5, open) |
443
+ | Assume every template is the final Drain3 one | Allow for a cluster the `max_clusters` cap evicted | `extract_patterns` re-reads each template from the live cluster once the feed ends, so a varying cluster carries its `<*>` slots and the priority boost counts them. A cluster LRU-evicted mid-feed is gone by then and keeps the template it had when first seen (`patterns.py`) |
442
444
  | Hand-edit the version | Land a conventional commit and let semantic-release do it | The old regime wrote `VERSION` by hand and took five commits to get tagging working. CONTRIBUTING makes it a standing rule |
443
445
 
444
446
  ### Where this sits
@@ -451,7 +453,7 @@ One declared edge in `dfe-infra/suite.yaml`, and it runs the surprising way.
451
453
 
452
454
  `dfe-stack suite --consumer logreducer` returns an empty edge list: no repo in
453
455
  that graph is something this library depends on. The runtime dependencies
454
- (drain3, psutil, loguru, numpy, scikit-learn, typer) are packages, declared in
456
+ (psutil, numpy, scikit-learn, typer) are packages, declared in
455
457
  `pyproject.toml`, which is where they belong.
456
458
 
457
459
  The reason dfe-engine gives for not declaring the dependency -- pin it "once it
@@ -293,8 +293,9 @@ reducer = logreducer.LogReducer(mode="hybrid", config=cfg)
293
293
  # ...or let logreducer read your env cascade directly: prefixed beats bare.
294
294
  cfg = logreducer.BigDialConfig.from_env("MYAPP", "LOGREDUCER") # MYAPP_MAX_MEMORY_GB wins
295
295
 
296
- # 2. Logging: your app owns the loguru sinks; logreducer registers nothing and
297
- # its records flow through YOUR handlers, formatted by YOUR standard.
296
+ # 2. Logging: your app owns the stdlib logging handlers; logreducer attaches
297
+ # nothing and its records flow through YOUR handlers, at YOUR level,
298
+ # formatted by YOUR standard.
298
299
  logreducer.setup_logging(enable=True, own_sinks=False)
299
300
  ```
300
301
 
@@ -324,7 +325,7 @@ Logging is off by default. Enable it and pick a format via env vars:
324
325
  LOG_LEVEL=DEBUG LOG_FORMAT=json logreducer app.log --log
325
326
  ```
326
327
 
327
- `LOG_FORMAT=json` emits one JSON object per line for log aggregators; the default is human-readable (coloured in a terminal, plain in CI/containers). Embedding? See `own_sinks=False` above.
328
+ Logging is the standard library `logging` module, under the `logreducer` logger. `LOG_FORMAT=json` emits one JSON object per line for log aggregators (keys `timestamp`, `level`, `target`, `function`, `line_number`, `message`, `fields`); the default is human-readable (coloured in a terminal, plain in CI/containers). Embedding? See `own_sinks=False` above.
328
329
 
329
330
  ## Development
330
331
 
@@ -342,6 +343,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the toolchain, test layout (including
342
343
 
343
344
  Copyright 2026 HYPERI PTY LIMITED.
344
345
 
346
+ Credits: template mining (`src/logreducer/template_mining.py`) is derived from [Drain3](https://github.com/logpai/Drain3), Copyright (c) 2020-2022 International Business Machines and the Drain3 project contributors, MIT licence. Drain3 builds on the Drain parser in LogPAI's [logparser](https://github.com/logpai/logparser). The module header carries Drain3's licence text in full.
347
+
345
348
  ## Context
346
349
 
347
350
  Depth: [docs/architecture.md](docs/architecture.md).
@@ -362,6 +365,7 @@ it is given and drops the rest.
362
365
  | `src/logreducer/sources.py`, `sinks.py` | the two structural protocols that ARE the integration surface, plus `FileSource` and `FileSink` |
363
366
  | `src/logreducer/sql.py`, `clickhouse.py`, `kafka.py` | the optional adapters, one extra each |
364
367
  | `src/logreducer/patterns.py`, `anomaly.py`, `temporal.py` | Drain3 mining and fuzzy dedup, Isolation Forest, time-window grouping |
368
+ | `src/logreducer/template_mining.py` | the Drain template miner, derived from Drain3 (credited under License) |
365
369
  | `src/logreducer/memory.py` | `MemoryMonitor`, `BoundedDeduplicator`, the file read strategies |
366
370
  | `src/logreducer/sampling.py`, `target.py` | dialect-aware SQL sampling, and the `reduce_to_target` batch loop |
367
371
  | `src/logreducer/config.py` | `BigDialConfig`, the level presets, the `from_env` cascade |
@@ -394,7 +398,7 @@ Individually: `uv run ruff format`, `uv run ruff check --fix`,
394
398
  | Assume a reused `LogReducer` starts clean | Nothing -- `_reset_components()` rebuilds them per run | The dedup seen-set, both Drain3 miners and the fuzzy LSH accumulate per line. Carried into the next run they drop every line as already-seen and return empty (`core.py`) |
395
399
  | Share one deduplicator across hybrid's two passes | Rebuild it before the anomaly pass | The pattern pass consumes the seen-set, so the anomaly pass sees everything as seen and no anomalies survive. Hybrid silently degrades to pattern-only (`core.py`) |
396
400
  | Assume an unknown config keyword is ignored | Read the `ValueError` and fix the name | A silently dropped override means a user's tuning quietly does nothing (`core.py`) |
397
- | Read `LogPattern.template` as the final Drain3 template | Treat it as a first-seen snapshot until #5 lands | `extract_patterns` captures `cluster.get_template()` when a cluster FIRST appears, before later lines widen slots to `<*>`, so a varying cluster stores its raw first line. The `<*>`-count priority boost then runs on that stale snapshot (#5, open) |
401
+ | Assume every template is the final Drain3 one | Allow for a cluster the `max_clusters` cap evicted | `extract_patterns` re-reads each template from the live cluster once the feed ends, so a varying cluster carries its `<*>` slots and the priority boost counts them. A cluster LRU-evicted mid-feed is gone by then and keeps the template it had when first seen (`patterns.py`) |
398
402
  | Hand-edit the version | Land a conventional commit and let semantic-release do it | The old regime wrote `VERSION` by hand and took five commits to get tagging working. CONTRIBUTING makes it a standing rule |
399
403
 
400
404
  ### Where this sits
@@ -407,7 +411,7 @@ One declared edge in `dfe-infra/suite.yaml`, and it runs the surprising way.
407
411
 
408
412
  `dfe-stack suite --consumer logreducer` returns an empty edge list: no repo in
409
413
  that graph is something this library depends on. The runtime dependencies
410
- (drain3, psutil, loguru, numpy, scikit-learn, typer) are packages, declared in
414
+ (psutil, numpy, scikit-learn, typer) are packages, declared in
411
415
  `pyproject.toml`, which is where they belong.
412
416
 
413
417
  The reason dfe-engine gives for not declaring the dependency -- pin it "once it
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "logreducer"
7
- version = "3.5.1"
7
+ version = "3.5.3"
8
8
  description = "Reduce GB-scale logs to a representative sample - streaming Python library and CLI with pattern mining and anomaly detection"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -35,12 +35,10 @@ keywords = [
35
35
  "anomaly-detection",
36
36
  ]
37
37
  dependencies = [
38
- "drain3>=0.9.0",
39
- "psutil>=7.2.0",
40
- "loguru>=0.7.3",
41
- "numpy>=2.0.0",
42
- "scikit-learn>=1.5.0",
43
- "typer>=0.25.0",
38
+ "psutil>=7.2.2",
39
+ "numpy>=2.5.3",
40
+ "scikit-learn>=1.9.1",
41
+ "typer>=0.27.3",
44
42
  ]
45
43
 
46
44
  [[project.authors]]
@@ -53,12 +51,12 @@ email = "dev@hyperi.io"
53
51
 
54
52
  [project.optional-dependencies]
55
53
  enhanced = [
56
- "xxhash>=3.0.0",
57
- "datasketch>=1.5.0",
54
+ "xxhash>=4.0.1",
55
+ "datasketch>=2.0.0",
58
56
  ]
59
- sql = ["sqlalchemy>=2.0.30"]
60
- clickhouse = ["clickhouse-connect>=1.0"]
61
- kafka = ["confluent-kafka>=2.14.0"]
57
+ sql = ["sqlalchemy>=2.1.3"]
58
+ clickhouse = ["clickhouse-connect>=1.9.0"]
59
+ kafka = ["confluent-kafka>=2.15.1"]
62
60
 
63
61
  [project.urls]
64
62
  Homepage = "https://github.com/hyperi-io/logreducer"
@@ -72,6 +70,7 @@ logreducer = "logreducer.cli:main"
72
70
 
73
71
  [tool.uv]
74
72
  index-strategy = "unsafe-best-match"
73
+ exclude-newer = "7 days"
75
74
  override-dependencies = []
76
75
 
77
76
  [[tool.uv.index]]
@@ -150,6 +149,7 @@ ignore = [
150
149
  fixable = ["ALL"]
151
150
  unfixable = []
152
151
  dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
152
+ external = ["V"]
153
153
 
154
154
  [tool.ruff.lint.per-file-ignores]
155
155
  "tests/**" = [
@@ -164,6 +164,15 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
164
164
  "PT012",
165
165
  "RUF043",
166
166
  ]
167
+ "src/**" = [
168
+ "D105",
169
+ "D107",
170
+ ]
171
+ "src/logreducer/sampling.py" = ["S608"]
172
+ "src/logreducer/clickhouse.py" = ["S608"]
173
+ "src/logreducer/core.py" = ["S311"]
174
+ "src/logreducer/memory.py" = ["S311"]
175
+ "src/logreducer/target.py" = ["S311"]
167
176
 
168
177
  [tool.ruff.lint.pydocstyle]
169
178
  convention = "google"
@@ -260,40 +269,43 @@ exclude = [
260
269
  "scripts/",
261
270
  ]
262
271
 
272
+ [tool.vulture]
273
+ min_confidence = 80
274
+
263
275
  [tool.vermin]
264
276
  targets = ["3.12"]
265
277
  no_tips = true
266
278
 
267
279
  [tool.hatch.build.targets.sdist]
268
280
  exclude = [
269
- "/ci",
281
+ "/CURSOR.md",
270
282
  "/GEMINI.md",
271
- "/hyperi-ai",
283
+ "/.cursor",
272
284
  "/.windsurf",
273
- "/CURSOR.md",
274
- "/CLAUDE.md",
275
- "/.claude",
276
285
  "/.gemini",
286
+ "/CLAUDE.md",
277
287
  "/.github/copilot-instructions.md",
288
+ "/.claude",
289
+ "/ci",
290
+ "/hyperi-ai",
278
291
  "/STATE.md",
279
- "/.cursor",
280
292
  ]
281
293
 
282
294
  [dependency-groups]
283
295
  dev = [
284
296
  "pytest>=9.0.0",
285
297
  "pytest-cov>=7.0.0",
286
- "ruff>=0.15.0",
298
+ "ruff>=0.16.10",
287
299
  "ty>=0.0.34",
288
300
  "mypy>=2.0",
289
301
  "bandit[toml]>=1.7.0",
290
302
  "pip-audit>=2.6.0",
291
303
  "vulture>=2.16",
292
304
  "vermin>=1.6.0",
293
- "sqlalchemy>=2.0.30",
294
- "clickhouse-connect>=1.0",
295
- "confluent-kafka>=2.14.0",
296
- "testcontainers[clickhouse,kafka,postgres,mysql]>=4.14.0",
305
+ "sqlalchemy>=2.1.3",
306
+ "clickhouse-connect>=1.9.0",
307
+ "confluent-kafka>=2.15.1",
308
+ "testcontainers[clickhouse,kafka,postgres,mysql]>=4.15.0",
297
309
  "psycopg[binary]>=3.2",
298
310
  "pymysql>=1.1.0",
299
311
  "python-dotenv>=1.0.0",
@@ -6,7 +6,7 @@ build-backend = "uv_build"
6
6
 
7
7
  [project]
8
8
  name = "logreducer"
9
- version = "3.5.1"
9
+ version = "3.5.3"
10
10
  authors = [
11
11
  { name = "HyperI Team", email = "dev@hyperi.io" },
12
12
  ]
@@ -43,27 +43,25 @@ keywords = [
43
43
  "anomaly-detection",
44
44
  ]
45
45
  dependencies = [
46
- "drain3>=0.9.0",
47
- "psutil>=7.2.0",
48
- "loguru>=0.7.3",
49
- "numpy>=2.0.0",
50
- "scikit-learn>=1.5.0",
51
- "typer>=0.25.0",
46
+ "psutil>=7.2.2",
47
+ "numpy>=2.5.3",
48
+ "scikit-learn>=1.9.1",
49
+ "typer>=0.27.3",
52
50
  ]
53
51
 
54
52
  [project.optional-dependencies]
55
53
  enhanced = [
56
- "xxhash>=3.0.0",
57
- "datasketch>=1.5.0",
54
+ "xxhash>=4.0.1",
55
+ "datasketch>=2.0.0",
58
56
  ]
59
57
  sql = [
60
- "sqlalchemy>=2.0.30",
58
+ "sqlalchemy>=2.1.3",
61
59
  ]
62
60
  clickhouse = [
63
- "clickhouse-connect>=1.0",
61
+ "clickhouse-connect>=1.9.0",
64
62
  ]
65
63
  kafka = [
66
- "confluent-kafka>=2.14.0",
64
+ "confluent-kafka>=2.15.1",
67
65
  ]
68
66
 
69
67
  [project.urls]
@@ -78,6 +76,7 @@ logreducer = "logreducer.cli:main"
78
76
 
79
77
  [tool.uv]
80
78
  index-strategy = "unsafe-best-match"
79
+ exclude-newer = "7 days"
81
80
  override-dependencies = []
82
81
  index = [
83
82
  { name = "pypi", url = "https://pypi.org/simple", default = true },
@@ -160,6 +159,9 @@ fixable = [
160
159
  ]
161
160
  unfixable = []
162
161
  dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
162
+ external = [
163
+ "V",
164
+ ]
163
165
 
164
166
  [tool.ruff.lint.per-file-ignores]
165
167
  "tests/**" = [
@@ -174,6 +176,25 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
174
176
  "PT012",
175
177
  "RUF043",
176
178
  ]
179
+ "src/**" = [
180
+ "D105",
181
+ "D107",
182
+ ]
183
+ "src/logreducer/sampling.py" = [
184
+ "S608",
185
+ ]
186
+ "src/logreducer/clickhouse.py" = [
187
+ "S608",
188
+ ]
189
+ "src/logreducer/core.py" = [
190
+ "S311",
191
+ ]
192
+ "src/logreducer/memory.py" = [
193
+ "S311",
194
+ ]
195
+ "src/logreducer/target.py" = [
196
+ "S311",
197
+ ]
177
198
 
178
199
  [tool.ruff.lint.pydocstyle]
179
200
  convention = "google"
@@ -278,6 +299,9 @@ exclude = [
278
299
  "scripts/",
279
300
  ]
280
301
 
302
+ [tool.vulture]
303
+ min_confidence = 80
304
+
281
305
  [tool.vermin]
282
306
  targets = [
283
307
  "3.12",
@@ -286,34 +310,34 @@ no_tips = true
286
310
 
287
311
  [tool.hatch.build.targets.sdist]
288
312
  exclude = [
289
- "/ci",
313
+ "/CURSOR.md",
290
314
  "/GEMINI.md",
291
- "/hyperi-ai",
315
+ "/.cursor",
292
316
  "/.windsurf",
293
- "/CURSOR.md",
294
- "/CLAUDE.md",
295
- "/.claude",
296
317
  "/.gemini",
318
+ "/CLAUDE.md",
297
319
  "/.github/copilot-instructions.md",
320
+ "/.claude",
321
+ "/ci",
322
+ "/hyperi-ai",
298
323
  "/STATE.md",
299
- "/.cursor",
300
324
  ]
301
325
 
302
326
  [dependency-groups]
303
327
  dev = [
304
328
  "pytest>=9.0.0",
305
329
  "pytest-cov>=7.0.0",
306
- "ruff>=0.15.0",
330
+ "ruff>=0.16.10",
307
331
  "ty>=0.0.34",
308
332
  "mypy>=2.0",
309
333
  "bandit[toml]>=1.7.0",
310
334
  "pip-audit>=2.6.0",
311
335
  "vulture>=2.16",
312
336
  "vermin>=1.6.0",
313
- "sqlalchemy>=2.0.30",
314
- "clickhouse-connect>=1.0",
315
- "confluent-kafka>=2.14.0",
316
- "testcontainers[clickhouse,kafka,postgres,mysql]>=4.14.0",
337
+ "sqlalchemy>=2.1.3",
338
+ "clickhouse-connect>=1.9.0",
339
+ "confluent-kafka>=2.15.1",
340
+ "testcontainers[clickhouse,kafka,postgres,mysql]>=4.15.0",
317
341
  "psycopg[binary]>=3.2",
318
342
  "pymysql>=1.1.0",
319
343
  "python-dotenv>=1.0.0",
@@ -1,8 +1,8 @@
1
- """
2
- Anomaly detection utilities
3
- """
1
+ """Anomaly detection utilities."""
4
2
 
5
- from loguru import logger
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
6
 
7
7
  # Try optional imports
8
8
  try:
@@ -15,7 +15,7 @@ except ImportError:
15
15
 
16
16
 
17
17
  class AnomalyDetector:
18
- """Detect anomalous log lines"""
18
+ """Detect anomalous log lines."""
19
19
 
20
20
  def __init__(self, contamination: float = 0.1):
21
21
  self.contamination = contamination
@@ -25,8 +25,7 @@ class AnomalyDetector:
25
25
  logger.warning("scikit-learn not available, anomaly detection disabled")
26
26
 
27
27
  def detect_anomalies(self, lines: list[str]) -> tuple[list[str], list[str]]:
28
- """
29
- Separate anomalous and normal lines
28
+ """Separate anomalous and normal lines.
30
29
 
31
30
  Returns:
32
31
  (anomalous_lines, normal_lines)
@@ -1,5 +1,6 @@
1
1
  """Configuration and tuning parameters for LogReducer."""
2
2
 
3
+ import logging
3
4
  import os
4
5
  import types
5
6
  import typing
@@ -7,7 +8,8 @@ from dataclasses import dataclass, fields
7
8
  from enum import Enum
8
9
 
9
10
  import psutil
10
- from loguru import logger
11
+
12
+ logger = logging.getLogger(__name__)
11
13
 
12
14
 
13
15
  class ProcessingLevel(Enum):
@@ -1,6 +1,4 @@
1
- """
2
- Core LogReducer implementation
3
- """
1
+ """Core LogReducer implementation."""
4
2
 
5
3
  import json
6
4
  import os
@@ -10,7 +8,7 @@ from dataclasses import replace
10
8
  from datetime import datetime
11
9
  from enum import Enum
12
10
  from pathlib import Path
13
- from typing import Any
11
+ from typing import Any, Literal, overload
14
12
 
15
13
  from .anomaly import AnomalyDetector
16
14
  from .config import BigDialConfig, OutputFormat, ProcessingLevel, ProcessingMode, get_preset_config
@@ -24,8 +22,7 @@ from .temporal import TemporalProcessor
24
22
 
25
23
 
26
24
  class LogReducer:
27
- """
28
- Main log reduction class
25
+ """Main log reduction class.
29
26
 
30
27
  Example:
31
28
  reducer = LogReducer(level="enhanced")
@@ -41,8 +38,7 @@ class LogReducer:
41
38
  config: BigDialConfig | None = None,
42
39
  **kwargs: Any,
43
40
  ) -> None:
44
- """
45
- Initialize LogReducer
41
+ """Initialize LogReducer.
46
42
 
47
43
  Args:
48
44
  level: Processing level (standard/enhanced/maximum)
@@ -134,13 +130,41 @@ class LogReducer:
134
130
  else:
135
131
  self.temporal_processor = None
136
132
 
133
+ @overload
134
+ def reduce(
135
+ self,
136
+ source: Source,
137
+ output_file: str | None = None,
138
+ return_metadata: Literal[False] = False,
139
+ sink: Sink | None = None,
140
+ ) -> list[str]: ...
141
+
142
+ @overload
143
+ def reduce(
144
+ self,
145
+ source: Source,
146
+ output_file: str | None = None,
147
+ *,
148
+ return_metadata: Literal[True],
149
+ sink: Sink | None = None,
150
+ ) -> dict[str, Any]: ...
151
+
152
+ @overload
137
153
  def reduce(
138
154
  self,
139
155
  source: Source,
140
156
  output_file: str | None = None,
141
157
  return_metadata: bool = False,
142
158
  sink: Sink | None = None,
143
- ) -> list[str] | dict:
159
+ ) -> list[str] | dict[str, Any]: ...
160
+
161
+ def reduce(
162
+ self,
163
+ source: Source,
164
+ output_file: str | None = None,
165
+ return_metadata: bool = False,
166
+ sink: Sink | None = None,
167
+ ) -> list[str] | dict[str, Any]:
144
168
  """Reduce a source of log lines to a representative sample.
145
169
 
146
170
  This is the core entry point: it operates on an abstraction, not on IO.
@@ -241,12 +265,37 @@ class LogReducer:
241
265
  }
242
266
  return result_lines
243
267
 
268
+ @overload
269
+ def process_file(
270
+ self,
271
+ input_file: str,
272
+ output_file: str | None = None,
273
+ return_metadata: Literal[False] = False,
274
+ ) -> list[str]: ...
275
+
276
+ @overload
277
+ def process_file(
278
+ self,
279
+ input_file: str,
280
+ output_file: str | None = None,
281
+ *,
282
+ return_metadata: Literal[True],
283
+ ) -> dict[str, Any]: ...
284
+
285
+ @overload
286
+ def process_file(
287
+ self,
288
+ input_file: str,
289
+ output_file: str | None = None,
290
+ return_metadata: bool = False,
291
+ ) -> list[str] | dict[str, Any]: ...
292
+
244
293
  def process_file(
245
294
  self,
246
295
  input_file: str,
247
296
  output_file: str | None = None,
248
297
  return_metadata: bool = False,
249
- ) -> list[str] | dict:
298
+ ) -> list[str] | dict[str, Any]:
250
299
  """Reduce a log file - convenience wrapper over reduce() + FileSource.
251
300
 
252
301
  Args:
@@ -302,7 +351,7 @@ class LogReducer:
302
351
  return unique_lines
303
352
 
304
353
  def _process_anomaly_mode(self, source: Source) -> list[str]:
305
- """Process using anomaly detection"""
354
+ """Process using anomaly detection."""
306
355
  if not self.anomaly_detector:
307
356
  if self.config.enable_logging:
308
357
  self.logger.warning("Anomaly detector not available, falling back to pattern mode")
@@ -329,7 +378,7 @@ class LogReducer:
329
378
  return result
330
379
 
331
380
  def _process_temporal_mode(self, source: Source) -> list[str]:
332
- """Process using temporal analysis"""
381
+ """Process using temporal analysis."""
333
382
  if not self.temporal_processor:
334
383
  if self.config.enable_logging:
335
384
  self.logger.warning("Temporal processor not available, falling back to pattern mode")
@@ -356,7 +405,7 @@ class LogReducer:
356
405
  return result
357
406
 
358
407
  def _process_hybrid_mode(self, source: Source) -> list[str]:
359
- """Process using combined approach"""
408
+ """Process using combined approach."""
360
409
  if self.config.enable_logging:
361
410
  self.logger.info("Hybrid mode: combining pattern and anomaly detection")
362
411
 
@@ -400,7 +449,7 @@ class LogReducer:
400
449
  }
401
450
 
402
451
  def _save_output(self, lines: list[str], output_file: str) -> None:
403
- """Save output to file in specified format"""
452
+ """Save output to file in specified format."""
404
453
  output_path = Path(output_file)
405
454
  output_path.parent.mkdir(parents=True, exist_ok=True)
406
455
 
@@ -453,7 +502,7 @@ class LogReducer:
453
502
  self.logger.info(f"Metadata saved to {meta_file}")
454
503
 
455
504
  def _print_summary(self) -> None:
456
- """Log processing summary"""
505
+ """Log processing summary."""
457
506
  self.logger.info("\n" + "=" * 60)
458
507
  self.logger.info("LOG REDUCTION SUMMARY")
459
508
  self.logger.info("=" * 60)
@@ -474,7 +523,7 @@ class LogReducer:
474
523
  self.logger.info("=" * 60)
475
524
 
476
525
  def estimate_processing(self, file_path: str) -> dict:
477
- """Estimate processing requirements before running"""
526
+ """Estimate processing requirements before running."""
478
527
  file_size = os.path.getsize(file_path)
479
528
  file_size_gb = file_size / (1024**3)
480
529