etiket-sync-agent-native 0.3.0b1__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.
@@ -0,0 +1,5 @@
1
+ from .native_sync_class import NativeSync
2
+ from .native_config_class import NativeConfigData
3
+
4
+ __all__ = ["NativeSync", "NativeConfigData"]
5
+ __version__ = "0.3.0b1"
@@ -0,0 +1,23 @@
1
+ import dataclasses
2
+ from typing import Optional
3
+
4
+ # Keep imports minimal to avoid hard dependencies in a fresh template
5
+ # from etiket_sync_agent.crud.sync_sources import crud_sync_sources, SyncSources
6
+ # from etiket_sync_agent.db import get_db_session_context
7
+
8
+ @dataclasses.dataclass
9
+ class NativeConfigData:
10
+ """Template config dataclass for a sync backend.
11
+
12
+ Add your configuration fields here and implement the validate method
13
+ to perform sanity checks and cross-check with existing sources.
14
+ """
15
+ # example_field: str | None = None
16
+
17
+ async def validate(self, current_sync_source: Optional[object] = None):
18
+ """Validate configuration.
19
+
20
+ Implement your validation logic here.
21
+ Raise ValueError with a helpful message on invalid config.
22
+ """
23
+ return True
@@ -0,0 +1,214 @@
1
+ import typing
2
+ import uuid
3
+ import datetime
4
+ import os
5
+
6
+ from etiket_client.local.dao.dataset import dao_dataset, DatasetUpdate as DatasetUpdateLocal
7
+ from etiket_client.local.dao.file import dao_file, FileUpdate as FileUpdateLocal
8
+ from etiket_client.local.database import get_db_session_context
9
+ from etiket_client.local.models.file import FileSelect, FileStatusLocal, FileType
10
+ from etiket_client.local.models.dataset import DatasetRead
11
+
12
+ from etiket_client.local.exceptions import DatasetNotFoundException
13
+
14
+ from etiket_client.remote.endpoints.dataset import dataset_read, dataset_create, dataset_update
15
+ from etiket_client.remote.endpoints.file import file_create, file_generate_presigned_upload_link_single
16
+ from etiket_client.remote.upload.single_part import upload_new_file_single
17
+
18
+ from etiket_client.remote.endpoints.models.dataset import DatasetCreate, DatasetUpdate, DatasetRead as RemoteDatasetRead
19
+ from etiket_client.remote.endpoints.models.file import FileCreate, FileRead
20
+ from etiket_client.remote.endpoints.models.types import FileStatusRem
21
+
22
+ from etiket_sync_agent.backends.sync_source_abstract import SyncSourceDatabaseBase, ScopeRequirement
23
+ from etiket_sync_agent.schemas import SyncItemSchema, SyncItemCreate
24
+ from etiket_sync_agent.sync.sync_records.manager import SyncRecordManager
25
+
26
+ from etiket_sync_agent.sync.sync_utilities import md5
27
+
28
+ from etiket_sync_agent_native.native_config_class import NativeConfigData
29
+
30
+ # TODO tests :: add sync loop item, that first creates a qcodes dataset and then changes that dataset
31
+ # -- this is important to see that the restrictions in the database are set correctly.
32
+
33
+ class NativeSync(SyncSourceDatabaseBase):
34
+ sync_agent_name: typing.ClassVar[str] = "native"
35
+ scope_requirement: typing.ClassVar[ScopeRequirement] = ScopeRequirement.DISABLED
36
+ supports_scope_mapping: typing.ClassVar[bool] = False
37
+ live_sync_implemented: typing.ClassVar[bool] = False
38
+ config_data_class: typing.ClassVar[typing.Type[NativeConfigData]] = NativeConfigData
39
+ has_owner: typing.ClassVar[bool] = False
40
+
41
+ @staticmethod
42
+ async def getNewDatasets(config_data: NativeConfigData, last_sync_item: SyncItemSchema | None) -> typing.List[SyncItemCreate]:
43
+ with get_db_session_context() as session:
44
+ last_timestamp = None
45
+ if last_sync_item is not None:
46
+ last_timestamp = last_sync_item.syncPriority
47
+ datasets = dao_dataset.get_sync_items(last_timestamp, session)
48
+ sync_items = []
49
+ for dataset in datasets:
50
+ modified_time = dataset.modified
51
+ for file in dataset.files:
52
+ if file.synchronized is False and file.modified > modified_time:
53
+ modified_time = file.modified
54
+ modified_time_timestamp = modified_time.replace(tzinfo=datetime.timezone.utc).timestamp()
55
+ sync_item = SyncItemCreate(datasetUUID = dataset.uuid,
56
+ dataIdentifier = str(dataset.uuid),
57
+ syncPriority = modified_time_timestamp,
58
+ owner = dataset.last_modified_by)
59
+ sync_items.append(sync_item)
60
+ return sync_items
61
+
62
+ @staticmethod
63
+ async def checkLiveDataset(configData: NativeConfigData, syncIdentifier: SyncItemSchema, maxPriority: bool) -> bool:
64
+ return False
65
+
66
+ @staticmethod
67
+ async def syncDatasetNormal(configData: NativeConfigData, syncIdentifier: SyncItemSchema, sync_record: SyncRecordManager):
68
+ # Here manual functions are used.
69
+ with sync_record.task("Start synchronization of the dataset"):
70
+ with get_db_session_context() as session:
71
+ with sync_record.task("Read local dataset"):
72
+ dataset_local = dao_dataset.read(syncIdentifier.datasetUUID, session)
73
+ sync_record.add_log("Success!")
74
+
75
+ dataset_remote = None
76
+ with sync_record.task("synchronize metadata to server"):
77
+ try :
78
+ dataset_remote = dataset_read(dataset_local.uuid)
79
+ sync_record.add_log("A remote dataset is already present.")
80
+ except DatasetNotFoundException:
81
+ sync_record.add_log("No remote dataset found.")
82
+
83
+ if not dataset_remote:
84
+ sync_record.add_log("Attempt to create remote dataset.")
85
+ dc = DatasetCreate(**dataset_local.model_dump(), scope_uuid=dataset_local.scope.uuid)
86
+ dataset_create(dc)
87
+ dataset_remote = dataset_read(dataset_local.uuid)
88
+ sync_record.add_log("Remote dataset created.")
89
+ else:
90
+ if dataset_local.modified > dataset_remote.modified:
91
+ if needs_metadata_update(dataset_local, dataset_remote):
92
+ sync_record.add_log("Updating metadata of remote dataset ...")
93
+ du = DatasetUpdate(**dataset_local.model_dump())
94
+ dataset_update(dataset_local.uuid, du)
95
+ sync_record.add_log("Metadata updated.")
96
+ dataset_remote = dataset_read(dataset_local.uuid)
97
+ else:
98
+ sync_record.add_log("Metadata up to date, no updates needed.")
99
+ else:
100
+ sync_record.add_log("Metadata up to date, no updates needed.")
101
+
102
+ with sync_record.task("Synchronizing files to server"):
103
+ files = dataset_local.files if dataset_local.files is not None else []
104
+ for file in files:
105
+ if file.status == FileStatusLocal.complete and file.synchronized is False and file.type != FileType.HDF5_CACHE:
106
+ with sync_record.add_upload_task(file.name) as file_upload_info:
107
+ file_upload_info.filename = file.filename
108
+ file_upload_info.file_uuid = file.uuid
109
+ file_upload_info.version_id = file.version_id
110
+ file_upload_info.file_path = file.local_path
111
+ file_upload_info.file_m_time = file.collected
112
+ file_upload_info.fileType = file.type
113
+ if file.local_path and os.path.exists(file.local_path):
114
+ file_upload_info.size = os.stat(file.local_path).st_size
115
+
116
+ sync_record.add_log(f"Synchronizing file with name {file.name} and version_id {file.version_id}")
117
+ fs = FileSelect(uuid=file.uuid, version_id=file.version_id)
118
+
119
+ file_remote = get_remote_file(dataset_remote.files, file.uuid, file.version_id)
120
+ if file_remote:
121
+ sync_record.add_log("File record already present on the remote server, updating details.")
122
+ # TODO (later) update details --> this is currently done auto in dataqruiser.
123
+ if file_remote.status == FileStatusRem.secured:
124
+ fu = FileUpdateLocal(synchronized=True)
125
+ dao_file.update(fs, fu, session, update_modified_by=False)
126
+ sync_record.add_log("File already secured, done.")
127
+ continue
128
+ else:
129
+ sync_record.add_log("File record already present on the remote server, but not yet secured, proceeding to upload.")
130
+ else:
131
+ sync_record.add_log("Creating file record on the remote server.")
132
+ fc = FileCreate(**file.model_dump(), ds_uuid=dataset_local.uuid)
133
+ file_create(fc)
134
+ sync_record.add_log("File record created.")
135
+
136
+
137
+ sync_record.add_log("Starting upload of the file.")
138
+ upload_info = file_generate_presigned_upload_link_single(file.uuid, file.version_id)
139
+ md5_checksum = md5(file.local_path)
140
+ file_upload_info.md5 = md5_checksum.hexdigest()
141
+ upload_new_file_single(file.local_path, upload_info, md5_checksum)
142
+ sync_record.add_log("Upload finished.")
143
+
144
+ fu = FileUpdateLocal(synchronized=True)
145
+ dao_file.update(fs, fu, session, update_modified_by=False)
146
+ else:
147
+ if file.status != FileStatusLocal.complete:
148
+ sync_record.add_log(f"skipping {file.name} (version :: {file.version_id}) as file status is not yet completed (status = {file.status}).")
149
+ elif file.type == FileType.HDF5_CACHE:
150
+ sync_record.add_log(f"skipping {file.name} (version :: {file.version_id}) as file is marked as cache.")
151
+ elif file.synchronized is True:
152
+ sync_record.add_log(f"skipping {file.name} (version :: {file.version_id}) as file is already synchronized.")
153
+ else:
154
+ raise ValueError(f"Error the code should not reach here. -- name : {file.name}, file_uuid : {file.uuid}, version_id : {file.version_id}, status : {file.status}, synced : {file.synchronized}")
155
+
156
+ # update the local dataset to mark it as synchronized
157
+ ds_local_reread = dao_dataset.read(dataset_local.uuid, session)
158
+ if ds_local_reread.modified == dataset_local.modified:
159
+ du = DatasetUpdateLocal(synchronized=True)
160
+ if needs_metadata_update(ds_local_reread, dataset_remote):
161
+ sync_record.add_log("Updating metadata of local dataset from newer remote ...")
162
+ du = DatasetUpdateLocal(
163
+ name=dataset_remote.name,
164
+ description=dataset_remote.description,
165
+ keywords=dataset_remote.keywords,
166
+ ranking=dataset_remote.ranking,
167
+ attributes=dataset_remote.attributes,
168
+ synchronized=True,
169
+ )
170
+ dao_dataset.update(dataset_local.uuid, du, session)
171
+ sync_record.add_log("Local metadata updated from remote and marked as synchronized.")
172
+ else:
173
+ sync_record.add_log("Not marked as synchronized, as the local dataset was modified during the sync.")
174
+
175
+ sync_record.add_log("Dataset sync is successful!")
176
+
177
+
178
+ @staticmethod
179
+ async def syncDatasetLive(configData: NativeConfigData, syncIdentifier: SyncItemSchema, sync_record: SyncRecordManager):
180
+ raise NotImplementedError
181
+
182
+ def get_remote_file(files : typing.List[FileRead], file_uuid : uuid.UUID, version_id : int):
183
+ for file in files:
184
+ if file.uuid == file_uuid and file.version_id == version_id:
185
+ return file
186
+ return None
187
+
188
+
189
+ def datasets_metadata_equal(ds_local: DatasetRead, ds_remote: RemoteDatasetRead) -> bool:
190
+ """Return True only if all relevant metadata fields match."""
191
+ if ds_local.uuid != ds_remote.uuid:
192
+ return False
193
+ if ds_local.alt_uid != ds_remote.alt_uid:
194
+ return False
195
+ if ds_local.collected != ds_remote.collected:
196
+ return False
197
+ if ds_local.name != ds_remote.name:
198
+ return False
199
+ if ds_local.description != ds_remote.description:
200
+ return False
201
+ if ds_local.creator != ds_remote.creator:
202
+ return False
203
+ if ds_local.ranking != ds_remote.ranking:
204
+ return False
205
+ if ds_local.keywords != ds_remote.keywords:
206
+ return False
207
+ if ds_local.attributes != ds_remote.attributes:
208
+ return False
209
+ return True
210
+
211
+
212
+ def needs_metadata_update(ds_local: DatasetRead, ds_remote: RemoteDatasetRead) -> bool:
213
+ """Return True if any relevant metadata field differs."""
214
+ return not datasets_metadata_equal(ds_local, ds_remote)
@@ -0,0 +1,23 @@
1
+ import dataclasses
2
+
3
+ from typing import Optional
4
+
5
+ import etiket_sync_agent_native
6
+
7
+ from etiket_sync_agent.crud.sync_sources import crud_sync_sources, SyncSources
8
+ from etiket_sync_agent.db import get_db_session_context
9
+
10
+ @dataclasses.dataclass
11
+ class NativeConfigData:
12
+ async def validate(self, current_sync_source : Optional[SyncSources] = None):
13
+ '''
14
+ There can only be one native sync source, validate this, as no parameters are needed.
15
+ '''
16
+ async with get_db_session_context() as session:
17
+ sync_sources = await crud_sync_sources.list_sync_sources(session)
18
+ n_sources = 0
19
+ for sync_source in sync_sources:
20
+ if sync_source.backend == etiket_sync_agent_native.__name__:
21
+ n_sources+=1
22
+ if n_sources>1:
23
+ raise ValueError("Native sync source already present. Only one may be added.")
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: etiket_sync_agent_native
3
+ Version: 0.3.0b1
4
+ Summary: Native filesystem backend for eTiKeT sync agent
5
+ Author: QHarbor team
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Homepage, https://qharbor.nl
8
+ Project-URL: Documentation, https://docs.qharbor.nl
9
+ Keywords: etiket,sync,backend,native,filesystem
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Operating System :: MacOS :: MacOS X
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Scientific/Engineering
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENCE
27
+ Requires-Dist: etiket_sync_agent>=0.3.0b1
28
+ Requires-Dist: etiket_client
29
+ Dynamic: license-file
30
+
31
+ # eTiKeT Sync Agent - Native Backend
32
+
33
+ Backend for synchronizing locally generated datasets (via the qdrive package) with the cloud.
34
+ This is one of the default backends included with the sync agent.
35
+
36
+ ## Installation
37
+
38
+ This package is a dependency of `etiket_sync_agent` and is installed automatically—no separate installation required.
39
+
40
+ ## Usage
41
+
42
+ Once installed, the native backend is automatically discovered and available for synchronization. No additional configuration is needed (for more info, see the [QHarbor documentation](https://docs.qharbor.nl)).
43
+
44
+ ## License
45
+
46
+ Copyright © 2025 QHarbor. All Rights Reserved. See [LICENCE](LICENCE) for details.
@@ -0,0 +1,10 @@
1
+ etiket_sync_agent_native/__init__.py,sha256=z9jRU4lduU4giCemKR8UFfKPKOYq8GlHIJH7Maw-zrA,162
2
+ etiket_sync_agent_native/native_config_class.py,sha256=rG7FGES9G3ohE9_L-g7LwzTAiUeBMH-YG7ta6EbctXg,805
3
+ etiket_sync_agent_native/native_sync_class.py,sha256=-aIqZIkZLrGDPbbaKb-YyX_hILGYVBiTZkc5pWfKBqE,12757
4
+ etiket_sync_agent_native/native_sync_config_class.py,sha256=gaqqUR25Aa4EbzQJj6_uPopoVhUZHc_VhzArhv3ZDl4,902
5
+ etiket_sync_agent_native-0.3.0b1.dist-info/licenses/LICENCE,sha256=tdZwE43Th9efUgN8-4UpMUyh0kYQ4Dk678agSuhpnc0,2357
6
+ etiket_sync_agent_native-0.3.0b1.dist-info/METADATA,sha256=65SObPNaD8B2zqQP35BG-9AZ8-5P7vyromom_DEUIlg,1874
7
+ etiket_sync_agent_native-0.3.0b1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ etiket_sync_agent_native-0.3.0b1.dist-info/entry_points.txt,sha256=nW66Tgz1B6Ggoco8Bn2KAwypT2RdZ5KP2XpCgMwCB0s,85
9
+ etiket_sync_agent_native-0.3.0b1.dist-info/top_level.txt,sha256=7JV6r6h6u4iLUFG_xy6hbkugbyiGG3kJ8BpnCWRXl9E,25
10
+ etiket_sync_agent_native-0.3.0b1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [etiket_sync_agent.backends]
2
+ native_sync_agent = etiket_sync_agent_native:NativeSync
@@ -0,0 +1,34 @@
1
+ All Rights Reserved License
2
+ Copyright ©️ 2024-2026 QHarbor B.V. All Rights Reserved.
3
+
4
+ Agreement to Terms
5
+ By accessing, downloading, installing, or viewing this Software (including its source code, binaries, or application files), you acknowledge and agree to the terms outlined below.
6
+
7
+ Terms and Conditions
8
+ This software and its source code (the "Software") are the exclusive property of QHarbor B.V. and are protected by copyright and other intellectual property laws.
9
+
10
+ License and Testing Exemptions
11
+ Commercial License: If you have entered into a separate commercial license agreement with QHarbor B.V., the terms of that agreement shall supersede the restrictions listed below.
12
+ Testing Permission: If you have obtained written permission for testing/evaluation from QHarbor B.V., you are permitted to install and use the Software for evaluation purposes. However, this permission strictly excludes the right to modify, alter, create derivative works, or reverse engineer the Software.
13
+
14
+ Prohibited Actions
15
+ Unless explicitly authorized by the exemptions above, you are NOT permitted to:
16
+ • Copy, reproduce, or duplicate the Software in any form (except as reasonably necessary for viewing or authorized installation)
17
+ • Modify, alter, or create derivative works based on the Software
18
+ • Distribute, publish, or share the Software with others
19
+ • Reverse engineer, decompile, or disassemble the Software
20
+ • Use the Software for any commercial or non-commercial purposes
21
+ • Transfer, sell, lease, or sublicense the Software
22
+ • Remove or alter any copyright notices or proprietary markings
23
+
24
+ Viewing Only
25
+ For those without a commercial license or written testing permission, the Software is made available for viewing and reference purposes only. Any access to view the source code or application does not grant any rights to use, copy, or modify the Software.
26
+
27
+ No Implied Rights
28
+ No rights are granted by implication, estoppel, or otherwise. All rights not expressly granted are reserved by the copyright holder.
29
+
30
+ Governing Law
31
+ This license and any disputes arising from it shall be governed by the laws of the Netherlands.
32
+
33
+ Disclaimer
34
+ This Software is provided "AS IS" without warranty of any kind. The copyright holder disclaims all warranties and shall not be liable for any damages arising from the use or inability to use this Software.
@@ -0,0 +1 @@
1
+ etiket_sync_agent_native