stidapi 1.4.4__tar.gz → 1.4.5__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stidapi
3
- Version: 1.4.4
3
+ Version: 1.4.5
4
4
  Summary: Python client for connecting to Equinor STIDapi
5
5
  Author: Åsmund Våge Fannemel
6
6
  Author-email: Åsmund Våge Fannemel <asmf@equinor.com>
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
13
  Classifier: Programming Language :: Python :: 3.14
14
- Requires-Dist: eq-api-connector>=1.1.1,<2
14
+ Requires-Dist: eq-api-connector>=1.2.0,<2
15
15
  Requires-Python: >=3.10, <4
16
16
  Project-URL: repository, https://github.com/equinor/STIDapi-python
17
17
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stidapi"
3
- version = "1.4.4"
3
+ version = "1.4.5"
4
4
  description = "Python client for connecting to Equinor STIDapi"
5
5
  requires-python = ">=3.10,<4"
6
6
  readme = "README.md"
@@ -13,7 +13,7 @@ classifiers = [
13
13
  "Programming Language :: Python :: 3.13",
14
14
  "Programming Language :: Python :: 3.14",
15
15
  ]
16
- dependencies = ["eq-api-connector>=1.1.1,<2"]
16
+ dependencies = ["eq-api-connector>=1.2.0,<2"]
17
17
 
18
18
  [[project.authors]]
19
19
  name = "Åsmund Våge Fannemel"
@@ -31,5 +31,5 @@ dev = [
31
31
  ]
32
32
 
33
33
  [build-system]
34
- requires = ["uv_build>=0.11.16,<0.13.0"]
34
+ requires = ["uv_build>=0.11.16,<0.14.0"]
35
35
  build-backend = "uv_build"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stidapi"
3
- version = "1.4.4"
3
+ version = "1.4.5"
4
4
  description = "Python client for connecting to Equinor STIDapi"
5
5
  authors = [{ name = "Åsmund Våge Fannemel", email = "asmf@equinor.com" }]
6
6
  requires-python = ">=3.10,<4"
@@ -14,7 +14,7 @@ classifiers = [
14
14
  "Programming Language :: Python :: 3.13",
15
15
  "Programming Language :: Python :: 3.14",
16
16
  ]
17
- dependencies = ["eq-api-connector>=1.1.1,<2"]
17
+ dependencies = ["eq-api-connector>=1.2.0,<2"]
18
18
 
19
19
  [project.urls]
20
20
  repository = "https://github.com/equinor/STIDapi-python"
@@ -28,5 +28,5 @@ dev = [
28
28
  ]
29
29
 
30
30
  [build-system]
31
- requires = ["uv_build>=0.11.16,<0.13.0"]
31
+ requires = ["uv_build>=0.11.16,<0.14.0"]
32
32
  build-backend = "uv_build"
@@ -220,7 +220,7 @@ class Doc:
220
220
  raise ValueError("doc_no must be a non-empty string")
221
221
 
222
222
  self._data: Dict[str, Any] = {}
223
- self._full_data_loaded = data is None or "currentRevision" in data
223
+ self._full_data_loaded = data is None or self._has_full_data(data)
224
224
 
225
225
  if data is not None:
226
226
  self._data = data
@@ -244,6 +244,15 @@ class Doc:
244
244
  """Get document number."""
245
245
  return self._data.get("docNo", "")
246
246
 
247
+ @staticmethod
248
+ def _has_full_data(data: Dict[str, Any]) -> bool:
249
+ """Return whether document data contains usable revision information."""
250
+ current_revision = data.get("currentRevision")
251
+ if isinstance(current_revision, dict) and current_revision:
252
+ return True
253
+ project_revisions = data.get("projectRevisions")
254
+ return isinstance(project_revisions, list) and bool(project_revisions)
255
+
247
256
  def _ensure_full_data(self) -> None:
248
257
  """Reload document data once when initialized from a partial reference."""
249
258
  if self._full_data_loaded:
@@ -354,10 +363,8 @@ class Doc:
354
363
  if not inst_code or not doc_no:
355
364
  return None
356
365
 
357
- url = urljoin(
358
- u.get_api_url(), str(inst_code) + "/document?docNo=" + str(doc_no)
359
- )
360
- return u.get_json(url)
366
+ url = u.build_url(f"{inst_code}/document", {"docNo": doc_no})
367
+ return u.get_json_dict(url)
361
368
 
362
369
  @staticmethod
363
370
  def from_data(data: Dict[str, Any]) -> "Doc":
