mercuto-client 0.2.6.dev0__py3-none-any.whl → 0.2.8__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.

Potentially problematic release.


This version of mercuto-client might be problematic. Click here for more details.

@@ -1,6 +1,4 @@
1
1
  import argparse
2
- import fnmatch
3
- import itertools
4
2
  import logging
5
3
  import logging.handlers
6
4
  import os
@@ -10,135 +8,13 @@ from typing import Callable, TypeVar
10
8
 
11
9
  import schedule
12
10
 
13
- from .. import MercutoClient, MercutoHTTPException
14
- from ..types import DataSample
15
11
  from .ftp import simple_ftp_server
16
- from .parsers import detect_parser
12
+ from .mercuto import MercutoIngester
17
13
  from .processor import FileProcessor
18
- from .util import batched, get_free_space_excluding_files, get_my_public_ip
14
+ from .util import get_free_space_excluding_files
19
15
 
20
16
  logger = logging.getLogger(__name__)
21
17
 
22
- NON_RETRYABLE_ERRORS = {400, 404, 409} # HTTP status codes that indicate non-retryable errors
23
-
24
-
25
- class MercutoIngester:
26
- def __init__(self, project_code: str, api_key: str, hostname: str = 'https://api.rockfieldcloud.com.au') -> None:
27
- self._client = MercutoClient(url=hostname)
28
- self._api_key = api_key
29
- with self._client.as_credentials(api_key=api_key) as client:
30
- self._project = client.projects().get_project(project_code)
31
- assert self._project['code'] == project_code
32
-
33
- self._secondary_channels = client.channels().get_channels(project_code, classification='SECONDARY')
34
- self._datatables = list(itertools.chain.from_iterable([dt['datatables'] for dt in client.devices().list_dataloggers(project_code)]))
35
-
36
- self._channel_map = {c['label']: c['code'] for c in self._secondary_channels}
37
-
38
- def update_mapping(self, mapping: dict[str, str]) -> None:
39
- """
40
- Update the channel label to channel code mapping.
41
- """
42
- self._channel_map.update(mapping)
43
- logger.info(f"Updated channel mapping: {self._channel_map}")
44
-
45
- @property
46
- def project_code(self) -> str:
47
- return self._project['code']
48
-
49
- def ping(self) -> None:
50
- """
51
- Ping the Mercuto serverto update the last seen IP address.
52
- """
53
- ip = get_my_public_ip()
54
- with self._client.as_credentials(api_key=self._api_key) as client:
55
- client.projects().ping_project(self.project_code, ip_address=ip)
56
- logging.info(f"Pinged Mercuto server from IP: {ip} for project: {self.project_code}")
57
-
58
- def matching_datatable(self, filename: str) -> str | None:
59
- """
60
- Check if any datatables on the project match this file name.
61
- Returns the datatable code if a match is found, otherwise None.
62
- """
63
- basename = os.path.basename(filename)
64
-
65
- def matches(test: str) -> bool:
66
- """
67
- test should be a pattern or a filename.
68
- E.g. "my_data.csv" or "my_data*.csv", or "/path/to/my_data*.csv"
69
- Do wildcard matching as well as prefix matching.
70
- """
71
- test_base = os.path.basename(test)
72
- if fnmatch.fnmatch(basename, test_base):
73
- return True
74
- lhs, _ = os.path.splitext(test_base)
75
- if basename.startswith(lhs):
76
- return True
77
- return False
78
-
79
- for dt in self._datatables:
80
- # Match using datatable pattern
81
- if matches(dt['name']):
82
- return dt['code']
83
- if dt['src'] and matches(dt['src']):
84
- return dt['code']
85
- return None
86
-
87
- def _upload_samples(self, samples: list[DataSample]) -> bool:
88
- """
89
- Upload samples to the Mercuto project.
90
- """
91
- try:
92
- with self._client.as_credentials(api_key=self._api_key) as client:
93
- for batch in batched(samples, 500):
94
- client.data().upload_samples(batch)
95
- return True
96
- except MercutoHTTPException as e:
97
- if e.status_code in NON_RETRYABLE_ERRORS:
98
- logger.exception(
99
- "Error indicates bad file that should not be retried. Skipping.")
100
- return True
101
- else:
102
- return False
103
-
104
- def _upload_file(self, file_path: str, datatable_code: str) -> bool:
105
- """
106
- Upload a file to the Mercuto project.
107
- """
108
- logging.info(f"Uploadeding file {file_path} to datatable {datatable_code} in project {self.project_code}")
109
- try:
110
- with self._client.as_credentials(api_key=self._api_key) as client:
111
- client.data().upload_file(
112
- project=self.project_code,
113
- datatable=datatable_code,
114
- file=file_path,
115
- )
116
- return True
117
- except MercutoHTTPException as e:
118
- if e.status_code in NON_RETRYABLE_ERRORS:
119
- logger.exception(
120
- "Error indicates bad file that should not be retried. Skipping.")
121
- return True
122
- else:
123
- return False
124
-
125
- def process_file(self, file_path: str) -> bool:
126
- """
127
- Process the received file.
128
- """
129
- logging.info(f"Processing file: {file_path}")
130
- datatable_code = self.matching_datatable(file_path)
131
- if datatable_code:
132
- logger.info(f"Matched datatable code: {datatable_code} for file: {file_path}")
133
- return self._upload_file(file_path, datatable_code)
134
- else:
135
- parser = detect_parser(file_path)
136
- samples = parser(file_path, self._channel_map)
137
- if not samples:
138
- logging.warning(f"No samples found in file: {file_path}")
139
- return True
140
- return self._upload_samples(samples)
141
-
142
18
 
