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.
- etiket_sync_agent_native/__init__.py +5 -0
- etiket_sync_agent_native/native_config_class.py +23 -0
- etiket_sync_agent_native/native_sync_class.py +214 -0
- etiket_sync_agent_native/native_sync_config_class.py +23 -0
- etiket_sync_agent_native-0.3.0b1.dist-info/METADATA +46 -0
- etiket_sync_agent_native-0.3.0b1.dist-info/RECORD +10 -0
- etiket_sync_agent_native-0.3.0b1.dist-info/WHEEL +5 -0
- etiket_sync_agent_native-0.3.0b1.dist-info/entry_points.txt +2 -0
- etiket_sync_agent_native-0.3.0b1.dist-info/licenses/LICENCE +34 -0
- etiket_sync_agent_native-0.3.0b1.dist-info/top_level.txt +1 -0
|
@@ -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,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
|