@@ -91,10 +91,9 @@ class Plant:
91
91
  @classmethod
92
92
  def get_all_data(cls):
93
93
  if len(cls._plant_list) == 0:
94
- parsed_json = u.get_json("/plants")
94
+ parsed_json = u.get_json_list("/plants")
95
95
 
96
- if isinstance(parsed_json, list):
97
- cls._plant_list = [x for x in parsed_json if "instCode" in x]
96
+ cls._plant_list = [x for x in parsed_json if isinstance(x, dict) and "instCode" in x]
98
97
 
99
98
  return cls._plant_list
100
99
 
@@ -1,6 +1,4 @@
1
1
  from typing import Any, Dict, List, Optional
2
- from urllib.parse import urljoin
3
-
4
2
  import stidapi.utils as u
5
3
 
6
4
 
@@ -50,9 +48,8 @@ class System:
50
48
  Returns:
51
49
  List of dictionaries containing system data.
52
50
  """
53
- url = urljoin(u.get_api_url(), f"{inst_code}/system?isValid={is_valid}")
54
- parsed_json = u.get_json(url)
55
- return parsed_json if isinstance(parsed_json, list) else []
51
+ url = u.build_url(f"{inst_code}/system", {"isValid": is_valid})
52
+ return [item for item in u.get_json_list(url) if isinstance(item, dict)]
56
53
 
57
54
  @staticmethod
58
55
  def get_system(inst_code: str, get_statistics: bool = False) -> List["System"]:
@@ -1,6 +1,4 @@
1
1
  from typing import Any, Dict, List, Optional, Tuple, Union
2
- from urllib.parse import urljoin
3
-
4
2
  from stidapi import Doc
5
3
  import stidapi.utils as u
6
4
 
@@ -41,28 +39,6 @@ class Tag:
41
39
  else:
42
40
  self._data = data
43
41
 
44
- if self.is_function:
45
- function_prop = [("functionBlock", "FB")]
46
- for prop in function_prop:
47
- if prop[0] in self._data:
48
- self.__setattr__(prop[1], self._data[prop[0]])
49
-
50
- if self.is_signal:
51
- signal_prop = [("signalType", "signalType")]
52
- for prop in signal_prop:
53
- if prop[0] in self._data:
54
- self.__setattr__(prop[1], self._data[prop[0]])
55
-
56
- optional_prop = [
57
- ("setpointL", "L"),
58
- ("setpointLl", "LL"),
59
- ("setpointH", "H"),
60
- ("setpointHh", "HH"),
61
- ]
62
- for prop in optional_prop:
63
- if prop[0] in self._data and self._data[prop[0]] is not None:
64
- self.__setattr__(prop[1], self._data[prop[0]])
65
-
66
42
  @property
67
43
  def data(self) -> Dict[str, Any]:
68
44
  """Get the tag data dictionary."""
@@ -107,15 +83,67 @@ class Tag:
107
83
 
108
84
  @property
109
85
  def is_function(self) -> bool:
110
- """Check if tag is a function block."""
111
- return bool(
112
- self.data.get("isFunction", self.category_description == "FUNCTION")
86
+ """Check if tag is a function tag.
87
+
88
+ True when the tag category is FUNCTION or when STID flags the tag as
89
+ having function data (``isFunction``).
90
+ """
91
+ return self.category_description == "FUNCTION" or bool(
92
+ self.data.get("isFunction", False)
113
93
  )
114
94
 
115
95
  @property
116
96
  def is_signal(self) -> bool:
117
- """Check if tag is a signal."""
118
- return bool(self.data.get("isSignal", self.category_description == "SIGNAL"))
97
+ """Check if tag is a signal tag.
98
+
99
+ True when the tag category is SIGNAL or when STID flags the tag as
100
+ having signal data (``isSignal``).
101
+ """
102
+ return self.category_description == "SIGNAL" or bool(
103
+ self.data.get("isSignal", False)
104
+ )
105
+
106
+ @property
107
+ def FB(self) -> Optional[str]:
108
+ """Get the function-block code for function tags.
109
+
110
+ Returns "" for tags identified by is_function without a defined function
111
+ block and None for tags that are not identified as functions.
112
+ """
113
+ if not self.is_function:
114
+ return None
115
+ return str(self.data.get("functionBlock") or "")
116
+
117
+ @property
118
+ def signalType(self) -> Optional[str]:
119
+ """Get the signal type for signal tags.
120
+
121
+ Returns "" for tags identified by is_signal without a defined signal
122
+ type and None for tags that are not identified as signals.
123
+ """
124
+ if not self.is_signal:
125
+ return None
126
+ return str(self.data.get("signalType") or "")
127
+
128
+ @property
129
+ def L(self) -> Optional[str]:
130
+ """Get the low setpoint, if defined."""
131
+ return self.data.get("setpointL")
132
+
133
+ @property
134
+ def LL(self) -> Optional[str]:
135
+ """Get the low-low setpoint, if defined."""
136
+ return self.data.get("setpointLl")
137
+
138
+ @property
139
+ def H(self) -> Optional[str]:
140
+ """Get the high setpoint, if defined."""
141
+ return self.data.get("setpointH")
142
+
143
+ @property
144
+ def HH(self) -> Optional[str]:
145
+ """Get the high-high setpoint, if defined."""
146
+ return self.data.get("setpointHh")
119
147
 
120
148
  @property
121
149
  def status(self) -> str:
@@ -190,12 +218,11 @@ class Tag:
190
218
  Returns:
191
219
  List[dict]: List of dicts describing document references.
192
220
  """
