unrealon 0.1.18__tar.gz → 0.1.20__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 (121) hide show
  1. {unrealon-0.1.18 → unrealon-0.1.20}/PKG-INFO +5 -1
  2. unrealon-0.1.20/github/README.md +230 -0
  3. {unrealon-0.1.18 → unrealon-0.1.20}/github/pyproject.toml +1 -1
  4. {unrealon-0.1.18 → unrealon-0.1.20}/pyproject.toml +11 -1
  5. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/__init__.py +5 -0
  6. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/__init__.py +2 -0
  7. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/enums.py +16 -0
  8. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedules/models.py +11 -18
  9. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_client.py +18 -0
  10. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_version.py +1 -1
  11. unrealon-0.1.20/src/unrealon/parsers/README.md +400 -0
  12. unrealon-0.1.20/src/unrealon/parsers/__init__.py +51 -0
  13. unrealon-0.1.20/src/unrealon/parsers/api_parser.py +281 -0
  14. unrealon-0.1.20/src/unrealon/parsers/base.py +230 -0
  15. unrealon-0.1.20/src/unrealon/parsers/browser_parser.py +313 -0
  16. unrealon-0.1.20/src/unrealon/parsers/cli.py +388 -0
  17. unrealon-0.1.20/src/unrealon/parsers/monitor.py +147 -0
  18. unrealon-0.1.20/src/unrealon/parsers/storage.py +104 -0
  19. unrealon-0.1.20/src/unrealon/parsers/upload.py +311 -0
  20. unrealon-0.1.20/src/unrealon/parsers/utils/__init__.py +18 -0
  21. unrealon-0.1.20/src/unrealon/parsers/utils/cleaner.py +93 -0
  22. unrealon-0.1.20/src/unrealon/parsers/utils/notify.py +135 -0
  23. unrealon-0.1.20/src/unrealon/parsers/utils/ocr.py +186 -0
  24. unrealon-0.1.20/src/unrealon/runner.py +190 -0
  25. unrealon-0.1.18/github/README.md +0 -209
  26. {unrealon-0.1.18 → unrealon-0.1.20}/.gitignore +0 -0
  27. {unrealon-0.1.18 → unrealon-0.1.20}/README.md +0 -0
  28. {unrealon-0.1.18 → unrealon-0.1.20}/examples/README.md +0 -0
  29. {unrealon-0.1.18 → unrealon-0.1.20}/github/unrealon/_api/generated/services/pyproject.toml +0 -0
  30. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/__init__.py +0 -0
  31. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/client.py +0 -0
  32. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/__init__.py +0 -0
  33. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/client.py +0 -0
  34. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/helpers/__init__.py +0 -0
  35. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/helpers/logger.py +0 -0
  36. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/helpers/retry.py +0 -0
  37. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/pyproject.toml +0 -0
  38. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__api_keys/__init__.py +0 -0
  39. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__api_keys/client.py +0 -0
  40. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__api_keys/models.py +0 -0
  41. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__api_keys/sync_client.py +0 -0
  42. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__process_control/__init__.py +0 -0
  43. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__process_control/client.py +0 -0
  44. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__process_control/models.py +0 -0
  45. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__process_control/sync_client.py +0 -0
  46. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__process_jobs/__init__.py +0 -0
  47. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__process_jobs/client.py +0 -0
  48. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__process_jobs/models.py +0 -0
  49. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__process_jobs/sync_client.py +0 -0
  50. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedule_events/__init__.py +0 -0
  51. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedule_events/client.py +0 -0
  52. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedule_events/models.py +0 -0
  53. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedule_events/sync_client.py +0 -0
  54. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedule_runs/__init__.py +0 -0
  55. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedule_runs/client.py +0 -0
  56. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedule_runs/models.py +0 -0
  57. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedule_runs/sync_client.py +0 -0
  58. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedules/__init__.py +0 -0
  59. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedules/client.py +0 -0
  60. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__schedules/sync_client.py +0 -0
  61. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_commands/__init__.py +0 -0
  62. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_commands/client.py +0 -0
  63. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_commands/models.py +0 -0
  64. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_commands/sync_client.py +0 -0
  65. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_control/__init__.py +0 -0
  66. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_control/client.py +0 -0
  67. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_control/models.py +0 -0
  68. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_control/sync_client.py +0 -0
  69. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_logs/__init__.py +0 -0
  70. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_logs/client.py +0 -0
  71. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_logs/models.py +0 -0
  72. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_logs/sync_client.py +0 -0
  73. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_sdk/__init__.py +0 -0
  74. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_sdk/client.py +0 -0
  75. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_sdk/models.py +0 -0
  76. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__service_sdk/sync_client.py +0 -0
  77. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__services/__init__.py +0 -0
  78. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__services/client.py +0 -0
  79. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__services/models.py +0 -0
  80. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/services__api__services/sync_client.py +0 -0
  81. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_api/generated/services/sync_client.py +0 -0
  82. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_config.py +0 -0
  83. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/_constants.py +0 -0
  84. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/core/__init__.py +0 -0
  85. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/core/lifecycle.py +0 -0
  86. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/core/signals.py +0 -0
  87. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/core/state.py +0 -0
  88. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/exceptions/__init__.py +0 -0
  89. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/exceptions/handlers.py +0 -0
  90. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/exceptions/types.py +0 -0
  91. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/__init__.py +0 -0
  92. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_config.py +0 -0
  93. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_connection.py +0 -0
  94. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_constants.py +0 -0
  95. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_handlers.py +0 -0
  96. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_logging.py +0 -0
  97. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_messaging.py +0 -0
  98. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_metrics.py +0 -0
  99. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_reconnect.py +0 -0
  100. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_registration.py +0 -0
  101. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/_types.py +0 -0
  102. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/circuit_breaker.py +0 -0
  103. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/generated/__init__.py +0 -0
  104. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/generated/unrealon_pb2.py +0 -0
  105. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/generated/unrealon_pb2.pyi +0 -0
  106. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/generated/unrealon_pb2_grpc.py +0 -0
  107. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/grpc/stream_service.py +0 -0
  108. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/logging/__init__.py +0 -0
  109. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/logging/_config.py +0 -0
  110. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/logging/_formatters.py +0 -0
  111. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/logging/_handlers.py +0 -0
  112. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/logging/_logger.py +0 -0
  113. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/logging/_project.py +0 -0
  114. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/models/__init__.py +0 -0
  115. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/scheduling/__init__.py +0 -0
  116. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/scheduling/_manager.py +0 -0
  117. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/scheduling/_models.py +0 -0
  118. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/services/__init__.py +0 -0
  119. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/utils/__init__.py +0 -0
  120. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/utils/metrics.py +0 -0
  121. {unrealon-0.1.18 → unrealon-0.1.20}/src/unrealon/utils/system.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unrealon
