hardpy 0.19.0__py3-none-any.whl → 0.20.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.
Files changed (27) hide show
  1. hardpy/__init__.py +2 -0
  2. hardpy/cli/cli.py +6 -0
  3. hardpy/common/config.py +19 -0
  4. hardpy/hardpy_panel/api.py +62 -1
  5. hardpy/hardpy_panel/frontend/dist/assets/{allPaths-C_-7WXHD.js → allPaths-BXbcAtew.js} +1 -1
  6. hardpy/hardpy_panel/frontend/dist/assets/{allPathsLoader-DgH0Xily.js → allPathsLoader-lJLHMNNZ.js} +2 -2
  7. hardpy/hardpy_panel/frontend/dist/assets/{browser-ponyfill-BbOvdqIF.js → browser-ponyfill-DzwgrUwX.js} +1 -1
  8. hardpy/hardpy_panel/frontend/dist/assets/{index-DEJb2W0B.js → index-CVhA7vmQ.js} +158 -158
  9. hardpy/hardpy_panel/frontend/dist/assets/{splitPathsBySizeLoader-o5HCcdVL.js → splitPathsBySizeLoader-BdwEQHyO.js} +1 -1
  10. hardpy/hardpy_panel/frontend/dist/index.html +1 -1
  11. hardpy/hardpy_panel/frontend/dist/locales/cs/translation.json +85 -0
  12. hardpy/pytest_hardpy/db/__init__.py +0 -2
  13. hardpy/pytest_hardpy/db/runstore.py +378 -10
  14. hardpy/pytest_hardpy/db/statestore.py +390 -5
  15. hardpy/pytest_hardpy/db/tempstore.py +219 -17
  16. hardpy/pytest_hardpy/plugin.py +24 -0
  17. hardpy/pytest_hardpy/pytest_wrapper.py +6 -1
  18. hardpy/pytest_hardpy/reporter/hook_reporter.py +18 -0
  19. hardpy/pytest_hardpy/result/__init__.py +2 -0
  20. hardpy/pytest_hardpy/result/report_loader/json_loader.py +49 -0
  21. hardpy/pytest_hardpy/result/report_synchronizer/synchronizer.py +25 -9
  22. {hardpy-0.19.0.dist-info → hardpy-0.20.0.dist-info}/METADATA +18 -2
  23. {hardpy-0.19.0.dist-info → hardpy-0.20.0.dist-info}/RECORD +26 -25
  24. hardpy/pytest_hardpy/db/base_store.py +0 -179
  25. {hardpy-0.19.0.dist-info → hardpy-0.20.0.dist-info}/WHEEL +0 -0
  26. {hardpy-0.19.0.dist-info → hardpy-0.20.0.dist-info}/entry_points.txt +0 -0
  27. {hardpy-0.19.0.dist-info → hardpy-0.20.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,17 +1,402 @@
1
1
  # Copyright (c) 2024 Everypin
2
2
  # GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
3
3
 
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ from abc import ABC, abstractmethod
8
+ from json import dumps
4
9
  from logging import getLogger
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from glom import PathAccessError, assign, glom
5
14
 
15
+ from hardpy.common.config import ConfigManager, StorageType
6
16
  from hardpy.common.singleton import SingletonMeta
7
- from hardpy.pytest_hardpy.db.base_store import BaseStore
17
+ from hardpy.pytest_hardpy.db.const import DatabaseField as DF # noqa: N817
8
18
  from hardpy.pytest_hardpy.db.schema import ResultStateStore
9
19
 
20
+ if TYPE_CHECKING:
21
+ from pycouchdb.client import Database # type: ignore[import-untyped]
22
+ from pydantic import BaseModel
23
+
24
+
25
+ def _create_default_doc_structure(doc_id: str, doc_id_for_rev: str) -> dict:
26
+ """Create default document structure with standard fields.
27
+
28
+ Args:
29
+ doc_id (str): Document ID to use
30
+ doc_id_for_rev (str): Document ID for _rev field (for JSON compatibility)
31
+
32
+ Returns:
33
+ dict: Default document structure
34
+ """
35
+ return {
36
+ "_id": doc_id,
37
+ "_rev": doc_id_for_rev,
38
+ DF.MODULES: {},
39
+ DF.DUT: {
40
+ DF.TYPE: None,
41
+ DF.NAME: None,
42
+ DF.REVISION: None,
43
+ DF.SERIAL_NUMBER: None,
44
+ DF.PART_NUMBER: None,
45
+ DF.SUB_UNITS: [],
46
+ DF.INFO: {},
47
+ },
48
+ DF.TEST_STAND: {
49
+ DF.HW_ID: None,
50
+ DF.NAME: None,
51
+ DF.REVISION: None,
52
+ DF.TIMEZONE: None,
53
+ DF.LOCATION: None,
54
+ DF.NUMBER: None,
55
+ DF.INSTRUMENTS: [],
56
+ DF.DRIVERS: {},
57
+ DF.INFO: {},
58
+ },
59
+ DF.PROCESS: {
60
+ DF.NAME: None,
61
+ DF.NUMBER: None,
62
+ DF.INFO: {},
63
+ },
64
+ }
65
+
66
+
67
+ class StateStoreInterface(ABC):
68
+ """Interface for state storage implementations."""
69
+
70
+ @abstractmethod
71
+ def get_field(self, key: str) -> Any: # noqa: ANN401
72
+ """Get field from the state store.
73
+
74
+ Args:
75
+ key (str): Field key, supports nested access with dots
76
+
77
+ Returns:
78
+ Any: Field value
79
+ """
80
+
81
+ @abstractmethod
82
+ def update_doc_value(self, key: str, value: Any) -> None: # noqa: ANN401
83
+ """Update document value in memory (does not persist).
84
+
85
+ Args:
86
+ key (str): Field key, supports nested access with dots
87
+ value (Any): Value to set
88
+ """
89
+
90
+ @abstractmethod
91
+ def update_db(self) -> None:
92
+ """Persist in-memory document to storage backend."""
93
+
94
+ @abstractmethod
95
+ def update_doc(self) -> None:
96
+ """Reload document from storage backend to memory."""
97
+
98
+ @abstractmethod
99
+ def get_document(self) -> BaseModel:
100
+ """Get full document with schema validation.
101
+
102
+ Returns:
103
+ BaseModel: Validated document model
104
+ """
105
+
106
+ @abstractmethod
107
+ def clear(self) -> None:
108
+ """Clear storage and reset to initial state."""
109
+
110
+ @abstractmethod
111
+ def compact(self) -> None:
112
+ """Optimize storage (implementation-specific, may be no-op)."""
10
113
 
11
- class StateStore(BaseStore, metaclass=SingletonMeta):
12
- """HardPy state storage interface for CouchDB."""
114
+
115
+ class JsonStateStore(StateStoreInterface):
116
+ """JSON file-based state storage implementation.
117
+
118
+ Stores test execution state using JSON files.
119
+ """
13
120
 
14
121
  def __init__(self) -> None:
15
- super().__init__("statestore")
122
+ config_manager = ConfigManager()
123
+ self._store_name = "statestore"
124
+ config_storage_path = Path(config_manager.config.database.storage_path)
125
+ if config_storage_path.is_absolute():
126
+ self._storage_dir = config_storage_path / "storage" / self._store_name
127
+ else:
128
+ self._storage_dir = Path(
129
+ config_manager.tests_path
130
+ / config_manager.config.database.storage_path
131
+ / "storage"
132
+ / self._store_name,
133
+ )
134
+ self._storage_dir.mkdir(parents=True, exist_ok=True)
135
+ self._doc_id = config_manager.config.database.doc_id
136
+ self._file_path = self._storage_dir / f"{self._doc_id}.json"
16
137
  self._log = getLogger(__name__)
17
- self._schema = ResultStateStore
138
+ self._schema: type[BaseModel] = ResultStateStore
139
+ self._doc: dict = self._init_doc()
140
+
141
+ def get_field(self, key: str) -> Any: # noqa: ANN401
142
+ """Get field value from document using dot notation.
143
+
144
+ Args:
145
+ key (str): Field key, supports nested access with dots
146
+
147
+ Returns:
148
+ Any: Field value, or None if path does not exist
149
+ """
150
+ try:
151
+ return glom(self._doc, key)
152
+ except PathAccessError:
153
+ return None
154
+
155
+ def update_doc_value(self, key: str, value: Any) -> None: # noqa: ANN401
156
+ """Update document value in memory (does not persist).
157
+
158
+ Args:
159
+ key (str): Field key, supports nested access with dots
160
+ value (Any): Value to set
161
+ """
162
+ try:
163
+ dumps(value)
164
+ except Exception: # noqa: BLE001
165
+ value = dumps(value, default=str)
166
+
167
+ if "." in key:
168
+ assign(self._doc, key, value, missing=dict)
169
+ else:
170
+ self._doc[key] = value
171
+
172
+ def update_db(self) -> None:
173
+ """Persist in-memory document to JSON file with atomic write."""
174
+ self._storage_dir.mkdir(parents=True, exist_ok=True)
175
+ temp_file = self._file_path.with_suffix(".tmp")
176
+
177
+ try:
178
+ with temp_file.open("w") as f:
179
+ json.dump(self._doc, f, indent=2, default=str)
180
+ temp_file.replace(self._file_path)
181
+ except Exception as exc:
182
+ self._log.error(f"Error writing to storage file: {exc}")
183
+ if temp_file.exists():
184
+ temp_file.unlink()
185
+ raise
186
+
187
+ def update_doc(self) -> None:
188
+ """Reload document from JSON file to memory."""
189
+ if self._file_path.exists():
190
+ try:
191
+ with self._file_path.open("r") as f:
192
+ self._doc = json.load(f)
193
+ except json.JSONDecodeError as exc:
194
+ self._log.error(f"Error reading storage file: {exc}")
195
+ except Exception as exc:
196
+ self._log.error(f"Error reading storage file: {exc}")
197
+ raise
198
+
199
+ def get_document(self) -> BaseModel:
200
+ """Get full document with schema validation.
201
+
202
+ Returns:
203
+ BaseModel: Validated document model
204
+ """
205
+ self.update_doc()
206
+ return self._schema(**self._doc)
207
+
208
+ def clear(self) -> None:
209
+ """Clear storage by resetting to initial state (in-memory only)."""
210
+ self._doc = _create_default_doc_structure(self._doc_id, self._doc_id)
211
+
212
+ def compact(self) -> None:
213
+ """Optimize storage (no-op for JSON file storage)."""
214
+
215
+ def _init_doc(self) -> dict:
216
+ """Initialize or load document structure."""
217
+ if self._file_path.exists():
218
+ try:
219
+ with self._file_path.open("r") as f:
220
+ doc = json.load(f)
221
+
222
+ if DF.MODULES not in doc:
223
+ doc[DF.MODULES] = {}
224
+
225
+ # Reset volatile fields for statestore
226
+ default_doc = _create_default_doc_structure(doc["_id"],
227
+ self._doc_id)
228
+ doc[DF.DUT] = default_doc[DF.DUT]
229
+ doc[DF.TEST_STAND] = default_doc[DF.TEST_STAND]
230
+ doc[DF.PROCESS] = default_doc[DF.PROCESS]
231
+
232
+ return doc
233
+ except json.JSONDecodeError:
234
+ self._log.warning(
235
+ f"Corrupted storage file {self._file_path}, creating new",
236
+ )
237
+ except Exception as exc: # noqa: BLE001
238
+ self._log.warning(f"Error loading storage file: {exc}, creating new")
239
+
240
+ return _create_default_doc_structure(self._doc_id, self._doc_id)
241
+
242
+
243
+ class CouchDBStateStore(StateStoreInterface):
244
+ """CouchDB-based state storage implementation.
245
+
246
+ Stores test execution state using CouchDB.
247
+ """
248
+
249
+ def __init__(self) -> None:
250
+ from pycouchdb import Server as DbServer # type: ignore[import-untyped]
251
+ from pycouchdb.exceptions import ( # type: ignore[import-untyped]
252
+ Conflict,
253
+ GenericError,
254
+ )
255
+ from requests.exceptions import ConnectionError # noqa: A004
256
+
257
+ config_manager = ConfigManager()
258
+ config = config_manager.config
259
+ self._db_srv = DbServer(config.database.url)
260
+ self._db_name = "statestore"
261
+ self._doc_id = config.database.doc_id
262
+ self._log = getLogger(__name__)
263
+ self._schema: type[BaseModel] = ResultStateStore
264
+
265
+ # Initialize database
266
+ try:
267
+ self._db: Database = self._db_srv.create(self._db_name) # type: ignore[name-defined]
268
+ except Conflict:
269
+ self._db = self._db_srv.database(self._db_name)
270
+ except GenericError as exc:
271
+ msg = f"Error initializing database {exc}"
272
+ raise RuntimeError(msg) from exc
273
+ except ConnectionError as exc:
274
+ msg = f"Error initializing database: {exc}"
275
+ raise RuntimeError(msg) from exc
276
+
277
+ self._doc: dict = self._init_doc()
278
+
279
+ def get_field(self, key: str) -> Any: # noqa: ANN401
280
+ """Get field from the state store.
281
+
282
+ Args:
283
+ key (str): Field key, supports nested access with dots
284
+
285
+ Returns:
286
+ Any: Field value, or None if path does not exist
287
+ """
288
+ try:
289
+ return glom(self._doc, key)
290
+ except PathAccessError:
291
+ return None
292
+
293
+ def update_doc_value(self, key: str, value: Any) -> None: # noqa: ANN401
294
+ """Update document value in memory (does not persist).
295
+
296
+ Args:
297
+ key (str): Field key, supports nested access with dots
298
+ value (Any): Value to set
299
+ """
300
+ try:
301
+ dumps(value)
302
+ except Exception: # noqa: BLE001
303
+ value = dumps(value, default=str)
304
+
305
+ if "." in key:
306
+ assign(self._doc, key, value, missing=dict)
307
+ else:
308
+ self._doc[key] = value
309
+
310
+ def update_db(self) -> None:
311
+ """Persist in-memory document to storage backend."""
312
+ from pycouchdb.exceptions import Conflict # type: ignore[import-untyped]
313
+
314
+ try:
315
+ self._doc = self._db.save(self._doc)
316
+ except Conflict:
317
+ self._doc["_rev"] = self._db.get(self._doc_id)["_rev"]
318
+ self._doc = self._db.save(self._doc)
319
+
320
+ def update_doc(self) -> None:
321
+ """Reload document from storage backend to memory."""
322
+ self._doc["_rev"] = self._db.get(self._doc_id)["_rev"]
323
+ self._doc = self._db.get(self._doc_id)
324
+
325
+ def get_document(self) -> BaseModel:
326
+ """Get full document with schema validation.
327
+
328
+ Returns:
329
+ BaseModel: Validated document model
330
+ """
331
+ self._doc = self._db.get(self._doc_id)
332
+ return self._schema(**self._doc)
333
+
334
+ def clear(self) -> None:
335
+ """Clear storage and reset to initial state."""
336
+ from pycouchdb.exceptions import ( # type: ignore[import-untyped]
337
+ Conflict,
338
+ NotFound,
339
+ )
340
+
341
+ try:
342
+ self._db.delete(self._doc_id)
343
+ except (Conflict, NotFound):
344
+ self._log.debug("Database will be created for the first time")
345
+ self._doc = self._init_doc()
346
+
347
+ def compact(self) -> None:
348
+ """Optimize storage (implementation-specific, may be no-op)."""
349
+ self._db.compact()
350
+
351
+ def _init_doc(self) -> dict:
352
+ """Initialize or load document structure."""
353
+ from pycouchdb.exceptions import NotFound # type: ignore[import-untyped]
354
+
355
+ try:
356
+ doc = self._db.get(self._doc_id)
357
+ except NotFound:
358
+ # CouchDB doesn't need _rev field in the default structure
359
+ default = _create_default_doc_structure(self._doc_id, self._doc_id)
360
+ del default["_rev"] # CouchDB manages _rev automatically
361
+ return default
362
+
363
+ if DF.MODULES not in doc:
364
+ doc[DF.MODULES] = {}
365
+
366
+ # Reset volatile fields
367
+ default_doc = _create_default_doc_structure(doc["_id"], self._doc_id)
368
+ doc[DF.DUT] = default_doc[DF.DUT]
369
+ doc[DF.TEST_STAND] = default_doc[DF.TEST_STAND]
370
+ doc[DF.PROCESS] = default_doc[DF.PROCESS]
371
+
372
+ return doc
373
+
374
+
375
+ class StateStore(metaclass=SingletonMeta):
376
+ """HardPy state storage factory for test execution state.
377
+
378
+ Creates appropriate storage backend based on configuration:
379
+ - JSON file storage when storage_type is "json"
380
+ - CouchDB storage when storage_type is "couchdb"
381
+
382
+ This ensures state data is stored in the same backend as the main data.
383
+
384
+ Note: This class acts as a factory. When instantiated, it returns
385
+ the appropriate concrete implementation (JsonStateStore or CouchDBStateStore).
386
+ """
387
+
388
+ def __new__(cls) -> StateStoreInterface: # type: ignore[misc]
389
+ """Create and return the appropriate storage implementation.
390
+
391
+ Returns:
392
+ StateStoreInterface: Concrete storage implementation based on config
393
+ """
394
+ config = ConfigManager()
395
+ storage_type = config.config.database.storage_type
396
+
397
+ if storage_type == StorageType.JSON:
398
+ return JsonStateStore()
399
+ if storage_type == StorageType.COUCHDB:
400
+ return CouchDBStateStore()
401
+ msg = f"Unknown storage type: {storage_type}"
402
+ raise ValueError(msg)
@@ -3,52 +3,254 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ import json
7
+ from abc import ABC, abstractmethod
6
8
  from logging import getLogger
9
+ from pathlib import Path
7
10
  from typing import TYPE_CHECKING
8
11
 
9
- from pycouchdb.exceptions import Conflict, NotFound
12
+ from uuid6 import uuid7
10
13
 
14
+ from hardpy.common.config import ConfigManager, StorageType
11
15
  from hardpy.common.singleton import SingletonMeta
12
- from hardpy.pytest_hardpy.db.base_store import BaseStore
13
16
  from hardpy.pytest_hardpy.db.schema import ResultRunStore
14
17
 
15
18
  if TYPE_CHECKING:
16
19
  from collections.abc import Generator
17
20
 
18
21
 
19
- class TempStore(BaseStore, metaclass=SingletonMeta):
20
- """HardPy temporary storage for data syncronization."""
22
+ class TempStoreInterface(ABC):
23
+ """Interface for temporary storage implementations."""
24
+
25
+ @abstractmethod
26
+ def push_report(self, report: ResultRunStore) -> bool:
27
+ """Push report to the temporary storage.
28
+
29
+ Args:
30
+ report (ResultRunStore): report to store
31
+
32
+ Returns:
33
+ bool: True if successful, False otherwise
34
+ """
35
+
36
+ @abstractmethod
37
+ def reports(self) -> Generator[dict]:
38
+ """Get all reports from the temporary storage.
39
+
40
+ Yields:
41
+ dict: report from temporary storage
42
+ """
43
+
44
+ @abstractmethod
45
+ def delete(self, report_id: str) -> bool:
46
+ """Delete report from the temporary storage.
47
+
48
+ Args:
49
+ report_id (str): report ID to delete
50
+
51
+ Returns:
52
+ bool: True if successful, False otherwise
53
+ """
54
+
55
+ def dict_to_schema(self, report: dict) -> ResultRunStore:
56
+ """Convert report dict to report schema.
57
+
58
+ Args:
59
+ report (dict): report dictionary
60
+
61
+ Returns:
62
+ ResultRunStore: validated report schema
63
+ """
64
+ return ResultRunStore(**report)
65
+
66
+
67
+ class JsonTempStore(TempStoreInterface):
68
+ """JSON file-based temporary storage implementation.
69
+
70
+ Stores reports temporarily when StandCloud sync fails using JSON files.
71
+ """
21
72
 
22
73
  def __init__(self) -> None:
23
- super().__init__("tempstore")
24
74
  self._log = getLogger(__name__)
25
- self._doc: dict = self._init_doc()
75
+ config = ConfigManager()
76
+ config_storage_path = Path(config.config.database.storage_path)
77
+ if config_storage_path.is_absolute():
78
+ self._storage_dir = config_storage_path / "storage" / "tempstore"
79
+ else:
80
+ self._storage_dir = Path(
81
+ config.tests_path
82
+ / config.config.database.storage_path
83
+ / "storage"
84
+ / "tempstore",
85
+ )
86
+ self._storage_dir.mkdir(parents=True, exist_ok=True)
26
87
  self._schema = ResultRunStore
27
88
 
28
89
  def push_report(self, report: ResultRunStore) -> bool:
29
- """Push report to the report database."""
90
+ """Push report to the temporary storage.
91
+
92
+ Args:
93
+ report (ResultRunStore): report to store
94
+
95
+ Returns:
96
+ bool: True if successful, False otherwise
97
+ """
30
98
  report_dict = report.model_dump()
31
- report_id = report_dict.pop("id")
99
+ report_dict.pop("id", None)
100
+ report_id = str(uuid7())
101
+ report_dict["_id"] = report_id
102
+ report_dict["_rev"] = report_id
103
+ report_file = self._storage_dir / f"{report_id}.json"
104
+
105
+ try:
106
+ with report_file.open("w") as f:
107
+ json.dump(report_dict, f, indent=2, default=str)
108
+ except Exception as exc: # noqa: BLE001
109
+ self._log.error(f"Error while saving report {report_id}: {exc}")
110
+ return False
111
+ else:
112
+ self._log.debug(f"Report saved with id: {report_id}")
113
+ return True
114
+
115
+ def reports(self) -> Generator[dict]:
116
+ """Get all reports from the temporary storage.
117
+
118
+ Yields:
119
+ dict: report from temporary storage
120
+ """
121
+ for report_file in self._storage_dir.glob("*.json"):
122
+ try:
123
+ with report_file.open("r") as f:
124
+ report_dict = json.load(f)
125
+ yield report_dict
126
+ except Exception as exc: # noqa: BLE001, PERF203
127
+ self._log.error(f"Error loading report from {report_file}: {exc}")
128
+ continue
129
+
130
+ def delete(self, report_id: str) -> bool:
131
+ """Delete report from the temporary storage.
132
+
133
+ Args:
134
+ report_id (str): report ID to delete
135
+
136
+ Returns:
137
+ bool: True if successful, False otherwise
138
+ """
139
+ report_file = self._storage_dir / f"{report_id}.json"
140
+ try:
141
+ report_file.unlink()
142
+ except FileNotFoundError:
143
+ self._log.warning(f"Report {report_id} not found in temporary storage")
144
+ return False
145
+ except Exception as exc: # noqa: BLE001
146
+ self._log.error(f"Error deleting report {report_id}: {exc}")
147
+ return False
148
+ else:
149
+ return True
150
+
151
+
152
+ class CouchDBTempStore(TempStoreInterface):
153
+ """CouchDB-based temporary storage implementation.
154
+
155
+ Stores reports temporarily when StandCloud sync fails using CouchDB.
156
+ """
157
+
158
+ def __init__(self) -> None:
159
+ from pycouchdb import Server as DbServer # type: ignore[import-untyped]
160
+ from pycouchdb.exceptions import Conflict # type: ignore[import-untyped]
161
+
162
+ self._log = getLogger(__name__)
163
+ config = ConfigManager()
164
+ self._db_srv = DbServer(config.config.database.url)
165
+ self._db_name = "tempstore"
166
+ self._schema = ResultRunStore
167
+
168
+ try:
169
+ self._db = self._db_srv.create(self._db_name)
170
+ except Conflict:
171
+ # database already exists
172
+ self._db = self._db_srv.database(self._db_name)
173
+
174
+ def push_report(self, report: ResultRunStore) -> bool:
175
+ """Push report to the temporary storage.
176
+
177
+ Args:
178
+ report (ResultRunStore): report to store
179
+
180
+ Returns:
181
+ bool: True if successful, False otherwise
182
+ """
183
+ from pycouchdb.exceptions import Conflict # type: ignore[import-untyped]
184
+
185
+ report_dict = report.model_dump()
186
+ report_id = report_dict.pop("id", None)
187
+ if not report_id:
188
+ self._log.error("Report missing required 'id' field")
189
+ return False
32
190
  try:
33
191
  self._db.save(report_dict)
34
192
  except Conflict as exc:
35
193
  self._log.error(f"Error while saving report {report_id}: {exc}")
36
194
  return False
37
- self._log.debug(f"Report saved with id: {report_id}")
38
- return True
195
+ else:
196
+ self._log.debug(f"Report saved with id: {report_id}")
197
+ return True
198
+
199
+ def reports(self) -> Generator[dict]:
200
+ """Get all reports from the temporary storage.
39
201
 
40
- def reports(self) -> Generator[ResultRunStore]:
41
- """Get all reports from the report database."""
202
+ Yields:
203
+ dict: report from temporary storage
204
+ """
42
205
  yield from self._db.all()
43
206
 
44
207
  def delete(self, report_id: str) -> bool:
45
- """Delete report from the report database."""
208
+ """Delete report from the temporary storage.
209
+
210
+ Args:
211
+ report_id (str): report ID to delete
212
+
213
+ Returns:
214
+ bool: True if successful, False otherwise
215
+ """
216
+ from pycouchdb.exceptions import ( # type: ignore[import-untyped]
217
+ Conflict,
218
+ NotFound,
219
+ )
220
+
46
221
  try:
47
222
  self._db.delete(report_id)
48
223
  except (NotFound, Conflict):
49
224
  return False
50
- return True
225
+ else:
226
+ return True
51
227
 
52
- def dict_to_schema(self, report: dict) -> ResultRunStore:
53
- """Convert report dict to report schema."""
54
- return self._schema(**report)
228
+
229
+ class TempStore(metaclass=SingletonMeta):
230
+ """HardPy temporary storage factory for data synchronization.
231
+
232
+ Creates appropriate storage backend based on configuration:
233
+ - JSON file storage when storage_type is "json"
234
+ - CouchDB storage when storage_type is "couchdb"
235
+
236
+ This ensures temporary reports are stored in the same backend as the main data.
237
+
238
+ Note: This class acts as a factory. When instantiated, it returns
239
+ the appropriate concrete implementation (JsonTempStore or CouchDBTempStore).
240
+ """
241
+
242
+ def __new__(cls) -> TempStoreInterface: # type: ignore[misc]
243
+ """Create and return the appropriate storage implementation.
244
+
245
+ Returns:
246
+ TempStoreInterface: Concrete storage implementation based on config
247
+ """
248
+ config = ConfigManager()
249
+ storage_type = config.config.database.storage_type
250
+
251
+ if storage_type == StorageType.JSON:
252
+ return JsonTempStore()
253
+ if storage_type == StorageType.COUCHDB:
254
+ return CouchDBTempStore()
255
+ msg = f"Unknown storage type: {storage_type}"
256
+ raise ValueError(msg)