193
- url = urljoin(
194
- u.get_api_url(),
195
- f"{str(self.inst_code)}/tag/document-refs?tagNo={str(self.tag_no)}&noContentAs200=true",
221
+ url = u.build_url(
222
+ f"{self.inst_code}/tag/document-refs",
223
+ {"tagNo": self.tag_no, "noContentAs200": True},
196
224
  )
197
- json_data = u.get_json(url)
198
- return json_data if isinstance(json_data, list) else []
225
+ return [item for item in u.get_json_list(url) if isinstance(item, dict)]
199
226
 
200
227
  def get_functional_location(self) -> str:
201
228
  """Get functional location of Tagged item or empty string if it does not have a functional location.
@@ -203,11 +230,10 @@ class Tag:
203
230
  Returns:
204
231
  str: Functional location or empty string if tagged item does not have a functional location.
205
232
  """
206
- url = urljoin(
207
- u.get_api_url(),
208
- f"portal/{self.inst_code}/tag/sap-tag?tagNo={self.tag_no}",
233
+ url = u.build_url(
234
+ f"portal/{self.inst_code}/tag/sap-tag", {"tagNo": self.tag_no}
209
235
  )
210
- json_data = u.get_json(url)
236
+ json_data = u.get_json_dict(url)
211
237
 
212
238
  if isinstance(json_data, dict) and "functionalLocation" in json_data:
213
239
  return json_data.get("functionalLocation", "")
@@ -237,7 +263,7 @@ class Tag:
237
263
  if not inst_code or not tag_no:
238
264
  return {}
239
265
 
240
- url = urljoin(u.get_api_url(), str(inst_code) + "/tag?tagNo=" + str(tag_no))
266
+ url = u.build_url(f"{inst_code}/tag", {"tagNo": tag_no})
241
267
  data = u.get_json(url)
242
268
 
243
269
  if isinstance(data, list) and len(data) == 1:
@@ -279,53 +305,20 @@ class Tag:
279
305
  List[Tag]: List of Tag objects with a bit less data than when getting
280
306
  """
281
307
 
282
- # Build query parameters
283
- if len(tag_no) > 0:
284
- tag_no = "tagNo=" + tag_no + "&"
285
-
286
- if len(description) > 0:
287
- description = "description=" + description + "&"
288
-
289
- if len(tag_status) > 0:
290
- tag_status = "tagStatus=" + tag_status + "&"
291
-
292
- if tag_category > 0:
293
- tag_category_string = "tagCategory=" + str(tag_category) + "&"
294
- else:
295
- tag_category_string = ""
296
-
297
- if len(tag_type) > 0:
298
- tag_type = "tagType=" + tag_type + "&"
299
-
300
- if len(system) > 0:
301
- system = "system=" + system + "&"
302
-
303
- if len(discipline_code) > 0:
304
- discipline_code = "disciplineCode=" + discipline_code + "&"
305
-
306
- skip_string = "skip=" + str(skip) + "&"
307
-
308
- # take shall be last, thus no trailing &
309
- take_string = "take=" + str(take)
310
-
311
- url = urljoin(
312
- u.get_api_url(),
313
- f"/{inst_code}/tags"
314
- + "?"
315
- + str(tag_no)
316
- + str(description)
317
- + str(tag_status)
318
- + str(tag_category_string)
319
- + str(tag_type)
320
- + str(system)
321
- + str(skip_string)
322
- + str(take_string),
323
- )
324
-
325
- result = u.get_json(url)
326
- if isinstance(result, list):
327
- return [Tag(data=x) for x in result]
328
- return []
308
+ query = {
309
+ "tagNo": tag_no,
310
+ "description": description,
311
+ "tagStatus": tag_status,
312
+ "tagCategory": tag_category if tag_category > 0 else None,
313
+ "tagType": tag_type,
314
+ "system": system,
315
+ "disciplineCode": discipline_code,
316
+ "skip": skip,
317
+ "take": take,
318
+ }
319
+ url = u.build_url(f"{inst_code}/tags", query)
320
+
321
+ return [Tag(data=x) for x in u.get_json_list(url) if isinstance(x, dict)]
329
322
 
330
323
  @staticmethod
331
324
  def search_additional_field(
@@ -341,17 +334,17 @@ class Tag:
341
334
  Returns:
342
335
  List[Tag]: List of Tag objects
343
336
  """
344
- url = urljoin(
345
- u.get_api_url(),
346
- f"/{inst_code}/tag-additional-field/{field_name}?searchValue={value}&noContentAs200=true",
337
+ url = u.build_url(
338
+ f"{inst_code}/tag-additional-field/{field_name}",
339
+ {"searchValue": value, "noContentAs200": True},
347
340
  )
348
- res = u.get_json(url)
341
+ res = u.get_json_list(url)
349
342
 
350
- if isinstance(res, list):
351
- return [Tag(inst_code=inst_code, tag_no=x["tagNo"], data=x) for x in res]
352
- return []
353
-
354
- raise ValueError(f"Invalid data {res} returned from query {url}")
343
+ return [
344
+ Tag(inst_code=inst_code, tag_no=x["tagNo"], data=x)
345
+ for x in res
346
+ if isinstance(x, dict) and "tagNo" in x
347
+ ]
355
348
 
356
349
  @staticmethod
357
350
  def get_from_additional_field(
@@ -1,6 +1,7 @@
1
1
  import json
2
2
  import logging
3
- from typing import List, Optional, Union
3
+ from typing import Any, Dict, List, Optional, Union
4
+ from urllib.parse import urlencode, urljoin
4
5
  from eq_api_connector import APIConnector
5
6
  from types import SimpleNamespace
6
7
 
@@ -47,10 +48,24 @@ def set_api_client(client: STIDApiClient):
47
48
  _client = client
48
49
 
49
50
 
50
- def get_api_url():
51
+ def get_api_url() -> str:
51
52
  return get_api_client().url
52
53
 
53
54
 
55
+ def build_url(path: str, query: Optional[Dict[str, Any]] = None) -> str:
56
+ """Build an API URL with URL-encoded query parameters."""
57
+ url = urljoin(get_api_url(), path.lstrip("/"))
58
+ if query:
59
+ query = {
60
+ key: value
61
+ for key, value in query.items()
62
+ if value is not None and value != ""
63
+ }
64
+ if query:
65
+ url = f"{url}?{urlencode(query)}"
66
+ return url
67
+
68
+
54
69
  def get_object_from_json(text: str):
55
70
  if isinstance(text, list):
56
71
  obj = [json.loads(x, object_hook=lambda d: SimpleNamespace(**d)) for x in text]
@@ -59,8 +74,27 @@ def get_object_from_json(text: str):
59
74
  return obj
60
75
 
61
76
 
62
- def get_json(url: str, raise_for_status=False):
77
+ def get_json(url: str, raise_for_status: bool = False) -> Any:
78
+ """Fetch JSON and propagate connector errors."""
63
79
  c = get_api_client()
64
80
  c.raise_for_status = raise_for_status
65
81
 
66
82
  return c.get_json(url=url)
83
+
84
+
85
+ def get_json_dict(
86
+ url: str, raise_for_status: bool = False
87
+ ) -> Optional[Dict[str, Any]]:
88
+ """Fetch JSON and return an object, or None for another response shape."""
89
+ data = get_json(url, raise_for_status=raise_for_status)
90
+ if data is None:
91
+ return None
92
+ return data if isinstance(data, dict) else None
93
+
94
+
95
+ def get_json_list(url: str, raise_for_status: bool = False) -> List[Any]:
96
+ """Fetch JSON and return an array, or an empty list for another shape."""
97
+ data = get_json(url, raise_for_status=raise_for_status)
98
+ if data is None:
99
+ return []
100
+ return data if isinstance(data, list) else []
File without changes
File without changes