3
- Version: 0.1.18
3
+ Version: 0.1.20
4
4
  Summary: Unrealon SDK - Service management for Django backend (registration, heartbeat, logging, commands)
5
5
  Project-URL: Homepage, https://github.com/markolofsen/unrealon-sdk
6
6
  Project-URL: Documentation, https://unrealon.com
@@ -17,6 +17,8 @@ Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
19
  Requires-Python: >=3.10
20
+ Requires-Dist: click>=8.1.0
21
+ Requires-Dist: cmdop
20
22
  Requires-Dist: croniter<7.0.0,>=6.0.0
21
23
  Requires-Dist: grpcio-tools<2.0.0,>=1.76.0
22
24
  Requires-Dist: grpcio<2.0.0,>=1.76.0
@@ -26,6 +28,8 @@ Requires-Dist: psutil>=6.0.0
26
28
  Requires-Dist: pydantic-settings>=2.7.0
27
29
  Requires-Dist: pydantic<3.0.0,>=2.10.0
28
30
  Requires-Dist: rich<15.0.0,>=14.3.1
31
+ Requires-Dist: sdkrouter
32
+ Requires-Dist: sdkrouter-tools
29
33
  Requires-Dist: tenacity>=9.1.0
30
34
  Provides-Extra: dev
31
35
  Requires-Dist: build>=1.2.0; extra == 'dev'