143
19
  T = TypeVar('T')
144
20
 
@@ -277,7 +153,7 @@ if __name__ == '__main__':
277
153
  with simple_ftp_server(directory=buffer_directory,
278
154
  username=args.username, password=args.password, port=args.port,
279
155
  callback=processor.add_file_to_db, rename=not args.no_rename,
280
- workdir=workdir):
156
+ workdir=ftp_dir):
281
157
  schedule.every(60).seconds.do(call_and_log_error, ingester.ping)
282
158
  schedule.every(5).seconds.do(call_and_log_error, processor.process_next_file)
283
159
  schedule.every(2).minutes.do(call_and_log_error, processor.cleanup_old_files)
@@ -0,0 +1,155 @@
1
+ import fnmatch
2
+ import itertools
3
+ import logging
4
+ import os
5
+ from typing import Optional
6
+
7
+ from .. import MercutoClient, MercutoHTTPException
8
+ from ..types import Channel, DataSample, DatatableOut, Project
9
+ from .parsers import detect_parser
10
+ from .util import batched, get_my_public_ip
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ NON_RETRYABLE_ERRORS = {400, 404, 409} # HTTP status codes that indicate non-retryable errors
15
+
16
+
17
+ class MercutoIngester:
18
+ def __init__(self, project_code: str, api_key: str, hostname: str = 'https://api.rockfieldcloud.com.au') -> None:
19
+ self._client = MercutoClient(url=hostname)
20
+ self._api_key = api_key
21
+ self._project_code = project_code
22
+
23
+ self._project: Optional[Project] = None
24
+ self._secondary_channels: Optional[list[Channel]] = None
25
+ self._datatables: Optional[list[DatatableOut]] = None
26
+
27
+ self._channel_map: dict[str, str] = {}
28
+
29
+ def _refresh_mercuto_data(self) -> None:
30
+ with self._client.as_credentials(api_key=self._api_key) as client:
31
+ self._project = client.projects().get_project(self._project_code)
32
+ assert self._project['code'] == self._project_code
33
+
34
+ self._secondary_channels = client.channels().get_channels(self._project_code, classification='SECONDARY')
35
+ self._datatables = list(itertools.chain.from_iterable([dt['datatables'] for dt in client.devices().list_dataloggers(self._project_code)]))
36
+
37
+ self._channel_map.update({c['label']: c['code'] for c in self._secondary_channels})
38
+
39
+ def _can_process(self) -> bool:
40
+ return self._project is not None and self._secondary_channels is not None and self._datatables is not None
41
+
42
+ def update_mapping(self, mapping: dict[str, str]) -> None:
43
+ """
44
+ Update the channel label to channel code mapping.
45
+ """
46
+ self._channel_map.update(mapping)
47
+ logger.info(f"Updated channel mapping: {self._channel_map}")
48
+
49
+ @property
50
+ def project_code(self) -> str:
51
+ return self._project_code
52
+
53
+ def ping(self) -> None:
54
+ """
55
+ Ping the Mercuto serverto update the last seen IP address.
56
+ """
57
+ ip = get_my_public_ip()
58
+ with self._client.as_credentials(api_key=self._api_key) as client:
59
+ client.projects().ping_project(self.project_code, ip_address=ip)
60
+ logging.info(f"Pinged Mercuto server from IP: {ip} for project: {self.project_code}")
61
+
62
+ def matching_datatable(self, filename: str) -> str | None:
63
+ """
64
+ Check if any datatables on the project match this file name.
65
+ Returns the datatable code if a match is found, otherwise None.
66
+ """
67
+ if self._datatables is None:
68
+ raise ValueError("Datatables not loaded. Call _refresh_mercuto_data() first.")
69
+
70
+ basename = os.path.basename(filename)
71
+
72
+ def matches(test: str) -> bool:
73
+ """
74
+ test should be a pattern or a filename.
75
+ E.g. "my_data.csv" or "my_data*.csv", or "/path/to/my_data*.csv"
76
+ Do wildcard matching as well as prefix matching.
77
+ """
78
+ test_base = os.path.basename(test)
79
+ if fnmatch.fnmatch(basename, test_base):
80
+ return True
81
+ lhs, _ = os.path.splitext(test_base)
82
+ if basename.startswith(lhs):
83
+ return True
84
+ return False
85
+
86
+ for dt in self._datatables:
87
+ # Match using datatable pattern
88
+ if matches(dt['name']):
89
+ return dt['code']
90
+ if dt['src'] and matches(dt['src']):
91
+ return dt['code']
92
+ return None
93
+
94
+ def _upload_samples(self, samples: list[DataSample]) -> bool:
95
+ """
96
+ Upload samples to the Mercuto project.
97
+ """
98
+ try:
99
+ with self._client.as_credentials(api_key=self._api_key) as client:
100
+ for batch in batched(samples, 500):
101
+ client.data().upload_samples(batch)
102
+ return True
103
+ except MercutoHTTPException as e:
104
+ if e.status_code in NON_RETRYABLE_ERRORS:
105
+ logger.exception(
106
+ "Error indicates bad file that should not be retried. Skipping.")
107
+ return True
108
+ else:
109
+ return False
110
+
111
+ def _upload_file(self, file_path: str, datatable_code: str) -> bool:
112
+ """
113
+ Upload a file to the Mercuto project.
114
+ """
115
+ logging.info(f"Uploadeding file {file_path} to datatable {datatable_code} in project {self.project_code}")
116
+ try:
117
+ with self._client.as_credentials(api_key=self._api_key) as client:
118
+ client.data().upload_file(
119
+ project=self.project_code,
120
+ datatable=datatable_code,
121
+ file=file_path,
122
+ )
123
+ return True
124
+ except MercutoHTTPException as e:
125
+ if e.status_code in NON_RETRYABLE_ERRORS:
126
+ logger.exception(
127
+ "Error indicates bad file that should not be retried. Skipping.")
128
+ return True
129
+ else:
130
+ return False
131
+
132
+ def process_file(self, file_path: str) -> bool:
133
+ """
134
+ Process the received file.
135
+ """
136
+
137
+ if not self._can_process():
138
+ logging.info("Refreshing Mercuto data...")
139
+ self._refresh_mercuto_data()
140
+ if not self._can_process():
141
+ logging.error("Failed to refresh Mercuto data. Cannot process file yet.")
142
+ return False
143
+
144
+ logging.info(f"Processing file: {file_path}")
145
+ datatable_code = self.matching_datatable(file_path)
146
+ if datatable_code:
147
+ logger.info(f"Matched datatable code: {datatable_code} for file: {file_path}")
148
+ return self._upload_file(file_path, datatable_code)
149
+ else:
150
+ parser = detect_parser(file_path)
151
+ samples = parser(file_path, self._channel_map)
152
+ if not samples:
153
+ logging.warning(f"No samples found in file: {file_path}")
154
+ return True
155
+ return self._upload_samples(samples)
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: mercuto-client
3
+ Version: 0.2.8
4
+ Summary: Library for interfacing with Rockfield's Mercuto API
5
+ Author-email: Daniel Whipp <daniel.whipp@rocktech.com.au>
6
+ License-Expression: AGPL-3.0-only
7
+ Project-URL: Homepage, https://mercuto.rockfieldcloud.com.au
8
+ Project-URL: Repository, https://github.com/RockfieldTechnologiesAustralia/mercuto-client
9
+ Project-URL: Documentation, https://github.com/RockfieldTechnologiesAustralia/mercuto-client/blob/main/README.md
10
+ Keywords: mercuto,rockfield,infratech
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Topic :: Software Development :: Build Tools
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: requests>=2.32
21
+ Requires-Dist: pyftpdlib>=2.0.1
22
+ Requires-Dist: python-dateutil>=2.9.0.post0
23
+ Requires-Dist: pytz>=2025.2
24
+ Requires-Dist: schedule>=1.2.2
25
+ Dynamic: license-file
26
+
27
+ # Mercuto Client Python Library
28
+
29
+ Library for interfacing with Rockfield's Mercuto public API.
30
+ This library is in an early development state and is subject to major structural changes at any time.
31
+
32
+ (Visit our Github Repository)[https://github.com/RockfieldTechnologiesAustralia/mercuto-client]
33
+
34
+ ## Installation
35
+ Install from PyPi: `pip install mercuto-client` or adding the same line into your `requirements.txt`.
36
+
37
+ ## Basic Usage
38
+
39
+ Use the `connect()` function exposed within the main package and provide your API key.
40
+
41
+ ```python
42
+ from mercuto_client import connect
43
+
44
+ client = connect(api_key="<YOUR API KEY>")
45
+ print(client.projects().get_projects())
46
+
47
+ # Logout after finished.
48
+ client.logout()
49
+
50
+ ```
51
+
52
+ You can also use the client as a context manager. It will logout automatically.
53
+
54
+ ```python
55
+ from mercuto_client import MercutoClient
56
+
57
+ with MercutoClient.as_credentials(api_key='<YOUR API KEY>') as client:
58
+ print(client.projects().get_projects())
59
+ ```
60
+
61
+ ## Current Status
62
+ This library is incomplete and may not be fully compliant with the latest Mercuto version. It is only updated periodically and provided for use without any warranty or guarantees.
63
+
64
+ - [x] API Based login (Completed)
65
+ - [ ] Username/password login
@@ -14,16 +14,17 @@ mercuto_client/_tests/test_ingester/test_file_processor.py,sha256=kC1DC0phmjl7jB
14
14
  mercuto_client/_tests/test_ingester/test_ftp.py,sha256=w1CHAGcZy88D2-nY61Gj16l1nHcer9LIKaMc_DXk23o,1318
15
15
  mercuto_client/_tests/test_ingester/test_parsers.py,sha256=SJIdPi_k0rZl2Ee3UFkUo4utJz2aq9Yv5PuEfpy_gog,5961
16
16
  mercuto_client/ingester/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- mercuto_client/ingester/__main__.py,sha256=aJPFVaM7pbkifPXD0RlooXu0q1l_uE00bWQ12H6hwJU,11789
17
+ mercuto_client/ingester/__main__.py,sha256=yF47P7WmCsRRtc8NjIeK1U7P7KJ8pQbQBUCpa1BDohs,6761
18
18
  mercuto_client/ingester/ftp.py,sha256=3-gMzoRCWjLZWeynjkwOXV59B4f0F2VnWp97fuUFTX4,4441
19
+ mercuto_client/ingester/mercuto.py,sha256=9guacyCKsRLi39tp312kSGx8KFnSu8PevP0HoQHNBPo,6125
19
20
  mercuto_client/ingester/processor.py,sha256=XlMMM0taSHZzth39qVMsUkPO0g_ahC7Xcb01rOjQp3I,11906
20
21
  mercuto_client/ingester/util.py,sha256=yq8jgVIDeH4N1TglzaT8uf7z9m0yW7d5NPdwKFVsJKU,1931
21
22
  mercuto_client/ingester/parsers/__init__.py,sha256=2RXriMSH9-ld0W6nHZH7dDZUs8HBbAIM3B7FZwlY5b4,1364
22
23
  mercuto_client/ingester/parsers/campbell.py,sha256=jnuoQug5Rv239ANGp_1BDTM9oTH8nD8k-EJV7N82E38,416
23
24
  mercuto_client/ingester/parsers/generic_csv.py,sha256=UbujpRzOYFIGHY6sbhoVjQX-2bO1arlaFMmfurmiZVI,4025
24
25
  mercuto_client/ingester/parsers/worldsensing.py,sha256=QoSm1cH9A5zkQKaUnLDR0jWzv16RFIimc1zWgjV66PM,974
25
- mercuto_client-0.2.6.dev0.dist-info/licenses/LICENSE,sha256=0R2QbX4pr5XSiwUc2JoGS7Ja4npcQHyZlGJsR-E73I8,32386
26
- mercuto_client-0.2.6.dev0.dist-info/METADATA,sha256=92RFUrBt8cpgJaIwQyjoZeX-pEBX3O88SU5JbcMNijc,752
27
- mercuto_client-0.2.6.dev0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
- mercuto_client-0.2.6.dev0.dist-info/top_level.txt,sha256=ecV4spooVaOU8AlclvojxY1LzLW1byDywh-ayLHvKCs,15
29
- mercuto_client-0.2.6.dev0.dist-info/RECORD,,
26
+ mercuto_client-0.2.8.dist-info/licenses/LICENSE,sha256=0R2QbX4pr5XSiwUc2JoGS7Ja4npcQHyZlGJsR-E73I8,32386
27
+ mercuto_client-0.2.8.dist-info/METADATA,sha256=J9EYId6CVKgJh773sirqtkY9gyEagG7atVjuJG_YscY,2296
28
+ mercuto_client-0.2.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
29
+ mercuto_client-0.2.8.dist-info/top_level.txt,sha256=ecV4spooVaOU8AlclvojxY1LzLW1byDywh-ayLHvKCs,15
30
+ mercuto_client-0.2.8.dist-info/RECORD,,
@@ -1,20 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: mercuto-client
3
- Version: 0.2.6.dev0
4
- Summary: Library for interfacing with Rockfield's Mercuto API
5
- Author-email: Daniel Whipp <daniel.whipp@rocktech.com.au>
6
- Keywords: mercuto,rockfield,infratech
7
- Classifier: Development Status :: 3 - Alpha
8
- Classifier: Intended Audience :: Developers
9
- Classifier: Topic :: Software Development :: Build Tools
10
- Classifier: Programming Language :: Python :: 3.10
11
- Classifier: Programming Language :: Python :: 3.11
12
- Classifier: Programming Language :: Python :: 3.12
13
- Requires-Python: >=3.10
14
- License-File: LICENSE
15
- Requires-Dist: requests>=2.32
16
- Requires-Dist: pyftpdlib>=2.0.1
17
- Requires-Dist: python-dateutil>=2.9.0.post0
18
- Requires-Dist: pytz>=2025.2
19
- Requires-Dist: schedule>=1.2.2
20
- Dynamic: license-file