seedsyncarr 1.1.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.
- seedsyncarr-1.1.1/.gitignore +29 -0
- seedsyncarr-1.1.1/PKG-INFO +26 -0
- seedsyncarr-1.1.1/__init__.py +0 -0
- seedsyncarr-1.1.1/common/__init__.py +13 -0
- seedsyncarr-1.1.1/common/app_process.py +166 -0
- seedsyncarr-1.1.1/common/bounded_ordered_set.py +203 -0
- seedsyncarr-1.1.1/common/config.py +596 -0
- seedsyncarr-1.1.1/common/constants.py +12 -0
- seedsyncarr-1.1.1/common/context.py +70 -0
- seedsyncarr-1.1.1/common/encryption.py +160 -0
- seedsyncarr-1.1.1/common/error.py +18 -0
- seedsyncarr-1.1.1/common/job.py +89 -0
- seedsyncarr-1.1.1/common/localization.py +7 -0
- seedsyncarr-1.1.1/common/multiprocessing_logger.py +87 -0
- seedsyncarr-1.1.1/common/persist.py +59 -0
- seedsyncarr-1.1.1/common/status.py +189 -0
- seedsyncarr-1.1.1/common/types.py +14 -0
- seedsyncarr-1.1.1/controller/__init__.py +11 -0
- seedsyncarr-1.1.1/controller/auto_queue.py +375 -0
- seedsyncarr-1.1.1/controller/controller.py +1115 -0
- seedsyncarr-1.1.1/controller/controller_job.py +29 -0
- seedsyncarr-1.1.1/controller/controller_persist.py +248 -0
- seedsyncarr-1.1.1/controller/delete/__init__.py +1 -0
- seedsyncarr-1.1.1/controller/delete/delete_process.py +50 -0
- seedsyncarr-1.1.1/controller/extract/__init__.py +3 -0
- seedsyncarr-1.1.1/controller/extract/dispatch.py +228 -0
- seedsyncarr-1.1.1/controller/extract/extract.py +61 -0
- seedsyncarr-1.1.1/controller/extract/extract_process.py +124 -0
- seedsyncarr-1.1.1/controller/file_operation_manager.py +228 -0
- seedsyncarr-1.1.1/controller/lftp_manager.py +157 -0
- seedsyncarr-1.1.1/controller/memory_monitor.py +256 -0
- seedsyncarr-1.1.1/controller/model_builder.py +581 -0
- seedsyncarr-1.1.1/controller/scan/__init__.py +4 -0
- seedsyncarr-1.1.1/controller/scan/active_scanner.py +52 -0
- seedsyncarr-1.1.1/controller/scan/local_scanner.py +41 -0
- seedsyncarr-1.1.1/controller/scan/remote_scanner.py +179 -0
- seedsyncarr-1.1.1/controller/scan/scanner_process.py +131 -0
- seedsyncarr-1.1.1/controller/scan_manager.py +167 -0
- seedsyncarr-1.1.1/controller/webhook_manager.py +92 -0
- seedsyncarr-1.1.1/docs/arr-setup.md +109 -0
- seedsyncarr-1.1.1/docs/configuration.md +81 -0
- seedsyncarr-1.1.1/docs/faq.md +52 -0
- seedsyncarr-1.1.1/docs/images/favicon.png +0 -0
- seedsyncarr-1.1.1/docs/images/logo.png +0 -0
- seedsyncarr-1.1.1/docs/images/screenshot-dashboard.png +0 -0
- seedsyncarr-1.1.1/docs/index.md +43 -0
- seedsyncarr-1.1.1/docs/install.md +67 -0
- seedsyncarr-1.1.1/lftp/__init__.py +14 -0
- seedsyncarr-1.1.1/lftp/job_status.py +99 -0
- seedsyncarr-1.1.1/lftp/job_status_parser.py +749 -0
- seedsyncarr-1.1.1/lftp/lftp.py +387 -0
- seedsyncarr-1.1.1/mkdocs.yml +69 -0
- seedsyncarr-1.1.1/model/__init__.py +3 -0
- seedsyncarr-1.1.1/model/diff.py +79 -0
- seedsyncarr-1.1.1/model/file.py +316 -0
- seedsyncarr-1.1.1/model/model.py +136 -0
- seedsyncarr-1.1.1/poetry.lock +1550 -0
- seedsyncarr-1.1.1/pyproject.toml +101 -0
- seedsyncarr-1.1.1/scan_fs.py +40 -0
- seedsyncarr-1.1.1/seedsyncarr.py +518 -0
- seedsyncarr-1.1.1/ssh/__init__.py +1 -0
- seedsyncarr-1.1.1/ssh/sshcp.py +208 -0
- seedsyncarr-1.1.1/system/__init__.py +2 -0
- seedsyncarr-1.1.1/system/file.py +86 -0
- seedsyncarr-1.1.1/system/scanner.py +216 -0
- seedsyncarr-1.1.1/tests/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/conftest.py +106 -0
- seedsyncarr-1.1.1/tests/integration/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/integration/test_controller/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/integration/test_controller/test_controller.py +2424 -0
- seedsyncarr-1.1.1/tests/integration/test_controller/test_extract/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/integration/test_controller/test_extract/test_extract.py +209 -0
- seedsyncarr-1.1.1/tests/integration/test_lftp/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/integration/test_lftp/test_lftp.py +164 -0
- seedsyncarr-1.1.1/tests/integration/test_web/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/integration/test_web/test_handler/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/integration/test_web/test_handler/test_auto_queue.py +140 -0
- seedsyncarr-1.1.1/tests/integration/test_web/test_handler/test_config.py +83 -0
- seedsyncarr-1.1.1/tests/integration/test_web/test_handler/test_controller.py +182 -0
- seedsyncarr-1.1.1/tests/integration/test_web/test_handler/test_server.py +10 -0
- seedsyncarr-1.1.1/tests/integration/test_web/test_handler/test_status.py +11 -0
- seedsyncarr-1.1.1/tests/integration/test_web/test_handler/test_stream_log.py +42 -0
- seedsyncarr-1.1.1/tests/integration/test_web/test_handler/test_stream_model.py +79 -0
- seedsyncarr-1.1.1/tests/integration/test_web/test_handler/test_stream_status.py +52 -0
- seedsyncarr-1.1.1/tests/integration/test_web/test_web_app.py +63 -0
- seedsyncarr-1.1.1/tests/unittests/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/unittests/test_common/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/unittests/test_common/test_app_process.py +214 -0
- seedsyncarr-1.1.1/tests/unittests/test_common/test_bounded_ordered_set.py +303 -0
- seedsyncarr-1.1.1/tests/unittests/test_common/test_config.py +1123 -0
- seedsyncarr-1.1.1/tests/unittests/test_common/test_constants.py +46 -0
- seedsyncarr-1.1.1/tests/unittests/test_common/test_context.py +118 -0
- seedsyncarr-1.1.1/tests/unittests/test_common/test_encryption.py +131 -0
- seedsyncarr-1.1.1/tests/unittests/test_common/test_error.py +61 -0
- seedsyncarr-1.1.1/tests/unittests/test_common/test_job.py +42 -0
- seedsyncarr-1.1.1/tests/unittests/test_common/test_localization.py +48 -0
- seedsyncarr-1.1.1/tests/unittests/test_common/test_multiprocessing_logger.py +154 -0
- seedsyncarr-1.1.1/tests/unittests/test_common/test_persist.py +99 -0
- seedsyncarr-1.1.1/tests/unittests/test_common/test_status.py +239 -0
- seedsyncarr-1.1.1/tests/unittests/test_common/test_types.py +77 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_auto_delete.py +495 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_auto_queue.py +1706 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_controller.py +169 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_controller_job.py +40 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_controller_persist.py +281 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_controller_unit.py +1157 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_extract/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_extract/test_dispatch.py +860 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_extract/test_extract_process.py +278 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_file_operation_manager.py +441 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_lftp_manager.py +235 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_memory_monitor.py +164 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_model_builder.py +1467 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_scan/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_scan/test_local_scanner.py +110 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_scan/test_remote_scanner.py +847 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_scan/test_scanner_process.py +193 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_scan_manager.py +246 -0
- seedsyncarr-1.1.1/tests/unittests/test_controller/test_webhook_manager.py +115 -0
- seedsyncarr-1.1.1/tests/unittests/test_lftp/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/unittests/test_lftp/test_job_status.py +232 -0
- seedsyncarr-1.1.1/tests/unittests/test_lftp/test_job_status_parser.py +1464 -0
- seedsyncarr-1.1.1/tests/unittests/test_lftp/test_job_status_parser_components.py +516 -0
- seedsyncarr-1.1.1/tests/unittests/test_lftp/test_lftp.py +752 -0
- seedsyncarr-1.1.1/tests/unittests/test_model/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/unittests/test_model/test_diff.py +115 -0
- seedsyncarr-1.1.1/tests/unittests/test_model/test_file.py +253 -0
- seedsyncarr-1.1.1/tests/unittests/test_model/test_model.py +166 -0
- seedsyncarr-1.1.1/tests/unittests/test_seedsyncarr.py +408 -0
- seedsyncarr-1.1.1/tests/unittests/test_ssh/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/unittests/test_ssh/test_sshcp.py +225 -0
- seedsyncarr-1.1.1/tests/unittests/test_system/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/unittests/test_system/test_file.py +82 -0
- seedsyncarr-1.1.1/tests/unittests/test_system/test_scanner.py +659 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_auth.py +242 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_handler/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_handler/test_auto_queue_handler.py +109 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_handler/test_config_handler.py +369 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_handler/test_controller_handler.py +834 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_handler/test_server_handler.py +52 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_handler/test_status_handler.py +28 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_handler/test_stream_heartbeat.py +115 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_handler/test_stream_log.py +219 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_handler/test_stream_model_handler.py +144 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_handler/test_stream_status_handler.py +95 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_serialize/__init__.py +0 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_serialize/test_serialize.py +15 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_serialize/test_serialize_auto_queue.py +37 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_serialize/test_serialize_config.py +212 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_serialize/test_serialize_log_record.py +291 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_serialize/test_serialize_model.py +332 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_serialize/test_serialize_status.py +145 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_stream_queue.py +91 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_web_app.py +228 -0
- seedsyncarr-1.1.1/tests/unittests/test_web/test_webhook_handler.py +298 -0
- seedsyncarr-1.1.1/tests/utils.py +24 -0
- seedsyncarr-1.1.1/web/__init__.py +3 -0
- seedsyncarr-1.1.1/web/handler/__init__.py +0 -0
- seedsyncarr-1.1.1/web/handler/auto_queue.py +50 -0
- seedsyncarr-1.1.1/web/handler/config.py +177 -0
- seedsyncarr-1.1.1/web/handler/controller.py +483 -0
- seedsyncarr-1.1.1/web/handler/server.py +34 -0
- seedsyncarr-1.1.1/web/handler/status.py +17 -0
- seedsyncarr-1.1.1/web/handler/stream_heartbeat.py +58 -0
- seedsyncarr-1.1.1/web/handler/stream_log.py +116 -0
- seedsyncarr-1.1.1/web/handler/stream_model.py +71 -0
- seedsyncarr-1.1.1/web/handler/stream_status.py +47 -0
- seedsyncarr-1.1.1/web/handler/webhook.py +193 -0
- seedsyncarr-1.1.1/web/serialize/__init__.py +6 -0
- seedsyncarr-1.1.1/web/serialize/serialize.py +13 -0
- seedsyncarr-1.1.1/web/serialize/serialize_auto_queue.py +17 -0
- seedsyncarr-1.1.1/web/serialize/serialize_config.py +38 -0
- seedsyncarr-1.1.1/web/serialize/serialize_log_record.py +89 -0
- seedsyncarr-1.1.1/web/serialize/serialize_model.py +110 -0
- seedsyncarr-1.1.1/web/serialize/serialize_status.py +68 -0
- seedsyncarr-1.1.1/web/utils.py +95 -0
- seedsyncarr-1.1.1/web/web_app.py +297 -0
- seedsyncarr-1.1.1/web/web_app_builder.py +62 -0
- seedsyncarr-1.1.1/web/web_app_job.py +79 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
.idea
|
|
2
|
+
*.pyc
|
|
3
|
+
/build
|
|
4
|
+
.venv
|
|
5
|
+
src/python/build
|
|
6
|
+
src/python/site
|
|
7
|
+
htmlcov/
|
|
8
|
+
.coverage
|
|
9
|
+
node_modules/
|
|
10
|
+
__pycache__/
|
|
11
|
+
src/python/.pytest_cache/
|
|
12
|
+
src/python/htmlcov/
|
|
13
|
+
src/angular/dist/
|
|
14
|
+
.DS_Store
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
|
|
18
|
+
# SSH private keys
|
|
19
|
+
id_rsa
|
|
20
|
+
*.pem
|
|
21
|
+
# AI tooling (local only)
|
|
22
|
+
.agents/
|
|
23
|
+
.aidesigner/*
|
|
24
|
+
!.aidesigner/.gitkeep
|
|
25
|
+
.claude/
|
|
26
|
+
.mcp.json
|
|
27
|
+
.planning/tmp/
|
|
28
|
+
docs/superpowers/
|
|
29
|
+
shield-claude-skill/
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: seedsyncarr
|
|
3
|
+
Version: 1.1.1
|
|
4
|
+
Summary: Fast file syncing from remote servers with a web UI, powered by LFTP
|
|
5
|
+
Author: thejuran
|
|
6
|
+
Requires-Python: <3.13,>=3.11
|
|
7
|
+
Requires-Dist: bottle>=0.13.4
|
|
8
|
+
Requires-Dist: cryptography<47,>=44.0.0
|
|
9
|
+
Requires-Dist: paste>=3.10.1
|
|
10
|
+
Requires-Dist: patool>=4.0.3
|
|
11
|
+
Requires-Dist: pexpect>=4.9.0
|
|
12
|
+
Requires-Dist: pytz>=2025.2
|
|
13
|
+
Requires-Dist: requests>=2.33.0
|
|
14
|
+
Requires-Dist: tblib>=3.2.2
|
|
15
|
+
Requires-Dist: timeout-decorator>=0.5.0
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: mkdocs-material>=9.7.1; extra == 'dev'
|
|
18
|
+
Requires-Dist: mkdocs>=1.6.1; extra == 'dev'
|
|
19
|
+
Requires-Dist: parameterized>=0.9.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: pyinstaller>=6.0.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-timeout>=2.3.1; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=9.0.3; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: testfixtures>=11.0.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: webtest>=3.0.7; extra == 'dev'
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .types import overrides as overrides
|
|
2
|
+
from .job import Job as Job
|
|
3
|
+
from .context import Context as Context, Args as Args
|
|
4
|
+
from .error import AppError as AppError, ServiceExit as ServiceExit, ServiceRestart as ServiceRestart
|
|
5
|
+
from .encryption import EncryptionError as EncryptionError, DecryptionError as DecryptionError
|
|
6
|
+
from .constants import Constants as Constants
|
|
7
|
+
from .config import Config as Config, ConfigError as ConfigError
|
|
8
|
+
from .persist import Persist as Persist, PersistError as PersistError, Serializable as Serializable
|
|
9
|
+
from .localization import Localization as Localization
|
|
10
|
+
from .multiprocessing_logger import MultiprocessingLogger as MultiprocessingLogger
|
|
11
|
+
from .status import Status as Status, IStatusListener as IStatusListener, StatusComponent as StatusComponent, IStatusComponentListener as IStatusComponentListener
|
|
12
|
+
from .app_process import AppProcess as AppProcess, AppOneShotProcess as AppOneShotProcess
|
|
13
|
+
from .bounded_ordered_set import BoundedOrderedSet as BoundedOrderedSet
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
from abc import abstractmethod
|
|
5
|
+
from multiprocessing import Process, Queue, Event
|
|
6
|
+
import queue
|
|
7
|
+
import signal
|
|
8
|
+
import threading
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
import tblib.pickling_support
|
|
12
|
+
|
|
13
|
+
from common import overrides, ServiceExit, MultiprocessingLogger
|
|
14
|
+
|
|
15
|
+
tblib.pickling_support.install()
|
|
16
|
+
|
|
17
|
+
class ExceptionWrapper:
|
|
18
|
+
"""
|
|
19
|
+
An exception wrapper that works across processes
|
|
20
|
+
Source: https://stackoverflow.com/a/26096355/8571324
|
|
21
|
+
"""
|
|
22
|
+
def __init__(self, ee):
|
|
23
|
+
self.ee = ee
|
|
24
|
+
__, __, self.tb = sys.exc_info()
|
|
25
|
+
|
|
26
|
+
def re_raise(self):
|
|
27
|
+
raise self.ee.with_traceback(self.tb)
|
|
28
|
+
|
|
29
|
+
class AppProcess(Process):
|
|
30
|
+
"""
|
|
31
|
+
Process with some additional functionality and fixes
|
|
32
|
+
* Support for a multiprocessing logger
|
|
33
|
+
* Removes signals to prevent join problems
|
|
34
|
+
* Propagates exceptions to owner process
|
|
35
|
+
* Safe terminate with timeout, followed by force terminate
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# Timeout before process is force terminated
|
|
39
|
+
__DEFAULT_TERMINATE_TIMEOUT_MS = 1000
|
|
40
|
+
|
|
41
|
+
def __init__(self, name: str):
|
|
42
|
+
self.__name = name
|
|
43
|
+
super().__init__(name=self.__name)
|
|
44
|
+
|
|
45
|
+
self.mp_logger = None
|
|
46
|
+
self.logger = logging.getLogger(self.__name)
|
|
47
|
+
self.__exception_queue = Queue()
|
|
48
|
+
self._terminate = Event()
|
|
49
|
+
|
|
50
|
+
def set_multiprocessing_logger(self, mp_logger: MultiprocessingLogger):
|
|
51
|
+
self.mp_logger = mp_logger
|
|
52
|
+
|
|
53
|
+
@overrides(Process)
|
|
54
|
+
def run(self):
|
|
55
|
+
# Replace the signal handlers that may have been set by main process to
|
|
56
|
+
# default handlers. Having non-default handlers in subprocesses causes
|
|
57
|
+
# a deadlock when attempting to join the process
|
|
58
|
+
# Info: https://stackoverflow.com/a/631605
|
|
59
|
+
|
|
60
|
+
# NOTE: There is a minuscule chance of deadlock if a signal is received
|
|
61
|
+
# between start of the method and these resets.
|
|
62
|
+
# The ideal solution is to remove the signal before the process is
|
|
63
|
+
# started. Unfortunately that's difficult to do here because the
|
|
64
|
+
# subprocess is started from a job thread, and python doesn't
|
|
65
|
+
# allow setting signals from outside the main thread.
|
|
66
|
+
# So we accept this risk for the quick and easy solution here
|
|
67
|
+
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
|
68
|
+
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
|
69
|
+
|
|
70
|
+
# Set the thread name for convenience
|
|
71
|
+
threading.current_thread().name = self.__name
|
|
72
|
+
|
|
73
|
+
# Configure the logger for this process
|
|
74
|
+
if self.mp_logger:
|
|
75
|
+
self.logger = self.mp_logger.get_process_safe_logger().getChild(self.__name)
|
|
76
|
+
|
|
77
|
+
self.logger.debug("Started process")
|
|
78
|
+
|
|
79
|
+
self.run_init()
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
while not self._terminate.is_set():
|
|
83
|
+
self.run_loop()
|
|
84
|
+
self.logger.debug("Process received terminate flag")
|
|
85
|
+
except ServiceExit:
|
|
86
|
+
self.logger.debug("Process received a ServiceExit")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
self.logger.debug("Process caught an exception")
|
|
89
|
+
self.__exception_queue.put(ExceptionWrapper(e))
|
|
90
|
+
raise
|
|
91
|
+
finally:
|
|
92
|
+
self.run_cleanup()
|
|
93
|
+
|
|
94
|
+
self.logger.debug("Exiting process")
|
|
95
|
+
|
|
96
|
+
@overrides(Process)
|
|
97
|
+
def terminate(self):
|
|
98
|
+
# Send a terminate signal, and force terminate after a timeout
|
|
99
|
+
self._terminate.set()
|
|
100
|
+
|
|
101
|
+
def elapsed_ms(start):
|
|
102
|
+
delta_in_s = (datetime.now() - start).total_seconds()
|
|
103
|
+
delta_in_ms = int(delta_in_s * 1000)
|
|
104
|
+
return delta_in_ms
|
|
105
|
+
|
|
106
|
+
timestamp_start = datetime.now()
|
|
107
|
+
while self.is_alive() and \
|
|
108
|
+
elapsed_ms(timestamp_start) < AppProcess.__DEFAULT_TERMINATE_TIMEOUT_MS:
|
|
109
|
+
time.sleep(0.01) # 10ms polling interval to avoid CPU spin
|
|
110
|
+
|
|
111
|
+
super().terminate()
|
|
112
|
+
|
|
113
|
+
def propagate_exception(self):
|
|
114
|
+
"""
|
|
115
|
+
Raises any exception that was caught by the process
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
exc = self.__exception_queue.get(block=False)
|
|
119
|
+
exc.re_raise()
|
|
120
|
+
except queue.Empty:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def run_init(self):
|
|
125
|
+
"""
|
|
126
|
+
Called once before the run loop
|
|
127
|
+
"""
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
@abstractmethod
|
|
131
|
+
def run_cleanup(self):
|
|
132
|
+
"""
|
|
133
|
+
Called once before cleanup
|
|
134
|
+
"""
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
@abstractmethod
|
|
138
|
+
def run_loop(self):
|
|
139
|
+
"""
|
|
140
|
+
Process behaviour should be implemented here.
|
|
141
|
+
This function is repeatedly called until process exits.
|
|
142
|
+
The check for graceful shutdown is performed between the loop iterations,
|
|
143
|
+
so try to limit the run time for this method.
|
|
144
|
+
"""
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
class AppOneShotProcess(AppProcess):
|
|
148
|
+
"""
|
|
149
|
+
App process that runs only once and then exits
|
|
150
|
+
"""
|
|
151
|
+
def run_loop(self):
|
|
152
|
+
self.run_once()
|
|
153
|
+
self._terminate.set()
|
|
154
|
+
|
|
155
|
+
def run_cleanup(self):
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
def run_init(self):
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
@abstractmethod
|
|
162
|
+
def run_once(self):
|
|
163
|
+
"""
|
|
164
|
+
Process behaviour should be implemented here
|
|
165
|
+
"""
|
|
166
|
+
pass
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from collections import OrderedDict
|
|
2
|
+
from typing import TypeVar, Generic, Iterator, Iterable, Optional, Set
|
|
3
|
+
|
|
4
|
+
T = TypeVar('T')
|
|
5
|
+
|
|
6
|
+
class BoundedOrderedSet(Generic[T]):
|
|
7
|
+
"""
|
|
8
|
+
A set-like container with a maximum size that evicts oldest entries.
|
|
9
|
+
|
|
10
|
+
This class provides:
|
|
11
|
+
- Set semantics (unique elements, membership testing)
|
|
12
|
+
- Insertion order preservation
|
|
13
|
+
- Automatic LRU-style eviction when maxlen is reached
|
|
14
|
+
- O(1) membership testing, add, and remove operations
|
|
15
|
+
|
|
16
|
+
When maxlen is reached and a new item is added, the oldest item
|
|
17
|
+
(first inserted that hasn't been removed) is automatically evicted.
|
|
18
|
+
|
|
19
|
+
Thread-safety: Not thread-safe. Callers must provide external synchronization.
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
>>> bset = BoundedOrderedSet(maxlen=3)
|
|
23
|
+
>>> bset.add('a')
|
|
24
|
+
>>> bset.add('b')
|
|
25
|
+
>>> bset.add('c')
|
|
26
|
+
>>> bset.add('d') # 'a' is evicted
|
|
27
|
+
>>> list(bset)
|
|
28
|
+
['b', 'c', 'd']
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
# Default maximum size (10,000 files is reasonable for most use cases)
|
|
32
|
+
DEFAULT_MAXLEN = 10000
|
|
33
|
+
|
|
34
|
+
def __init__(self, maxlen: Optional[int] = None, iterable: Optional[Iterable[T]] = None):
|
|
35
|
+
"""
|
|
36
|
+
Initialize a bounded ordered set.
|
|
37
|
+
|
|
38
|
+
:param maxlen: Maximum number of elements. If None, uses DEFAULT_MAXLEN.
|
|
39
|
+
Must be positive.
|
|
40
|
+
:param iterable: Optional iterable of initial elements.
|
|
41
|
+
"""
|
|
42
|
+
self._maxlen = maxlen if maxlen is not None else self.DEFAULT_MAXLEN
|
|
43
|
+
if self._maxlen < 1:
|
|
44
|
+
raise ValueError("maxlen must be positive, got {}".format(self._maxlen))
|
|
45
|
+
|
|
46
|
+
# Use OrderedDict as backing store - keys are elements, values are ignored
|
|
47
|
+
self._data: OrderedDict[T, None] = OrderedDict()
|
|
48
|
+
|
|
49
|
+
# Track total evictions for monitoring/debugging
|
|
50
|
+
self._total_evictions = 0
|
|
51
|
+
|
|
52
|
+
if iterable is not None:
|
|
53
|
+
for item in iterable:
|
|
54
|
+
self.add(item)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def maxlen(self) -> int:
|
|
58
|
+
"""Maximum number of elements allowed."""
|
|
59
|
+
return self._maxlen
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def total_evictions(self) -> int:
|
|
63
|
+
"""Total number of elements evicted since creation."""
|
|
64
|
+
return self._total_evictions
|
|
65
|
+
|
|
66
|
+
def add(self, item: T) -> Optional[T]:
|
|
67
|
+
"""
|
|
68
|
+
Add an item to the set.
|
|
69
|
+
|
|
70
|
+
If the item already exists, this is a no-op (it does NOT update order).
|
|
71
|
+
If adding would exceed maxlen, the oldest item is evicted first.
|
|
72
|
+
|
|
73
|
+
:param item: Item to add
|
|
74
|
+
:return: The evicted item if eviction occurred, None otherwise
|
|
75
|
+
"""
|
|
76
|
+
# If item exists, do nothing (standard set behavior)
|
|
77
|
+
if item in self._data:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
evicted = None
|
|
81
|
+
|
|
82
|
+
# Evict oldest if at capacity
|
|
83
|
+
if len(self._data) >= self._maxlen:
|
|
84
|
+
# popitem(last=False) removes the first (oldest) item
|
|
85
|
+
evicted, _ = self._data.popitem(last=False)
|
|
86
|
+
self._total_evictions += 1
|
|
87
|
+
|
|
88
|
+
self._data[item] = None
|
|
89
|
+
return evicted
|
|
90
|
+
|
|
91
|
+
def touch(self, item: T) -> bool:
|
|
92
|
+
"""
|
|
93
|
+
Move an item to the end (most recent position) if it exists.
|
|
94
|
+
|
|
95
|
+
This refreshes the item's position in the LRU order, preventing
|
|
96
|
+
it from being evicted soon. If the item doesn't exist, does nothing.
|
|
97
|
+
|
|
98
|
+
:param item: Item to refresh
|
|
99
|
+
:return: True if item was found and touched, False otherwise
|
|
100
|
+
"""
|
|
101
|
+
if item not in self._data:
|
|
102
|
+
return False
|
|
103
|
+
# Move to end (most recent position)
|
|
104
|
+
self._data.move_to_end(item)
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
def discard(self, item: T) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Remove an item from the set if present.
|
|
110
|
+
|
|
111
|
+
Does not raise an error if the item is not present.
|
|
112
|
+
|
|
113
|
+
:param item: Item to remove
|
|
114
|
+
"""
|
|
115
|
+
self._data.pop(item, None)
|
|
116
|
+
|
|
117
|
+
def remove(self, item: T) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Remove an item from the set.
|
|
120
|
+
|
|
121
|
+
:param item: Item to remove
|
|
122
|
+
:raises KeyError: If item is not in the set
|
|
123
|
+
"""
|
|
124
|
+
del self._data[item]
|
|
125
|
+
|
|
126
|
+
def clear(self) -> None:
|
|
127
|
+
"""Remove all items from the set."""
|
|
128
|
+
self._data.clear()
|
|
129
|
+
|
|
130
|
+
def difference_update(self, other: Iterable[T]) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Remove all items that are in 'other' from this set.
|
|
133
|
+
|
|
134
|
+
Equivalent to: self -= other
|
|
135
|
+
|
|
136
|
+
:param other: Iterable of items to remove
|
|
137
|
+
"""
|
|
138
|
+
for item in other:
|
|
139
|
+
self.discard(item)
|
|
140
|
+
|
|
141
|
+
def __contains__(self, item: T) -> bool:
|
|
142
|
+
"""Check if item is in the set."""
|
|
143
|
+
return item in self._data
|
|
144
|
+
|
|
145
|
+
def __len__(self) -> int:
|
|
146
|
+
"""Return the number of items in the set."""
|
|
147
|
+
return len(self._data)
|
|
148
|
+
|
|
149
|
+
def __iter__(self) -> Iterator[T]:
|
|
150
|
+
"""Iterate over items in insertion order."""
|
|
151
|
+
return iter(self._data)
|
|
152
|
+
|
|
153
|
+
def __bool__(self) -> bool:
|
|
154
|
+
"""Return True if the set is non-empty."""
|
|
155
|
+
return bool(self._data)
|
|
156
|
+
|
|
157
|
+
def __repr__(self) -> str:
|
|
158
|
+
items = list(self._data.keys())
|
|
159
|
+
return "BoundedOrderedSet({}, maxlen={})".format(items, self._maxlen)
|
|
160
|
+
|
|
161
|
+
def __eq__(self, other) -> bool:
|
|
162
|
+
"""
|
|
163
|
+
Test equality with another BoundedOrderedSet or regular set.
|
|
164
|
+
|
|
165
|
+
Only compares contents, not maxlen or eviction count.
|
|
166
|
+
"""
|
|
167
|
+
if isinstance(other, BoundedOrderedSet):
|
|
168
|
+
return set(self._data.keys()) == set(other._data.keys())
|
|
169
|
+
elif isinstance(other, (set, frozenset)):
|
|
170
|
+
return set(self._data.keys()) == other
|
|
171
|
+
return NotImplemented
|
|
172
|
+
|
|
173
|
+
def copy(self) -> "BoundedOrderedSet[T]":
|
|
174
|
+
"""
|
|
175
|
+
Create a shallow copy of this set.
|
|
176
|
+
|
|
177
|
+
The copy has the same maxlen but eviction count is reset to 0.
|
|
178
|
+
"""
|
|
179
|
+
new_set: BoundedOrderedSet[T] = BoundedOrderedSet(maxlen=self._maxlen)
|
|
180
|
+
new_set._data = self._data.copy()
|
|
181
|
+
return new_set
|
|
182
|
+
|
|
183
|
+
def as_set(self) -> Set[T]:
|
|
184
|
+
"""Return a regular set containing all items."""
|
|
185
|
+
return set(self._data.keys())
|
|
186
|
+
|
|
187
|
+
def as_list(self) -> list:
|
|
188
|
+
"""Return a list of items in insertion order."""
|
|
189
|
+
return list(self._data.keys())
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def from_iterable(cls, iterable: Iterable[T], maxlen: Optional[int] = None) -> "BoundedOrderedSet[T]":
|
|
193
|
+
"""
|
|
194
|
+
Create a BoundedOrderedSet from an iterable.
|
|
195
|
+
|
|
196
|
+
If the iterable has more items than maxlen, only the last maxlen
|
|
197
|
+
items (in iteration order) will be retained.
|
|
198
|
+
|
|
199
|
+
:param iterable: Source iterable
|
|
200
|
+
:param maxlen: Maximum size (uses DEFAULT_MAXLEN if None)
|
|
201
|
+
:return: New BoundedOrderedSet
|
|
202
|
+
"""
|
|
203
|
+
return cls(maxlen=maxlen, iterable=iterable)
|