@@ -0,0 +1,230 @@
1
+ # Unrealon SDK
2
+
3
+ Python SDK for monitoring and managing services via the Unrealon platform.
4
+
5
+ ## Features
6
+
7
+ - **Monitoring** — real-time service status visibility
8
+ - **Cloud Logs** — all logs accessible in the web interface
9
+ - **Control** — pause/resume/stop directly from the dashboard
10
+ - **Metrics** — counters for processed items and errors
11
+ - **Scheduling** — automatic cron-based task execution
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install unrealon
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ### Minimal Example
22
+
23
+ ```python
24
+ from unrealon import ServiceClient
25
+
26
+ with ServiceClient(api_key="pk_xxx", service_name="my-service") as client:
27
+ client.info("Started")
28
+
29
+ for item in items:
30
+ process(item)
31
+ client.increment_processed()
32
+
33
+ client.info("Done")
34
+ ```
35
+
36
+ The service will register, logs will stream to cloud, and metrics will be displayed.
37
+
38
+ ### With Pause/Resume Support
39
+
40
+ ```python
41
+ from unrealon import ServiceClient
42
+
43
+ with ServiceClient(api_key="pk_xxx", service_name="my-parser") as client:
44
+ client.info("Started")
45
+
46
+ for item in items:
47
+ client.check_interrupt() # Parser pauses here if Pause is clicked
48
+
49
+ process(item)
50
+ client.increment_processed()
51
+
52
+ client.info("Done")
53
+ ```
54
+
55
+ `check_interrupt()` does two things:
56
+ - If **Pause** was clicked — waits until Resume
57
+ - If **Stop** was clicked — raises `StopInterrupt`
58
+
59
+ ## Continuous Mode
60
+
61
+ A service that waits for commands from the dashboard:
62
+
63
+ ```python
64
+ import time
65
+ from unrealon import ServiceClient
66
+ from unrealon.exceptions import StopInterrupt
67
+
68
+ with ServiceClient(api_key="pk_xxx", service_name="my-parser") as client:
69
+
70
+ def handle_run(params: dict) -> dict:
71
+ limit = params.get("limit", 100)
72
+
73
+ client.set_busy()
74
+ try:
75
+ for i in range(limit):
76
+ client.check_interrupt()
77
+ do_work()
78
+ client.increment_processed()
79
+ return {"status": "ok"}
80
+ except StopInterrupt:
81
+ return {"status": "stopped"}
82
+ finally:
83
+ client.set_idle()
84
+
85
+ client.on_command("run", handle_run)
86
+
87
+ # Wait for commands
88
+ client.set_idle()
89
+ while not client.should_stop:
90
+ time.sleep(1)
91
+ ```
92
+
93
+ Now from the dashboard you can:
94
+ - Click **Run** — executes `handle_run`
95
+ - Click **Pause** — parser stops at `check_interrupt()`
96
+ - Click **Resume** — continues from where it left off
97
+ - Click **Stop** — graceful shutdown
98
+
99
+ ## API
100
+
101
+ ### Logging
102
+
103
+ ```python
104
+ client.debug("Debug message")
105
+ client.info("Info message", key="value")
106
+ client.warning("Warning")
107
+ client.error("Error", code=500)
108
+ ```
109
+
110
+ Logs go to three places: console (Rich), file, and cloud.
111
+
112
+ ### Metrics
113
+
114
+ ```python
115
+ client.increment_processed() # +1 processed
116
+ client.increment_processed(10) # +10 processed
117
+ client.increment_errors() # +1 error
118
+ ```
119
+
120
+ ### Status
121
+
122
+ ```python
123
+ client.set_busy() # Shows "Busy" in dashboard
124
+ client.set_idle() # Shows "Idle"
125
+ ```
126
+
127
+ ### State
128
+
129
+ ```python
130
+ client.is_paused # True if paused
131
+ client.should_stop # True if stop requested
132
+ client.is_connected # True if connected to server
133
+ ```
134
+
135
+ ### Commands
136
+
137
+ ```python
138
+ # Register handler
139
+ client.on_command("run", handle_run)
140
+ client.on_command("custom", handle_custom)
141
+
142
+ # Handler receives params and returns result
143
+ def handle_run(params: dict) -> dict:
144
+ limit = params.get("limit", 10)
145
+ # ... do work ...
146
+ return {"status": "ok", "processed": 100}
147
+ ```
148
+
149
+ ### Schedules
150
+
151
+ Schedules run automatically based on cron expressions.
152
+ If the schedule's `action_type` matches a registered command,
153
+ the same handler is used:
154
+
155
+ ```python
156
+ # This handler works for both manual Run and scheduled runs
157
+ client.on_command("run", handle_run)
158
+ ```
159
+
160
+ For different behavior, register a schedule-specific handler:
161
+
162
+ ```python
163
+ @client.on_schedule("process")
164
+ def handle_scheduled_process(schedule, params):
165
+ # schedule.name, schedule.id are available
166
+ return {"items_processed": 100}
167
+ ```
168
+
169
+ ## Configuration
170
+
171
+ ### Via Environment Variables
172
+
173
+ ```bash
174
+ export UNREALON_API_KEY=pk_xxx
175
+ export UNREALON_SERVICE_NAME=my-service
176
+ ```
177
+
178
+ ```python
179
+ # Picks up from env
180
+ with ServiceClient() as client:
181
+ ...
182
+ ```
183
+
184
+ ### Dev Mode (Local Server)
185
+
186
+ ```python
187
+ with ServiceClient(
188
+ api_key="dk_xxx",
189
+ service_name="my-service",
190
+ dev_mode=True, # Connects to localhost:50051
191
+ ) as client:
192
+ ...
193
+ ```
194
+
195
+ ## Exceptions
196
+
197
+ ```python
198
+ from unrealon.exceptions import (
199
+ StopInterrupt, # Stop requested (inherits BaseException!)
200
+ UnrealonError, # Base SDK error
201
+ AuthenticationError, # Bad API key
202
+ RegistrationError, # Can't register
203
+ )
204
+
205
+ try:
206
+ with ServiceClient(...) as client:
207
+ for item in items:
208
+ client.check_interrupt()
209
+ process(item)
210
+ except StopInterrupt:
211
+ print("Stopped by command")
212
+ ```
213
+
214
+ **Important**: `StopInterrupt` inherits from `BaseException`, not `Exception`.
215
+ This means `except Exception` won't catch it — by design, so generic
216
+ error handlers don't swallow the stop command.
217
+
218
+ ## Standalone Logger
219
+
220
+ You can use the logger separately from the SDK:
221
+
222
+ ```python
223
+ from unrealon.logging import get_logger
224
+
225
+ log = get_logger("myapp")
226
+ log.info("Starting", version="1.0")
227
+ log.error("Failed", error="connection timeout")
228
+ ```
229
+
230
+ Logs go to console and file (without cloud).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unrealon"
3
- version = "0.1.14"
3
+ version = "0.1.18"
4
4
  description = "Lightweight Python SDK for managing distributed workers"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "unrealon"
