citrascope 0.1.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
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.
- citrascope/__main__.py +8 -5
- citrascope/api/abstract_api_client.py +7 -0
- citrascope/api/citra_api_client.py +30 -1
- citrascope/citra_scope_daemon.py +214 -61
- citrascope/hardware/abstract_astro_hardware_adapter.py +70 -2
- citrascope/hardware/adapter_registry.py +94 -0
- citrascope/hardware/indi_adapter.py +456 -16
- citrascope/hardware/kstars_dbus_adapter.py +179 -0
- citrascope/hardware/nina_adv_http_adapter.py +593 -0
- citrascope/hardware/nina_adv_http_survey_template.json +328 -0
- citrascope/logging/__init__.py +2 -1
- citrascope/logging/_citrascope_logger.py +80 -1
- citrascope/logging/web_log_handler.py +74 -0
- citrascope/settings/citrascope_settings.py +145 -0
- citrascope/settings/settings_file_manager.py +126 -0
- citrascope/tasks/runner.py +124 -28
- citrascope/tasks/scope/base_telescope_task.py +25 -10
- citrascope/tasks/scope/static_telescope_task.py +11 -3
- citrascope/web/__init__.py +1 -0
- citrascope/web/app.py +470 -0
- citrascope/web/server.py +123 -0
- citrascope/web/static/api.js +82 -0
- citrascope/web/static/app.js +500 -0
- citrascope/web/static/config.js +362 -0
- citrascope/web/static/img/citra.png +0 -0
- citrascope/web/static/img/favicon.png +0 -0
- citrascope/web/static/style.css +120 -0
- citrascope/web/static/websocket.js +127 -0
- citrascope/web/templates/dashboard.html +354 -0
- {citrascope-0.1.0.dist-info → citrascope-0.3.0.dist-info}/METADATA +68 -36
- citrascope-0.3.0.dist-info/RECORD +38 -0
- {citrascope-0.1.0.dist-info → citrascope-0.3.0.dist-info}/WHEEL +1 -1
- citrascope/settings/_citrascope_settings.py +0 -42
- citrascope-0.1.0.dist-info/RECORD +0 -21
- {citrascope-0.1.0.dist-info → citrascope-0.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "1",
|
|
3
|
+
"$type": "NINA.Sequencer.Container.SequenceRootContainer, NINA.Sequencer",
|
|
4
|
+
"Strategy": {
|
|
5
|
+
"$type": "NINA.Sequencer.Container.ExecutionStrategy.SequentialStrategy, NINA.Sequencer"
|
|
6
|
+
},
|
|
7
|
+
"Name": "{{SEQUENCE_NAME}}",
|
|
8
|
+
"Conditions": {
|
|
9
|
+
"$id": "2",
|
|
10
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.Conditions.ISequenceCondition, NINA.Sequencer]], System.ObjectModel",
|
|
11
|
+
"$values": []
|
|
12
|
+
},
|
|
13
|
+
"IsExpanded": true,
|
|
14
|
+
"Items": {
|
|
15
|
+
"$id": "3",
|
|
16
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.SequenceItem.ISequenceItem, NINA.Sequencer]], System.ObjectModel",
|
|
17
|
+
"$values": [
|
|
18
|
+
{
|
|
19
|
+
"$id": "4",
|
|
20
|
+
"$type": "NINA.Sequencer.Container.StartAreaContainer, NINA.Sequencer",
|
|
21
|
+
"Strategy": {
|
|
22
|
+
"$type": "NINA.Sequencer.Container.ExecutionStrategy.SequentialStrategy, NINA.Sequencer"
|
|
23
|
+
},
|
|
24
|
+
"Name": "Start",
|
|
25
|
+
"Conditions": {
|
|
26
|
+
"$id": "5",
|
|
27
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.Conditions.ISequenceCondition, NINA.Sequencer]], System.ObjectModel",
|
|
28
|
+
"$values": []
|
|
29
|
+
},
|
|
30
|
+
"IsExpanded": true,
|
|
31
|
+
"Items": {
|
|
32
|
+
"$id": "6",
|
|
33
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.SequenceItem.ISequenceItem, NINA.Sequencer]], System.ObjectModel",
|
|
34
|
+
"$values": []
|
|
35
|
+
},
|
|
36
|
+
"Triggers": {
|
|
37
|
+
"$id": "7",
|
|
38
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.Trigger.ISequenceTrigger, NINA.Sequencer]], System.ObjectModel",
|
|
39
|
+
"$values": []
|
|
40
|
+
},
|
|
41
|
+
"Parent": {
|
|
42
|
+
"$ref": "1"
|
|
43
|
+
},
|
|
44
|
+
"ErrorBehavior": 0,
|
|
45
|
+
"Attempts": 1
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"$id": "8",
|
|
49
|
+
"$type": "NINA.Sequencer.Container.TargetAreaContainer, NINA.Sequencer",
|
|
50
|
+
"Strategy": {
|
|
51
|
+
"$type": "NINA.Sequencer.Container.ExecutionStrategy.SequentialStrategy, NINA.Sequencer"
|
|
52
|
+
},
|
|
53
|
+
"Name": "Targets",
|
|
54
|
+
"Conditions": {
|
|
55
|
+
"$id": "9",
|
|
56
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.Conditions.ISequenceCondition, NINA.Sequencer]], System.ObjectModel",
|
|
57
|
+
"$values": []
|
|
58
|
+
},
|
|
59
|
+
"IsExpanded": true,
|
|
60
|
+
"Items": {
|
|
61
|
+
"$id": "10",
|
|
62
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.SequenceItem.ISequenceItem, NINA.Sequencer]], System.ObjectModel",
|
|
63
|
+
"$values": [
|
|
64
|
+
{
|
|
65
|
+
"$id": "11",
|
|
66
|
+
"$type": "NINA.Sequencer.Container.SequentialContainer, NINA.Sequencer",
|
|
67
|
+
"Strategy": {
|
|
68
|
+
"$type": "NINA.Sequencer.Container.ExecutionStrategy.SequentialStrategy, NINA.Sequencer"
|
|
69
|
+
},
|
|
70
|
+
"Name": "Sequential Instruction Set",
|
|
71
|
+
"Conditions": {
|
|
72
|
+
"$id": "12",
|
|
73
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.Conditions.ISequenceCondition, NINA.Sequencer]], System.ObjectModel",
|
|
74
|
+
"$values": []
|
|
75
|
+
},
|
|
76
|
+
"IsExpanded": true,
|
|
77
|
+
"Items": {
|
|
78
|
+
"$id": "15",
|
|
79
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.SequenceItem.ISequenceItem, NINA.Sequencer]], System.ObjectModel",
|
|
80
|
+
"$values": [
|
|
81
|
+
{
|
|
82
|
+
"$id": "20",
|
|
83
|
+
"$type": "NINA.Joko.Plugin.Orbitals.SequenceItems.ManualTLEContainer, NINA.Joko.Plugin.Orbitals",
|
|
84
|
+
"TLEData": "{{TLE_DATA}}",
|
|
85
|
+
"OffsetExpanded": false,
|
|
86
|
+
"OffsetCoordinates": {
|
|
87
|
+
"$id": "21",
|
|
88
|
+
"$type": "NINA.Joko.Plugin.Orbitals.Utility.InputCoordinatesEx, NINA.Joko.Plugin.Orbitals",
|
|
89
|
+
"RAHours": 0,
|
|
90
|
+
"RAMinutes": 0,
|
|
91
|
+
"RASeconds": 0.0,
|
|
92
|
+
"NegativeDec": false,
|
|
93
|
+
"NegativeRA": false,
|
|
94
|
+
"DecDegrees": 0,
|
|
95
|
+
"DecMinutes": 0,
|
|
96
|
+
"DecSeconds": 0.0
|
|
97
|
+
},
|
|
98
|
+
"Target": {
|
|
99
|
+
"$id": "22",
|
|
100
|
+
"$type": "NINA.Astrometry.InputTarget, NINA.Astrometry",
|
|
101
|
+
"Expanded": true,
|
|
102
|
+
"TargetName": "{{SATELLITE_NAME}}",
|
|
103
|
+
"PositionAngle": 0.0,
|
|
104
|
+
"InputCoordinates": {
|
|
105
|
+
"$id": "23",
|
|
106
|
+
"$type": "NINA.Astrometry.InputCoordinates, NINA.Astrometry",
|
|
107
|
+
"RAHours": 3,
|
|
108
|
+
"RAMinutes": 33,
|
|
109
|
+
"RASeconds": 12.92135,
|
|
110
|
+
"NegativeDec": true,
|
|
111
|
+
"DecDegrees": -6,
|
|
112
|
+
"DecMinutes": 0,
|
|
113
|
+
"DecSeconds": 9.52085
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"Strategy": {
|
|
117
|
+
"$type": "NINA.Sequencer.Container.ExecutionStrategy.SequentialStrategy, NINA.Sequencer"
|
|
118
|
+
},
|
|
119
|
+
"Name": "{{SATELLITE_NAME}}",
|
|
120
|
+
"Conditions": {
|
|
121
|
+
"$id": "24",
|
|
122
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.Conditions.ISequenceCondition, NINA.Sequencer]], System.ObjectModel",
|
|
123
|
+
"$values": []
|
|
124
|
+
},
|
|
125
|
+
"IsExpanded": true,
|
|
126
|
+
"Items": {
|
|
127
|
+
"$id": "25",
|
|
128
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.SequenceItem.ISequenceItem, NINA.Sequencer]], System.ObjectModel",
|
|
129
|
+
"$values": [
|
|
130
|
+
{
|
|
131
|
+
"$id": "26",
|
|
132
|
+
"$type": "DaleGhent.NINA.PlaneWaveTools.TLE.TLEControl, PlaneWave Tools",
|
|
133
|
+
"Line1": "{{TLE_LINE1}}",
|
|
134
|
+
"Line2": "{{TLE_LINE2}}",
|
|
135
|
+
"Axis0Distance": -0.14,
|
|
136
|
+
"Axis1Distance": -0.01,
|
|
137
|
+
"Parent": {
|
|
138
|
+
"$ref": "20"
|
|
139
|
+
},
|
|
140
|
+
"ErrorBehavior": 0,
|
|
141
|
+
"Attempts": 1
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
{
|
|
145
|
+
"$id": "27",
|
|
146
|
+
"$type": "NINA.Sequencer.SequenceItem.FilterWheel.SwitchFilter, NINA.Sequencer",
|
|
147
|
+
"Filter": {
|
|
148
|
+
"$id": "28",
|
|
149
|
+
"$type": "NINA.Core.Model.Equipment.FilterInfo, NINA.Core",
|
|
150
|
+
"_name": "Clear",
|
|
151
|
+
"_focusOffset": 0,
|
|
152
|
+
"_position": 0,
|
|
153
|
+
"_autoFocusExposureTime": -1.0,
|
|
154
|
+
"_autoFocusFilter": true,
|
|
155
|
+
"FlatWizardFilterSettings": {
|
|
156
|
+
"$id": "29",
|
|
157
|
+
"$type": "NINA.Core.Model.Equipment.FlatWizardFilterSettings, NINA.Core",
|
|
158
|
+
"FlatWizardMode": 0,
|
|
159
|
+
"HistogramMeanTarget": 0.5,
|
|
160
|
+
"HistogramTolerance": 0.1,
|
|
161
|
+
"MaxFlatExposureTime": 30.0,
|
|
162
|
+
"MinFlatExposureTime": 0.01,
|
|
163
|
+
"MaxAbsoluteFlatDeviceBrightness": 32767,
|
|
164
|
+
"MinAbsoluteFlatDeviceBrightness": 0,
|
|
165
|
+
"Gain": -1,
|
|
166
|
+
"Offset": -1,
|
|
167
|
+
"Binning": {
|
|
168
|
+
"$id": "30",
|
|
169
|
+
"$type": "NINA.Core.Model.Equipment.BinningMode, NINA.Core",
|
|
170
|
+
"X": 1,
|
|
171
|
+
"Y": 1
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"_autoFocusBinning": {
|
|
175
|
+
"$id": "31",
|
|
176
|
+
"$type": "NINA.Core.Model.Equipment.BinningMode, NINA.Core",
|
|
177
|
+
"X": 1,
|
|
178
|
+
"Y": 1
|
|
179
|
+
},
|
|
180
|
+
"_autoFocusGain": -1,
|
|
181
|
+
"_autoFocusOffset": -1
|
|
182
|
+
},
|
|
183
|
+
"Parent": {
|
|
184
|
+
"$ref": "20"
|
|
185
|
+
},
|
|
186
|
+
"ErrorBehavior": 0,
|
|
187
|
+
"Attempts": 1
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"$id": "32",
|
|
191
|
+
"$type": "NINA.Sequencer.SequenceItem.Focuser.MoveFocuserAbsolute, NINA.Sequencer",
|
|
192
|
+
"Position": 9000,
|
|
193
|
+
"Parent": {
|
|
194
|
+
"$ref": "20"
|
|
195
|
+
},
|
|
196
|
+
"ErrorBehavior": 0,
|
|
197
|
+
"Attempts": 1
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"$id": "33",
|
|
201
|
+
"$type": "NINA.Sequencer.SequenceItem.Imaging.TakeExposure, NINA.Sequencer",
|
|
202
|
+
"ExposureTime": 1.0,
|
|
203
|
+
"Gain": -1,
|
|
204
|
+
"Offset": -1,
|
|
205
|
+
"Binning": {
|
|
206
|
+
"$id": "34",
|
|
207
|
+
"$type": "NINA.Core.Model.Equipment.BinningMode, NINA.Core",
|
|
208
|
+
"X": 4,
|
|
209
|
+
"Y": 4
|
|
210
|
+
},
|
|
211
|
+
"ImageType": "LIGHT",
|
|
212
|
+
"ExposureCount": 130,
|
|
213
|
+
"Parent": {
|
|
214
|
+
"$ref": "20"
|
|
215
|
+
},
|
|
216
|
+
"ErrorBehavior": 0,
|
|
217
|
+
"Attempts": 1
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
"Triggers": {
|
|
223
|
+
"$id": "63",
|
|
224
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.Trigger.ISequenceTrigger, NINA.Sequencer]], System.ObjectModel",
|
|
225
|
+
"$values": []
|
|
226
|
+
},
|
|
227
|
+
"Parent": {
|
|
228
|
+
"$ref": "11"
|
|
229
|
+
},
|
|
230
|
+
"ErrorBehavior": 0,
|
|
231
|
+
"Attempts": 1
|
|
232
|
+
}
|
|
233
|
+
]
|
|
234
|
+
},
|
|
235
|
+
"Triggers": {
|
|
236
|
+
"$id": "112",
|
|
237
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.Trigger.ISequenceTrigger, NINA.Sequencer]], System.ObjectModel",
|
|
238
|
+
"$values": []
|
|
239
|
+
},
|
|
240
|
+
"Parent": {
|
|
241
|
+
"$ref": "8"
|
|
242
|
+
},
|
|
243
|
+
"ErrorBehavior": 0,
|
|
244
|
+
"Attempts": 1
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
},
|
|
248
|
+
"Triggers": {
|
|
249
|
+
"$id": "113",
|
|
250
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.Trigger.ISequenceTrigger, NINA.Sequencer]], System.ObjectModel",
|
|
251
|
+
"$values": []
|
|
252
|
+
},
|
|
253
|
+
"Parent": {
|
|
254
|
+
"$ref": "1"
|
|
255
|
+
},
|
|
256
|
+
"ErrorBehavior": 0,
|
|
257
|
+
"Attempts": 1
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"$id": "114",
|
|
261
|
+
"$type": "NINA.Sequencer.Container.EndAreaContainer, NINA.Sequencer",
|
|
262
|
+
"Strategy": {
|
|
263
|
+
"$type": "NINA.Sequencer.Container.ExecutionStrategy.SequentialStrategy, NINA.Sequencer"
|
|
264
|
+
},
|
|
265
|
+
"Name": "End",
|
|
266
|
+
"Conditions": {
|
|
267
|
+
"$id": "115",
|
|
268
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.Conditions.ISequenceCondition, NINA.Sequencer]], System.ObjectModel",
|
|
269
|
+
"$values": []
|
|
270
|
+
},
|
|
271
|
+
"IsExpanded": true,
|
|
272
|
+
"Items": {
|
|
273
|
+
"$id": "116",
|
|
274
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.SequenceItem.ISequenceItem, NINA.Sequencer]], System.ObjectModel",
|
|
275
|
+
"$values": [
|
|
276
|
+
{
|
|
277
|
+
"$id": "117",
|
|
278
|
+
"$type": "NINA.Sequencer.Container.SequentialContainer, NINA.Sequencer",
|
|
279
|
+
"Strategy": {
|
|
280
|
+
"$type": "NINA.Sequencer.Container.ExecutionStrategy.SequentialStrategy, NINA.Sequencer"
|
|
281
|
+
},
|
|
282
|
+
"Name": "Sequential Instruction Set",
|
|
283
|
+
"Conditions": {
|
|
284
|
+
"$id": "118",
|
|
285
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.Conditions.ISequenceCondition, NINA.Sequencer]], System.ObjectModel",
|
|
286
|
+
"$values": []
|
|
287
|
+
},
|
|
288
|
+
"IsExpanded": true,
|
|
289
|
+
"Items": {
|
|
290
|
+
"$id": "119",
|
|
291
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.SequenceItem.ISequenceItem, NINA.Sequencer]], System.ObjectModel",
|
|
292
|
+
"$values": []
|
|
293
|
+
},
|
|
294
|
+
"Triggers": {
|
|
295
|
+
"$id": "123",
|
|
296
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.Trigger.ISequenceTrigger, NINA.Sequencer]], System.ObjectModel",
|
|
297
|
+
"$values": []
|
|
298
|
+
},
|
|
299
|
+
"Parent": {
|
|
300
|
+
"$ref": "114"
|
|
301
|
+
},
|
|
302
|
+
"ErrorBehavior": 0,
|
|
303
|
+
"Attempts": 1
|
|
304
|
+
}
|
|
305
|
+
]
|
|
306
|
+
},
|
|
307
|
+
"Triggers": {
|
|
308
|
+
"$id": "124",
|
|
309
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.Trigger.ISequenceTrigger, NINA.Sequencer]], System.ObjectModel",
|
|
310
|
+
"$values": []
|
|
311
|
+
},
|
|
312
|
+
"Parent": {
|
|
313
|
+
"$ref": "1"
|
|
314
|
+
},
|
|
315
|
+
"ErrorBehavior": 0,
|
|
316
|
+
"Attempts": 1
|
|
317
|
+
}
|
|
318
|
+
]
|
|
319
|
+
},
|
|
320
|
+
"Triggers": {
|
|
321
|
+
"$id": "125",
|
|
322
|
+
"$type": "System.Collections.ObjectModel.ObservableCollection`1[[NINA.Sequencer.Trigger.ISequenceTrigger, NINA.Sequencer]], System.ObjectModel",
|
|
323
|
+
"$values": []
|
|
324
|
+
},
|
|
325
|
+
"Parent": null,
|
|
326
|
+
"ErrorBehavior": 0,
|
|
327
|
+
"Attempts": 1
|
|
328
|
+
}
|
citrascope/logging/__init__.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from logging.handlers import TimedRotatingFileHandler
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
2
5
|
|
|
3
6
|
|
|
4
7
|
class ExcludeHttpRequestFilter(logging.Filter):
|
|
@@ -6,6 +9,22 @@ class ExcludeHttpRequestFilter(logging.Filter):
|
|
|
6
9
|
return "HTTP Request:" not in record.getMessage()
|
|
7
10
|
|
|
8
11
|
|
|
12
|
+
class ExcludeWebLogsFilter(logging.Filter):
|
|
13
|
+
"""Filter out web-related logs (uvicorn, HTTP requests) from file logging."""
|
|
14
|
+
|
|
15
|
+
def filter(self, record):
|
|
16
|
+
# Exclude uvicorn loggers
|
|
17
|
+
if record.name.startswith("uvicorn"):
|
|
18
|
+
return False
|
|
19
|
+
# Exclude HTTP request messages
|
|
20
|
+
if "HTTP Request:" in record.getMessage():
|
|
21
|
+
return False
|
|
22
|
+
# Exclude WebSocket messages
|
|
23
|
+
if "WebSocket" in record.getMessage():
|
|
24
|
+
return False
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
|
|
9
28
|
class ColoredFormatter(logging.Formatter):
|
|
10
29
|
COLORS = {
|
|
11
30
|
"DEBUG": "\033[94m", # Blue
|
|
@@ -17,14 +36,26 @@ class ColoredFormatter(logging.Formatter):
|
|
|
17
36
|
RESET = "\033[0m"
|
|
18
37
|
|
|
19
38
|
def format(self, record):
|
|
39
|
+
# Save original levelname
|
|
40
|
+
original_levelname = record.levelname
|
|
41
|
+
|
|
42
|
+
# Temporarily add color codes
|
|
20
43
|
color = self.COLORS.get(record.levelname, self.RESET)
|
|
21
44
|
record.levelname = f"{color}{record.levelname}{self.RESET}"
|
|
22
|
-
|
|
45
|
+
|
|
46
|
+
# Format the record
|
|
47
|
+
result = super().format(record)
|
|
48
|
+
|
|
49
|
+
# Restore original levelname so other handlers don't get colored version
|
|
50
|
+
record.levelname = original_levelname
|
|
51
|
+
|
|
52
|
+
return result
|
|
23
53
|
|
|
24
54
|
|
|
25
55
|
CITRASCOPE_LOGGER = logging.getLogger()
|
|
26
56
|
CITRASCOPE_LOGGER.setLevel(logging.INFO)
|
|
27
57
|
|
|
58
|
+
# Console handler with colors
|
|
28
59
|
handler = logging.StreamHandler()
|
|
29
60
|
handler.addFilter(ExcludeHttpRequestFilter())
|
|
30
61
|
log_format = "%(asctime)s %(levelname)s %(message)s"
|
|
@@ -33,3 +64,51 @@ formatter = ColoredFormatter(fmt=log_format, datefmt=date_format)
|
|
|
33
64
|
handler.setFormatter(formatter)
|
|
34
65
|
CITRASCOPE_LOGGER.handlers.clear()
|
|
35
66
|
CITRASCOPE_LOGGER.addHandler(handler)
|
|
67
|
+
|
|
68
|
+
# File handler will be added by setup_file_logging()
|
|
69
|
+
_file_handler: Optional[TimedRotatingFileHandler] = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def setup_file_logging(log_file_path: Path, backup_count: int = 30) -> None:
|
|
73
|
+
"""Setup file-based logging with daily rotation.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
log_file_path: Path to the log file (should include date in filename).
|
|
77
|
+
backup_count: Number of daily log files to keep (default 30 days).
|
|
78
|
+
"""
|
|
79
|
+
global _file_handler
|
|
80
|
+
|
|
81
|
+
# Remove existing file handler if present
|
|
82
|
+
if _file_handler is not None:
|
|
83
|
+
CITRASCOPE_LOGGER.removeHandler(_file_handler)
|
|
84
|
+
_file_handler.close()
|
|
85
|
+
_file_handler = None
|
|
86
|
+
|
|
87
|
+
# Create new file handler with daily rotation
|
|
88
|
+
_file_handler = TimedRotatingFileHandler(
|
|
89
|
+
filename=str(log_file_path),
|
|
90
|
+
when="midnight",
|
|
91
|
+
interval=1,
|
|
92
|
+
backupCount=backup_count,
|
|
93
|
+
encoding="utf-8",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Add filter to exclude web logs
|
|
97
|
+
_file_handler.addFilter(ExcludeWebLogsFilter())
|
|
98
|
+
|
|
99
|
+
# Use plain formatter (no ANSI colors for files)
|
|
100
|
+
plain_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
|
101
|
+
_file_handler.setFormatter(plain_formatter)
|
|
102
|
+
_file_handler.setLevel(logging.INFO)
|
|
103
|
+
|
|
104
|
+
# Add to logger
|
|
105
|
+
CITRASCOPE_LOGGER.addHandler(_file_handler)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_file_handler() -> Optional[TimedRotatingFileHandler]:
|
|
109
|
+
"""Get the current file handler.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
The file handler if file logging is set up, None otherwise.
|
|
113
|
+
"""
|
|
114
|
+
return _file_handler
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Log handler that streams logs to web clients via WebSocket."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from collections import deque
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WebLogHandler(logging.Handler):
|
|
10
|
+
"""Custom log handler that buffers logs and makes them available to web clients."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, max_logs: int = 1000):
|
|
13
|
+
super().__init__()
|
|
14
|
+
self.log_buffer = deque(maxlen=max_logs)
|
|
15
|
+
self.web_app = None
|
|
16
|
+
self.loop = None
|
|
17
|
+
|
|
18
|
+
def set_web_app(self, web_app, loop=None):
|
|
19
|
+
"""Set the web app instance for broadcasting logs."""
|
|
20
|
+
self.web_app = web_app
|
|
21
|
+
self.loop = loop
|
|
22
|
+
|
|
23
|
+
def emit(self, record):
|
|
24
|
+
"""Emit a log record."""
|
|
25
|
+
try:
|
|
26
|
+
# Filter out web-related logs from the web UI
|
|
27
|
+
# (uvicorn.access, WebSocket messages, HTTP Request logs, etc.)
|
|
28
|
+
if (
|
|
29
|
+
record.name.startswith("uvicorn")
|
|
30
|
+
or "WebSocket" in record.getMessage()
|
|
31
|
+
or "HTTP Request:" in record.getMessage()
|
|
32
|
+
):
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
# Get the original levelname without color codes
|
|
36
|
+
# Record.levelname might have ANSI codes from ColoredFormatter
|
|
37
|
+
level = record.levelname
|
|
38
|
+
# Strip ANSI codes from level if present
|
|
39
|
+
import re
|
|
40
|
+
|
|
41
|
+
level = re.sub(r"\x1b\[\d+m", "", level)
|
|
42
|
+
|
|
43
|
+
log_entry = {
|
|
44
|
+
"timestamp": self.format_time(record),
|
|
45
|
+
"level": level,
|
|
46
|
+
"message": record.getMessage(), # Use raw message, not formatted
|
|
47
|
+
"module": record.module,
|
|
48
|
+
}
|
|
49
|
+
self.log_buffer.append(log_entry)
|
|
50
|
+
|
|
51
|
+
# Broadcast to web clients if available
|
|
52
|
+
if self.web_app and hasattr(self.web_app, "broadcast_log") and self.loop:
|
|
53
|
+
# Schedule the broadcast in the web server's event loop
|
|
54
|
+
try:
|
|
55
|
+
if self.loop.is_running():
|
|
56
|
+
asyncio.run_coroutine_threadsafe(self.web_app.broadcast_log(log_entry), self.loop)
|
|
57
|
+
except Exception:
|
|
58
|
+
# Silently fail if we can't broadcast
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
except Exception:
|
|
62
|
+
self.handleError(record)
|
|
63
|
+
|
|
64
|
+
def format_time(self, record):
|
|
65
|
+
"""Format the timestamp."""
|
|
66
|
+
from datetime import datetime
|
|
67
|
+
|
|
68
|
+
return datetime.fromtimestamp(record.created).isoformat()
|
|
69
|
+
|
|
70
|
+
def get_recent_logs(self, limit: Optional[int] = None):
|
|
71
|
+
"""Get recent log entries."""
|
|
72
|
+
if limit:
|
|
73
|
+
return list(self.log_buffer)[-limit:]
|
|
74
|
+
return list(self.log_buffer)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""CitraScope settings class using JSON-based configuration."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
import platformdirs
|
|
7
|
+
|
|
8
|
+
# Application constants for platformdirs
|
|
9
|
+
# Defined before imports to avoid circular dependency
|
|
10
|
+
APP_NAME = "citrascope"
|
|
11
|
+
APP_AUTHOR = "citra-space"
|
|
12
|
+
|
|
13
|
+
from citrascope.logging import CITRASCOPE_LOGGER
|
|
14
|
+
from citrascope.settings.settings_file_manager import SettingsFileManager
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CitraScopeSettings:
|
|
18
|
+
"""Settings for CitraScope loaded from JSON configuration file."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
dev: bool = False,
|
|
23
|
+
log_level: str = "INFO",
|
|
24
|
+
keep_images: bool = False,
|
|
25
|
+
):
|
|
26
|
+
"""Initialize settings from JSON config file.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
dev: If True, use development API endpoint
|
|
30
|
+
log_level: Logging level (DEBUG, INFO, WARNING, ERROR)
|
|
31
|
+
keep_images: If True, preserve captured images
|
|
32
|
+
"""
|
|
33
|
+
self.config_manager = SettingsFileManager()
|
|
34
|
+
|
|
35
|
+
# Load configuration from file
|
|
36
|
+
config = self.config_manager.load_config()
|
|
37
|
+
|
|
38
|
+
# Application data directories
|
|
39
|
+
self._images_dir = Path(platformdirs.user_data_dir(APP_NAME, appauthor=APP_AUTHOR)) / "images"
|
|
40
|
+
|
|
41
|
+
# API Settings
|
|
42
|
+
self.host: str = config.get("host", "dev.api.citra.space" if dev else "api.citra.space")
|
|
43
|
+
self.port: int = config.get("port", 443)
|
|
44
|
+
self.use_ssl: bool = config.get("use_ssl", True)
|
|
45
|
+
self.personal_access_token: str = config.get("personal_access_token", "")
|
|
46
|
+
self.telescope_id: str = config.get("telescope_id", "")
|
|
47
|
+
|
|
48
|
+
# Hardware adapter selection
|
|
49
|
+
self.hardware_adapter: str = config.get("hardware_adapter", "")
|
|
50
|
+
|
|
51
|
+
# Hardware adapter-specific settings stored as dict
|
|
52
|
+
self.adapter_settings: Dict[str, Any] = config.get("adapter_settings", {})
|
|
53
|
+
|
|
54
|
+
# Runtime settings (can be overridden by CLI flags)
|
|
55
|
+
self.log_level: str = log_level if log_level != "INFO" else config.get("log_level", "INFO")
|
|
56
|
+
self.keep_images: bool = keep_images if keep_images else config.get("keep_images", False)
|
|
57
|
+
|
|
58
|
+
# Task retry configuration
|
|
59
|
+
self.max_task_retries: int = config.get("max_task_retries", 3)
|
|
60
|
+
self.initial_retry_delay_seconds: int = config.get("initial_retry_delay_seconds", 30)
|
|
61
|
+
self.max_retry_delay_seconds: int = config.get("max_retry_delay_seconds", 300)
|
|
62
|
+
|
|
63
|
+
# Log file configuration
|
|
64
|
+
self.file_logging_enabled: bool = config.get("file_logging_enabled", True)
|
|
65
|
+
self.log_retention_days: int = config.get("log_retention_days", 30)
|
|
66
|
+
|
|
67
|
+
if dev:
|
|
68
|
+
self.host = "dev.api.citra.space"
|
|
69
|
+
CITRASCOPE_LOGGER.info("Using development API endpoint.")
|
|
70
|
+
|
|
71
|
+
def get_images_dir(self) -> Path:
|
|
72
|
+
"""Get the path to the images directory.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Path object pointing to the images directory.
|
|
76
|
+
"""
|
|
77
|
+
return self._images_dir
|
|
78
|
+
|
|
79
|
+
def ensure_images_directory(self) -> None:
|
|
80
|
+
"""Create images directory if it doesn't exist."""
|
|
81
|
+
if not self._images_dir.exists():
|
|
82
|
+
self._images_dir.mkdir(parents=True)
|
|
83
|
+
|
|
84
|
+
def is_configured(self) -> bool:
|
|
85
|
+
"""Check if minimum required configuration is present.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
True if personal_access_token, telescope_id, and hardware_adapter are set.
|
|
89
|
+
"""
|
|
90
|
+
return bool(self.personal_access_token and self.telescope_id and self.hardware_adapter)
|
|
91
|
+
|
|
92
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
93
|
+
"""Convert settings to dictionary for serialization.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dictionary of all settings.
|
|
97
|
+
"""
|
|
98
|
+
return {
|
|
99
|
+
"host": self.host,
|
|
100
|
+
"port": self.port,
|
|
101
|
+
"use_ssl": self.use_ssl,
|
|
102
|
+
"personal_access_token": self.personal_access_token,
|
|
103
|
+
"telescope_id": self.telescope_id,
|
|
104
|
+
"hardware_adapter": self.hardware_adapter,
|
|
105
|
+
"adapter_settings": self.adapter_settings,
|
|
106
|
+
"log_level": self.log_level,
|
|
107
|
+
"keep_images": self.keep_images,
|
|
108
|
+
"max_task_retries": self.max_task_retries,
|
|
109
|
+
"initial_retry_delay_seconds": self.initial_retry_delay_seconds,
|
|
110
|
+
"max_retry_delay_seconds": self.max_retry_delay_seconds,
|
|
111
|
+
"file_logging_enabled": self.file_logging_enabled,
|
|
112
|
+
"log_retention_days": self.log_retention_days,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
def save(self) -> None:
|
|
116
|
+
"""Save current settings to JSON config file."""
|
|
117
|
+
self.config_manager.save_config(self.to_dict())
|
|
118
|
+
CITRASCOPE_LOGGER.info(f"Configuration saved to {self.config_manager.get_config_path()}")
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def from_dict(cls, config: Dict[str, Any]) -> "CitraScopeSettings":
|
|
122
|
+
"""Create settings instance from dictionary.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
config: Dictionary of configuration values.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
New CitraScopeSettings instance.
|
|
129
|
+
"""
|
|
130
|
+
settings = cls()
|
|
131
|
+
settings.host = config.get("host", settings.host)
|
|
132
|
+
settings.port = config.get("port", settings.port)
|
|
133
|
+
settings.use_ssl = config.get("use_ssl", settings.use_ssl)
|
|
134
|
+
settings.personal_access_token = config.get("personal_access_token", "")
|
|
135
|
+
settings.telescope_id = config.get("telescope_id", "")
|
|
136
|
+
settings.hardware_adapter = config.get("hardware_adapter", "")
|
|
137
|
+
settings.adapter_settings = config.get("adapter_settings", {})
|
|
138
|
+
settings.log_level = config.get("log_level", "INFO")
|
|
139
|
+
settings.keep_images = config.get("keep_images", False)
|
|
140
|
+
settings.max_task_retries = config.get("max_task_retries", 3)
|
|
141
|
+
settings.initial_retry_delay_seconds = config.get("initial_retry_delay_seconds", 30)
|
|
142
|
+
settings.max_retry_delay_seconds = config.get("max_retry_delay_seconds", 300)
|
|
143
|
+
settings.file_logging_enabled = config.get("file_logging_enabled", True)
|
|
144
|
+
settings.log_retention_days = config.get("log_retention_days", 30)
|
|
145
|
+
return settings
|