7
- version = "0.1.18"
7
+ version = "0.1.20"
8
8
  description = "Unrealon SDK - Service management for Django backend (registration, heartbeat, logging, commands)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -35,6 +35,10 @@ dependencies = [
35
35
  "protobuf (>=6.33.5,<7.0.0)",
36
36
  "rich (>=14.3.1,<15.0.0)",
37
37
  "croniter (>=6.0.0,<7.0.0)",
38
+ "click>=8.1.0",
39
+ "cmdop",
40
+ "sdkrouter",
41
+ "sdkrouter-tools",
38
42
  ]
39
43
 
40
44
  [project.optional-dependencies]
@@ -93,3 +97,9 @@ strict = true
93
97
  asyncio_mode = "auto"
94
98
  testpaths = ["tests"]
95
99
  pythonpath = ["src"]
100
+
101
+ # Local development dependencies (for parsers module)
102
+ [tool.uv.sources]
103
+ cmdop = { path = "../../../../../@projects/cmdop/projects/software/cmdop_sdk/libs/sdk_python", editable = true }
104
+ sdkrouter = { path = "../../../../../@projects/sdkrouter/solution/packages/sdkrouter_py", editable = true }
105
+ sdkrouter-tools = { path = "../../../../../@projects/sdkrouter/solution/packages/sdkrouter_tools_py", editable = true }
@@ -53,6 +53,7 @@ from .exceptions import (
53
53
  from .grpc import GRPCStreamService
54
54
  from .logging import get_logger
55
55
  from .models import ServiceStatus
56
+ from .runner import TaskRunner
56
57
  from .scheduling import Schedule, ScheduleResult, ScheduleRunStatus
57
58
 
58
59
  __all__ = [
@@ -96,4 +97,8 @@ __all__ = [
96
97
  "Schedule",
97
98
  "ScheduleResult",
98
99
  "ScheduleRunStatus",
100
+ # Runner
101
+ "TaskRunner",
102
+ # Parsers submodule (import as: from unrealon.parsers import ...)
103
+ # Note: parsers module requires optional dependencies: pip install unrealon[parsers]
99
104
  ]
@@ -36,6 +36,7 @@ from .enums import (
36
36
  CommandCommandType,
37
37
  CommandStatus,
38
38
  LogEntryRequestLevel,
39
+ PatchedScheduleCreateRequestActionType,
39
40
  PatchedServiceDetailRequestConnectionType,
40
41
  ProcessJobJobType,
41
42
  ProcessJobStatus,
@@ -285,6 +286,7 @@ __all__ = [
285
286
  "CommandCommandType",
286
287
  "CommandStatus",
287
288
  "LogEntryRequestLevel",
289
+ "PatchedScheduleCreateRequestActionType",
288
290
  "PatchedServiceDetailRequestConnectionType",
289
291
  "ProcessJobJobType",
290
292
  "ProcessJobStatus",
@@ -99,6 +99,22 @@ class LogEntryRequestLevel(StrEnum):
99
99
 
100
100
 
101
101
 
102
+ class PatchedScheduleCreateRequestActionType(StrEnum):
103
+ """
104
+ Action type to execute
105
+ * `run` - Run (execute parser)
106
+ * `pause` - Pause service
107
+ * `resume` - Resume service
108
+ * `custom` - Custom action
109
+ """
110
+
111
+ RUN = "run"
112
+ PAUSE = "pause"
113
+ RESUME = "resume"
114
+ CUSTOM = "custom"
115
+
116
+
117
+
102
118
  class PatchedServiceDetailRequestConnectionType(StrEnum):
103
119
  """
104
120
  How to control this service (local, cloud, ssh, or none)
@@ -6,7 +6,7 @@ from typing import Any
6
6
 
7
7
  from pydantic import BaseModel, ConfigDict, Field
8
8
 
9
- from ..enums import ScheduleEventEventType, ScheduleRunStatus
9
+ from ..enums import PatchedScheduleCreateRequestActionType, ScheduleEventEventType, ScheduleRunStatus
10
10
 
11
11
 
12
12
  class Schedule(BaseModel):
@@ -34,10 +34,9 @@ class Schedule(BaseModel):
34
34
  max_length=100,
35
35
  )
36
36
  frequency_display: Any = ...
37
- action_type: str | None = Field(
37
+ action_type: PatchedScheduleCreateRequestActionType | None = Field(
38
38
  None,
39
- description='Action type to execute (process,...',
40
- max_length=50,
39
+ description='Action type to execute * `run` ...',
41
40
  )
42
41
  next_run_at: str | None = Field(None, description='Next scheduled execution time')
43
42
  last_run_at: Any | None = Field(None, description='Last execution time')
@@ -105,11 +104,9 @@ class ScheduleCreateRequest(BaseModel):
105
104
  min_length=1,
106
105
  max_length=50,
107
106
  )
108
- action_type: str | None = Field(
107
+ action_type: PatchedScheduleCreateRequestActionType | None = Field(
109
108
  None,
110
- description='Action type to execute (process,...',
111
- min_length=1,
112
- max_length=50,
109
+ description='Action type to execute * `run` ...',
113
110
  )
114
111
  action_params: dict[str, Any] | None = Field(
115
112
  None,
@@ -163,10 +160,9 @@ class ScheduleCreate(BaseModel):
163
160
  description="Timezone for schedule (e.g., 'Eu...",
164
161
  max_length=50,
165
162
  )
166
- action_type: str | None = Field(
163
+ action_type: PatchedScheduleCreateRequestActionType | None = Field(
167
164
  None,
168
- description='Action type to execute (process,...',
169
- max_length=50,
165
+ description='Action type to execute * `run` ...',
170
166
  )
171
167
  action_params: dict[str, Any] | None = Field(
172
168
  None,
@@ -223,10 +219,9 @@ class ScheduleDetail(BaseModel):
223
219
  description="Timezone for schedule (e.g., 'Eu...",
224
220
  max_length=50,
225
221
  )
226
- action_type: str | None = Field(
222
+ action_type: PatchedScheduleCreateRequestActionType | None = Field(
227
223
  None,
228
- description='Action type to execute (process,...',
229
- max_length=50,
224
+ description='Action type to execute * `run` ...',
230
225
  )
231
226
  action_params: dict[str, Any] | None = Field(
232
227
  None,
@@ -294,11 +289,9 @@ class PatchedScheduleCreateRequest(BaseModel):
294
289
  min_length=1,
295
290
  max_length=50,
296
291
  )
297
- action_type: str | None = Field(
292
+ action_type: PatchedScheduleCreateRequestActionType | None = Field(
298
293
  None,
299
- description='Action type to execute (process,...',
300
- min_length=1,
301
- max_length=50,
294
+ description='Action type to execute * `run` ...',
302
295
  )
303
296
  action_params: dict[str, Any] | None = Field(
304
297
  None,
@@ -79,6 +79,7 @@ class ServiceClient:
79
79
  "_logger",
80
80
  "_cloud_handler",
81
81
  "_resume_event",
82
+ "_log_level",
82
83
  )
83
84
 
84
85
  def __init__(
@@ -94,6 +95,7 @@ class ServiceClient:
94
95
  heartbeat_interval: int | None = None,
95
96
  log_batch_size: int | None = None,
96
97
  log_flush_interval: float | None = None,
98
+ log_level: str = "INFO",
97
99
  ) -> None:
98
100
  """
99
101
  Initialize service client.
@@ -109,6 +111,7 @@ class ServiceClient:
109
111
  heartbeat_interval: Heartbeat interval in seconds
110
112
  log_batch_size: Number of logs to batch before sending
111
113
  log_flush_interval: Max seconds to wait before flushing logs
114
+ log_level: Minimum log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
112
115
  """
113
116
  config_kwargs: dict[str, object] = {}
114
117
  if api_key:
@@ -151,11 +154,14 @@ class ServiceClient:
151
154
  self._resume_event.set() # Start as "not paused" (event is set)
152
155
 
153
156
  # Initialize logger with Rich console + file, cloud handler added on start
157
+ self._log_level = log_level.upper()
154
158
  self._logger: UnrealonLogger = get_logger(
155
159
  name=self._config.service_name,
160
+ level=self._log_level, # type: ignore[arg-type]
156
161
  log_to_cloud=False, # Will be connected after gRPC start
157
162
  )
158
163
  self._cloud_handler: CloudHandler = CloudHandler()
164
+ self._cloud_handler.setLevel(getattr(logging, self._log_level))
159
165
 
160
166
  @property
161
167
  def grpc(self) -> GRPCStreamService:
@@ -603,10 +609,17 @@ class ServiceClient:
603
609
 
604
610
  def _setup_signal_handlers(self) -> None:
605
611
  """Setup graceful shutdown signal handlers."""
612
+ import os
606
613
 
607
614
  def signal_handler(signum: int, _frame: FrameType | None) -> None:
615
+ if self._shutdown_requested:
616
+ # Second signal - force exit immediately
617
+ logger.info("Received signal %d again, forcing exit...", signum)
618
+ os._exit(1)
608
619
  logger.info("Received signal %d, requesting shutdown...", signum)
609
620
  self._shutdown_requested = True
621
+ # Unblock any waiting threads
622
+ self._resume_event.set()
610
623
 
611
624
  try:
612
625
  self._original_sigint = signal.signal(signal.SIGINT, signal_handler)
@@ -631,6 +644,7 @@ class AsyncServiceClient:
631
644
  "_grpc",
632
645
  "_logger",
633
646
  "_cloud_handler",
647
+ "_log_level",
634
648
  )
635
649
 
636
650
  def __init__(
@@ -643,6 +657,7 @@ class AsyncServiceClient:
643
657
  dev_mode: bool = False,
644
658
  source_code: str | None = None,
645
659
  description: str | None = None,
660
+ log_level: str = "INFO",
646
661
  ) -> None:
647
662
  """Initialize async service client."""
648
663
  config_kwargs: dict[str, object] = {}
@@ -674,11 +689,14 @@ class AsyncServiceClient:
674
689
  self._grpc: GRPCStreamService | None = None
675
690
 
676
691
  # Initialize logger with Rich console + file, cloud handler added on start
692
+ self._log_level = log_level.upper()
677
693
  self._logger: UnrealonLogger = get_logger(
678
694
  name=self._config.service_name,
695
+ level=self._log_level, # type: ignore[arg-type]
679
696
  log_to_cloud=False,
680
697
  )
681
698
  self._cloud_handler: CloudHandler = CloudHandler()
699
+ self._cloud_handler.setLevel(getattr(logging, self._log_level))
682
700
 
683
701
  @property
684
702
  def grpc(self) -> GRPCStreamService:
@@ -1,3 +1,3 @@
1
1
  """Version information."""
2
2
 
3
- __version__ = "0.1.18"
3
+ __version__ = "0.1.20"