pyetp 0.0.45__tar.gz → 0.0.46__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.
Files changed (32) hide show
  1. {pyetp-0.0.45/src/pyetp.egg-info → pyetp-0.0.46}/PKG-INFO +8 -3
  2. {pyetp-0.0.45 → pyetp-0.0.46}/README.md +6 -0
  3. {pyetp-0.0.45 → pyetp-0.0.46}/pyproject.toml +1 -2
  4. {pyetp-0.0.45 → pyetp-0.0.46}/src/pyetp/__init__.py +1 -2
  5. {pyetp-0.0.45 → pyetp-0.0.46}/src/pyetp/_version.py +3 -3
  6. {pyetp-0.0.45 → pyetp-0.0.46}/src/pyetp/client.py +210 -136
  7. {pyetp-0.0.45 → pyetp-0.0.46}/src/pyetp/uri.py +3 -1
  8. {pyetp-0.0.45 → pyetp-0.0.46}/src/pyetp/utils_arrays.py +1 -7
  9. {pyetp-0.0.45 → pyetp-0.0.46}/src/pyetp/utils_xml.py +1 -6
  10. {pyetp-0.0.45 → pyetp-0.0.46/src/pyetp.egg-info}/PKG-INFO +8 -3
  11. {pyetp-0.0.45 → pyetp-0.0.46}/src/pyetp.egg-info/SOURCES.txt +1 -0
  12. {pyetp-0.0.45 → pyetp-0.0.46}/src/resqml_objects/epc_readers.py +3 -7
  13. pyetp-0.0.46/src/resqml_objects/parsers.py +25 -0
  14. pyetp-0.0.46/src/resqml_objects/serializers.py +33 -0
  15. pyetp-0.0.46/src/resqml_objects/surface_helpers.py +295 -0
  16. {pyetp-0.0.45 → pyetp-0.0.46}/src/resqml_objects/v201/generated.py +540 -19
  17. pyetp-0.0.45/src/resqml_objects/parsers.py +0 -12
  18. pyetp-0.0.45/src/resqml_objects/serializers.py +0 -10
  19. {pyetp-0.0.45 → pyetp-0.0.46}/CONTRIBUTING.md +0 -0
  20. {pyetp-0.0.45 → pyetp-0.0.46}/LICENSE.md +0 -0
  21. {pyetp-0.0.45 → pyetp-0.0.46}/MANIFEST.in +0 -0
  22. {pyetp-0.0.45 → pyetp-0.0.46}/SECURITY.md +0 -0
  23. {pyetp-0.0.45 → pyetp-0.0.46}/setup.cfg +0 -0
  24. {pyetp-0.0.45 → pyetp-0.0.46}/src/pyetp/config.py +0 -0
  25. {pyetp-0.0.45 → pyetp-0.0.46}/src/pyetp/resqml_objects.py +0 -0
  26. {pyetp-0.0.45 → pyetp-0.0.46}/src/pyetp/types.py +0 -0
  27. {pyetp-0.0.45 → pyetp-0.0.46}/src/pyetp.egg-info/dependency_links.txt +0 -0
  28. {pyetp-0.0.45 → pyetp-0.0.46}/src/pyetp.egg-info/requires.txt +0 -0
  29. {pyetp-0.0.45 → pyetp-0.0.46}/src/pyetp.egg-info/top_level.txt +0 -0
  30. {pyetp-0.0.45 → pyetp-0.0.46}/src/resqml_objects/__init__.py +0 -0
  31. {pyetp-0.0.45 → pyetp-0.0.46}/src/resqml_objects/v201/__init__.py +0 -0
  32. {pyetp-0.0.45 → pyetp-0.0.46}/src/resqml_objects/v201/utils.py +0 -0
@@ -1,16 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyetp
3
- Version: 0.0.45
3
+ Version: 0.0.46
4
4
  Summary: Interface with OSDU RDDMS using ETP protocol
5
5
  Author-email: Adam Cheng <52572642+adamchengtkc@users.noreply.github.com>
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: homepage, https://github.com/equinor/pyetp
8
8
  Classifier: Development Status :: 3 - Alpha
9
- Classifier: Programming Language :: Python :: 3.10
10
9
  Classifier: Programming Language :: Python :: 3.11
11
10
  Classifier: Programming Language :: Python :: 3.12
12
11
  Classifier: Programming Language :: Python :: 3.13
13
- Requires-Python: >=3.10
12
+ Requires-Python: >=3.11
14
13
  Description-Content-Type: text/markdown
15
14
  License-File: LICENSE.md
16
15
  Requires-Dist: numpy>=1.26.0
@@ -29,6 +28,12 @@ Dynamic: license-file
29
28
  [![PyPI version](https://badge.fury.io/py/pyetp.svg)](https://badge.fury.io/py/pyetp)
30
29
  ![License](https://img.shields.io/github/license/equinor/pyetp)
31
30
 
31
+ Pyetp is a library implementing an ETP v1.2 client with utilities and support
32
+ for working with RESQML v2.0.1 models.
33
+
34
+ > The following Energistics (c) products were used in the creation of this work:
35
+ > Energistics Transfer Protocol (ETP) v1.2 and RESQML v2.0.1
36
+
32
37
  # Installing the library
33
38
  This package is published to PyPI, and can be installed via:
34
39
  ```bash
@@ -4,6 +4,12 @@
4
4
  [![PyPI version](https://badge.fury.io/py/pyetp.svg)](https://badge.fury.io/py/pyetp)
5
5
  ![License](https://img.shields.io/github/license/equinor/pyetp)
6
6
 
7
+ Pyetp is a library implementing an ETP v1.2 client with utilities and support
8
+ for working with RESQML v2.0.1 models.
9
+
10
+ > The following Energistics (c) products were used in the creation of this work:
11
+ > Energistics Transfer Protocol (ETP) v1.2 and RESQML v2.0.1
12
+
7
13
  # Installing the library
8
14
  This package is published to PyPI, and can be installed via:
9
15
  ```bash
@@ -9,12 +9,11 @@ description = "Interface with OSDU RDDMS using ETP protocol"
9
9
  authors = [
10
10
  {name = "Adam Cheng", email = "52572642+adamchengtkc@users.noreply.github.com"}
11
11
  ]
12
- requires-python = ">=3.10"
12
+ requires-python = ">=3.11"
13
13
  readme = "README.md"
14
14
  license = "Apache-2.0"
15
15
  classifiers = [
16
16
  "Development Status :: 3 - Alpha",
17
- "Programming Language :: Python :: 3.10",
18
17
  "Programming Language :: Python :: 3.11",
19
18
  "Programming Language :: Python :: 3.12",
20
19
  "Programming Language :: Python :: 3.13",
@@ -1,5 +1,5 @@
1
1
  from ._version import __version__
2
- from .client import ETPClient, ETPError, connect, etp_connect, etp_persistent_connect
2
+ from .client import ETPClient, ETPError, connect, etp_connect
3
3
  from .uri import DataObjectURI, DataspaceURI
4
4
 
5
5
  __all__ = [
@@ -8,7 +8,6 @@ __all__ = [
8
8
  "ETPError",
9
9
  "connect",
10
10
  "etp_connect",
11
- "etp_persistent_connect",
12
11
  "DataObjectURI",
13
12
  "DataspaceURI",
14
13
  ]
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.0.45'
32
- __version_tuple__ = version_tuple = (0, 0, 45)
31
+ __version__ = version = '0.0.46'
32
+ __version_tuple__ = version_tuple = (0, 0, 46)
33
33
 
34
- __commit_id__ = commit_id = 'gef277bc08'
34
+ __commit_id__ = commit_id = 'ge1c2dc80b'
@@ -2,7 +2,6 @@ import asyncio
2
2
  import contextlib
3
3
  import datetime
4
4
  import logging
5
- import sys
6
5
  import typing as T
7
6
  import uuid
8
7
  import warnings
@@ -154,40 +153,6 @@ from resqml_objects import parse_resqml_v201_object, serialize_resqml_v201_objec
154
153
 
155
154
  logger = logging.getLogger(__name__)
156
155
 
157
- try:
158
- # for py >3.11, we can raise grouped exceptions
159
- from builtins import ExceptionGroup # type: ignore
160
- except ImportError:
161
- # Python 3.10
162
- def ExceptionGroup(msg, errors):
163
- return errors[0]
164
-
165
-
166
- try:
167
- # Python >= 3.11
168
- from asyncio import timeout
169
- except ImportError:
170
- # Python 3.10
171
- from contextlib import asynccontextmanager
172
-
173
- import async_timeout
174
-
175
- @asynccontextmanager
176
- async def timeout(delay: T.Optional[float]) -> T.Any:
177
- try:
178
- async with async_timeout.timeout(delay):
179
- yield None
180
- except asyncio.CancelledError as e:
181
- raise asyncio.TimeoutError(f"Timeout ({delay}s)") from e
182
-
183
- TimeoutError = asyncio.TimeoutError
184
-
185
- try:
186
- # Python >= 3.11
187
- from typing import Self
188
- except ImportError:
189
- Self = "ETPClient"
190
-
191
156
 
192
157
  class ETPError(Exception):
193
158
  def __init__(self, message: str, code: int):
@@ -315,7 +280,7 @@ class ETPClient(ETPConnection):
315
280
  for ti in timeout_intervals(self.etp_timeout):
316
281
  try:
317
282
  # Wait for an event for `ti` seconds.
318
- async with timeout(ti):
283
+ async with asyncio.timeout(ti):
319
284
  await self._recv_events[correlation_id].wait()
320
285
  except TimeoutError:
321
286
  # Check if the receiver task is still running.
@@ -323,11 +288,18 @@ class ETPClient(ETPConnection):
323
288
  # Raise any errors by waiting for the task to finish.
324
289
  await self.__recvtask
325
290
 
326
- logger.error(
327
- "Receiver task terminated without errors. This should not happen"
291
+ # Check that the receiver task stopped due to a
292
+ # (successfully) closed websockets connection.
293
+ try:
294
+ await self.ws.recv()
295
+ except websockets.ConnectionClosedOK:
296
+ pass
297
+
298
+ # Terminate client with an error.
299
+ raise ReceiveWorkerExited(
300
+ "Receiver task terminated prematurely due to a closed "
301
+ "websockets connection"
328
302
  )
329
-
330
- raise ReceiveWorkerExited
331
303
  else:
332
304
  # Break out of for-loop, and start processing message.
333
305
  break
@@ -372,13 +344,15 @@ class ETPClient(ETPConnection):
372
344
  errors.extend(body.errors.values())
373
345
  return errors
374
346
 
375
- async def __aexit__(self, *exc_details) -> None:
376
- await self.close(reason="Client exiting")
377
-
378
- async def close(self, reason=""):
347
+ async def __aexit__(
348
+ self,
349
+ exc_type: T.Type[BaseException] | None,
350
+ exc_value: BaseException | None,
351
+ traceback: TracebackType | None,
352
+ ) -> None:
379
353
  close_session_sent = False
380
354
  try:
381
- await self._send(CloseSession(reason=reason))
355
+ await self._send(CloseSession(reason="Client exiting"))
382
356
  close_session_sent = True
383
357
  except websockets.ConnectionClosed:
384
358
  logger.error(
@@ -419,7 +393,7 @@ class ETPClient(ETPConnection):
419
393
  # In some cases the server does not drop the connection after we
420
394
  # have sent the `CloseSession`-message. We therefore add a timeout
421
395
  # to the reading of possibly lost messages.
422
- async with timeout(self.etp_timeout or 10):
396
+ async with asyncio.timeout(self.etp_timeout or 10):
423
397
  async for msg in self.ws:
424
398
  counter += 1
425
399
  except websockets.ConnectionClosed:
@@ -428,7 +402,7 @@ class ETPClient(ETPConnection):
428
402
  pass
429
403
  except TimeoutError:
430
404
  if close_session_sent:
431
- logger.error(
405
+ logger.warning(
432
406
  "Websockets connection was not closed within "
433
407
  f"{self.etp_timeout or 10} seconds after the "
434
408
  "`CloseSession`-message was sent"
@@ -442,17 +416,30 @@ class ETPClient(ETPConnection):
442
416
 
443
417
  logger.debug("Client closed")
444
418
 
419
+ async def close(self):
420
+ """Closing method that tears down the ETP-connection via the
421
+ `ETPClient.__aexit__`-method, and closes the websockets connection.
422
+ This method should _only_ be used if the user has set up a connection
423
+ via `etp_client = await connect(...)` or `etp_client = await
424
+ etp_connect(...)` and will handle the closing of the connection
425
+ manually.
426
+ """
427
+
428
+ await self.__aexit__(None, None, None)
429
+ # The websockets connection should be closed from the ETP-server once
430
+ # it has received a `CloseSession`-message. However, calling close on
431
+ # the websockets connection does not do anything if it is already
432
+ # closed.
433
+ await self.ws.close()
434
+
445
435
  async def __recv(self):
446
436
  logger.debug("Starting receiver loop")
447
437
 
448
- while True:
449
- # We use this way of receiving messages, instead of the `async
450
- # for`-pattern, in order to raise all
451
- # `websockets.exceptions.ConnectionClosed`-errors (including the
452
- # `websockets.exceptions.ConnectionClosedOK` error). In the `async
453
- # for`-case a closing code of `1000` (normal closing) just exits
454
- # the loop.
455
- msg_data = await self.ws.recv()
438
+ # Using `async for` makes the receiver task exit without errors on a
439
+ # `websockets.exceptions.ConnectionClosedOK`-exception. This ensures a
440
+ # smoother clean-up in case the main-task errors resulting in a closed
441
+ # websockets connection down the line.
442
+ async for msg_data in self.ws:
456
443
  msg = Message.decode_binary_message(
457
444
  T.cast(bytes, msg_data), ETPClient.generic_transition_table
458
445
  )
@@ -468,11 +455,9 @@ class ETPClient(ETPConnection):
468
455
  # set response on send event
469
456
  self._recv_events[msg.header.correlation_id].set()
470
457
 
471
- #
472
- # session related
473
- #
458
+ logger.info("Websockets connection closed and receiver task stopped")
474
459
 
475
- async def __aenter__(self) -> Self:
460
+ async def __aenter__(self) -> T.Self:
476
461
  return await self.request_session()
477
462
 
478
463
  async def request_session(self):
@@ -811,20 +796,19 @@ class ETPClient(ETPConnection):
811
796
  "obj must be Grid2DRepresentation"
812
797
  )
813
798
  sgeo = gri.grid2d_patch.geometry.points.supporting_geometry # type: ignore
814
- if sys.version_info[1] != 10:
815
- assert isinstance(
816
- gri.grid2d_patch.geometry.points, ro.Point3dZValueArray
817
- ), "Points must be Point3dZValueArray"
818
- assert isinstance(
819
- gri.grid2d_patch.geometry.points.zvalues, ro.DoubleHdf5Array
820
- ), "Values must be DoubleHdf5Array"
821
- assert isinstance(
822
- gri.grid2d_patch.geometry.points.supporting_geometry,
823
- ro.Point3dLatticeArray,
824
- ), "Points support_geo must be Point3dLatticeArray"
825
- assert isinstance(sgeo, ro.Point3dLatticeArray), (
826
- "supporting_geometry must be Point3dLatticeArray"
827
- )
799
+ assert isinstance(gri.grid2d_patch.geometry.points, ro.Point3dZValueArray), (
800
+ "Points must be Point3dZValueArray"
801
+ )
802
+ assert isinstance(
803
+ gri.grid2d_patch.geometry.points.zvalues, ro.DoubleHdf5Array
804
+ ), "Values must be DoubleHdf5Array"
805
+ assert isinstance(
806
+ gri.grid2d_patch.geometry.points.supporting_geometry,
807
+ ro.Point3dLatticeArray,
808
+ ), "Points support_geo must be Point3dLatticeArray"
809
+ assert isinstance(sgeo, ro.Point3dLatticeArray), (
810
+ "supporting_geometry must be Point3dLatticeArray"
811
+ )
828
812
  assert isinstance(
829
813
  gri.grid2d_patch.geometry.points.zvalues.values, ro.Hdf5Dataset
830
814
  ), "Values must be Hdf5Dataset"
@@ -1407,6 +1391,7 @@ class connect:
1407
1391
  # ... = await connect(...)
1408
1392
 
1409
1393
  def __await__(self):
1394
+ # The caller is response for calling `close()` on the client.
1410
1395
  return self.__aenter__().__await__()
1411
1396
 
1412
1397
  # async with connect(...) as ...:
@@ -1420,7 +1405,7 @@ class connect:
1420
1405
  if self.data_partition is not None:
1421
1406
  headers["data-partition-id"] = self.data_partition
1422
1407
 
1423
- self.ws = await websockets.connect(
1408
+ ws = await websockets.connect(
1424
1409
  self.server_url,
1425
1410
  subprotocols=[ETPClient.SUB_PROTOCOL], # type: ignore
1426
1411
  additional_headers=headers,
@@ -1430,7 +1415,7 @@ class connect:
1430
1415
  )
1431
1416
 
1432
1417
  self.client = ETPClient(
1433
- self.ws,
1418
+ ws=ws,
1434
1419
  etp_timeout=self.timeout,
1435
1420
  max_message_size=SETTINGS.MaxWebSocketMessagePayloadSize,
1436
1421
  application_name=SETTINGS.application_name,
@@ -1448,67 +1433,156 @@ class connect:
1448
1433
 
1449
1434
  # exit the async context manager
1450
1435
  async def __aexit__(self, exc_type, exc: Exception, tb: TracebackType):
1436
+ # The `ETPClient.close`-method also closes the websockets connection.
1451
1437
  await self.client.close()
1452
- await self.ws.close()
1453
1438
 
1454
1439
 
1455
- @contextlib.asynccontextmanager
1456
- async def etp_connect(
1457
- uri: str,
1458
- data_partition_id: str | None = None,
1459
- authorization: str | None = None,
1460
- etp_timeout: float = 10.0,
1461
- max_message_size: float = 2**20,
1462
- ) -> ETPClient:
1463
- additional_headers = {}
1464
-
1465
- if authorization is not None:
1466
- additional_headers["Authorization"] = authorization
1467
- if data_partition_id is not None:
1468
- additional_headers["data-partition-id"] = data_partition_id
1469
-
1470
- subprotocols = ["etp12.energistics.org"]
1471
-
1472
- async with (
1473
- websockets.connect(
1474
- uri=uri,
1475
- subprotocols=subprotocols,
1476
- max_size=max_message_size,
1477
- additional_headers=additional_headers,
1478
- ) as ws,
1479
- ETPClient(
1480
- ws=ws,
1481
- etp_timeout=etp_timeout,
1482
- max_message_size=max_message_size,
1483
- ) as etp_client,
1484
- ):
1485
- yield etp_client
1486
-
1487
-
1488
- async def etp_persistent_connect(
1489
- uri: str,
1490
- data_partition_id: str | None = None,
1491
- authorization: str | None = None,
1492
- etp_timeout: float = 10.0,
1493
- max_message_size: float = 2**20,
1494
- ) -> AsyncGenerator[ETPClient]:
1495
- additional_headers = {}
1496
-
1497
- if authorization is not None:
1498
- additional_headers["Authorization"] = authorization
1499
- if data_partition_id is not None:
1500
- additional_headers["data-partition-id"] = data_partition_id
1501
-
1502
- subprotocols = ["etp12.energistics.org"]
1503
- async for ws in websockets.connect(
1504
- uri=uri,
1505
- subprotocols=subprotocols,
1506
- max_size=max_message_size,
1507
- additional_headers=additional_headers,
1508
- ):
1509
- async with ETPClient(
1510
- ws=ws,
1511
- etp_timeout=etp_timeout,
1512
- max_message_size=max_message_size,
1513
- ) as etp_client:
1514
- yield etp_client
1440
+ class etp_connect:
1441
+ """
1442
+ Connect to an ETP server via websockets.
1443
+
1444
+ This class can act as:
1445
+
1446
+ 1. A context manager handling setup and tear-down of the connection.
1447
+ 2. An asynchronous iterator which can be used to persistently retry to
1448
+ connect if the websockets connection drops.
1449
+ 3. An awaitable connection that must be manually closed by the user.
1450
+
1451
+ See below for examples of all three cases.
1452
+
1453
+ Parameters
1454
+ ----------
1455
+ uri: str
1456
+ The uri to the ETP server. This should be the uri to a websockets
1457
+ endpoint.
1458
+ data_partition_id: str | None
1459
+ The data partition id used when connecting to the OSDU open-etp-server
1460
+ in multi-partition mode. Default is `None`.
1461
+ authorization: str | SecretStr | None
1462
+ Bearer token used for authenticating to the ETP server. This token
1463
+ should be on the form `"Bearer 1234..."`. Default is `None`.
1464
+ etp_timeout: float | None
1465
+ The timeout in seconds for when to stop waiting for a message from the
1466
+ ETP server. Setting it to `None` will persist the connection
1467
+ indefinetly. Default is `None`.
1468
+ max_message_size: float
1469
+ The maximum number of bytes for a single websockets message. Default is
1470
+ `2**20` corresponding to `1` MiB.
1471
+
1472
+
1473
+ Examples
1474
+ --------
1475
+ An example of connecting to the ETP server using :func:`etp_connect` as a
1476
+ context manager is:
1477
+
1478
+ async with etp_connect(...) as etp_client:
1479
+ ...
1480
+
1481
+ In this case the closing message and the websockets connection is closed
1482
+ once the program exits the context manager.
1483
+
1484
+
1485
+ To persist a connection if the websockets connection is dropped (for any
1486
+ reason), use :func:`etp_connect` as an asynchronous generator, viz.:
1487
+
1488
+ import websockets
1489
+
1490
+ async for etp_client in etp_connect(...):
1491
+ try:
1492
+ ...
1493
+ except websockets.ConnectionClosed:
1494
+ continue
1495
+
1496
+ # Include `break` to avoid re-running the whole block if the
1497
+ # iteration runs without any errors.
1498
+ break
1499
+
1500
+ Note that in this case the whole program under the `try`-block is re-run
1501
+ from the start if the iteration completes normally, or if the websockets
1502
+ connection is dropped. Therefore, make sure to include a `break` at the end
1503
+ of the `try`-block (as in the example above).
1504
+
1505
+
1506
+ The third option is to set up a connection via `await` and then manually
1507
+ close the connection once done:
1508
+
1509
+ etp_client = await etp_connect(...)
1510
+ ...
1511
+ await etp_client.close()
1512
+ """
1513
+
1514
+ def __init__(
1515
+ self,
1516
+ uri: str,
1517
+ data_partition_id: str | None = None,
1518
+ authorization: str | SecretStr | None = None,
1519
+ etp_timeout: float | None = None,
1520
+ max_message_size: float = 2**20,
1521
+ ) -> None:
1522
+ self.uri = uri
1523
+ self.data_partition_id = data_partition_id
1524
+ self.authorization = SecretStr(authorization)
1525
+ self.etp_timeout = etp_timeout
1526
+ self.max_message_size = max_message_size
1527
+ self.subprotocols = ["etp12.energistics.org"]
1528
+
1529
+ def __await__(self) -> ETPClient:
1530
+ # The caller is responsible for calling `close()` on the client.
1531
+ return self.__aenter__().__await__()
1532
+
1533
+ def get_additional_headers(self) -> dict[str, str]:
1534
+ additional_headers = {}
1535
+
1536
+ if self.authorization.get_secret_value() is not None:
1537
+ additional_headers["Authorization"] = self.authorization.get_secret_value()
1538
+
1539
+ if self.data_partition_id is not None:
1540
+ additional_headers["data-partition-id"] = self.data_partition_id
1541
+
1542
+ return additional_headers
1543
+
1544
+ async def __aenter__(self) -> ETPClient:
1545
+ self.stack = contextlib.AsyncExitStack()
1546
+ try:
1547
+ ws = await self.stack.enter_async_context(
1548
+ websockets.connect(
1549
+ uri=self.uri,
1550
+ subprotocols=self.subprotocols,
1551
+ max_size=self.max_message_size,
1552
+ additional_headers=self.get_additional_headers(),
1553
+ )
1554
+ )
1555
+ etp_client = await self.stack.enter_async_context(
1556
+ ETPClient(
1557
+ ws=ws,
1558
+ etp_timeout=self.etp_timeout,
1559
+ max_message_size=self.max_message_size,
1560
+ )
1561
+ )
1562
+ except BaseException:
1563
+ await self.stack.aclose()
1564
+ raise
1565
+
1566
+ return etp_client
1567
+
1568
+ async def __aexit__(
1569
+ self,
1570
+ exc_type: T.Type[BaseException] | None,
1571
+ exc_value: BaseException | None,
1572
+ traceback: TracebackType | None,
1573
+ ) -> None:
1574
+ return await self.stack.aclose()
1575
+
1576
+ async def __aiter__(self) -> AsyncGenerator[ETPClient]:
1577
+ async for ws in websockets.connect(
1578
+ uri=self.uri,
1579
+ subprotocols=self.subprotocols,
1580
+ max_size=self.max_message_size,
1581
+ additional_headers=self.get_additional_headers(),
1582
+ ):
1583
+ async with ETPClient(
1584
+ ws=ws,
1585
+ etp_timeout=self.etp_timeout,
1586
+ max_message_size=self.max_message_size,
1587
+ ) as etp_client:
1588
+ yield etp_client
@@ -83,7 +83,9 @@ class DataObjectURI(_DataObjectURI, _Mixin):
83
83
  obj.Meta, "target_namespace"
84
84
  )
85
85
  namespace = namespace.lower()
86
- # TODO: we can rather look at citation.format - which could be used for xmlformat ? - however to be backward capatiable we check namespaces instead
86
+ # TODO: we can rather look at citation.format - which could be used
87
+ # for xmlformat ? - however to be backward capatiable we check
88
+ # namespaces instead
87
89
  if namespace.endswith("resqmlv2"):
88
90
  domain = "resqml20"
89
91
  elif namespace.endswith("data/commonv2"):
@@ -1,4 +1,3 @@
1
- import sys
2
1
  import typing as T
3
2
 
4
3
  import numpy as np
@@ -50,12 +49,7 @@ _ANY_LOGICAL_ARRAY_TYPE_MAP: dict[npt.DTypeLike, AnyLogicalArrayType] = {
50
49
  }
51
50
 
52
51
  valid_logical_array_dtypes = list(_ANY_LOGICAL_ARRAY_TYPE_MAP)
53
- if (sys.version_info.major, sys.version_info.minor) == (3, 10):
54
- LogicalArrayDTypes: T.TypeAlias = T.Union[
55
- tuple(v.type for v in valid_logical_array_dtypes)
56
- ]
57
- else:
58
- LogicalArrayDTypes: T.TypeAlias = T.Union[tuple(valid_logical_array_dtypes)]
52
+ LogicalArrayDTypes: T.TypeAlias = T.Union[tuple(valid_logical_array_dtypes)]
59
53
 
60
54
  _INV_ANY_LOGICAL_ARRAY_TYPE_MAP: dict[AnyLogicalArrayType, npt.DTypeLike] = {
61
55
  v: k for k, v in _ANY_LOGICAL_ARRAY_TYPE_MAP.items()
@@ -118,13 +118,8 @@ def instantiate_resqml_grid(
118
118
  grid2d_patch=ro.Grid2dPatch(
119
119
  # TODO: Perhaps we can use this for tiling?
120
120
  patch_index=0,
121
- # NumPy-arrays are C-ordered, meaning that the last index is
122
- # the index that changes most rapidly. However, xtgeo uses nrow for
123
- # axis 1 in the array, and ncol for axis 0. This means that
124
- # surf.nrow is the fastest changing axis, and surf.ncol the slowest
125
- # changing axis (as surf.values.shape == (surf.ncol, surf.nrow))
126
- fastest_axis_count=ny,
127
121
  slowest_axis_count=nx,
122
+ fastest_axis_count=ny,
128
123
  geometry=ro.PointGeometry(
129
124
  local_crs=get_data_object_reference(crs),
130
125
  points=ro.Point3dZValueArray(
@@ -1,16 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyetp
3
- Version: 0.0.45
3
+ Version: 0.0.46
4
4
  Summary: Interface with OSDU RDDMS using ETP protocol
5
5
  Author-email: Adam Cheng <52572642+adamchengtkc@users.noreply.github.com>
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: homepage, https://github.com/equinor/pyetp
8
8
  Classifier: Development Status :: 3 - Alpha
9
- Classifier: Programming Language :: Python :: 3.10
10
9
  Classifier: Programming Language :: Python :: 3.11
11
10
  Classifier: Programming Language :: Python :: 3.12
12
11
  Classifier: Programming Language :: Python :: 3.13
13
- Requires-Python: >=3.10
12
+ Requires-Python: >=3.11
14
13
  Description-Content-Type: text/markdown
15
14
  License-File: LICENSE.md
16
15
  Requires-Dist: numpy>=1.26.0
@@ -29,6 +28,12 @@ Dynamic: license-file
29
28
  [![PyPI version](https://badge.fury.io/py/pyetp.svg)](https://badge.fury.io/py/pyetp)
30
29
  ![License](https://img.shields.io/github/license/equinor/pyetp)
31
30
 
31
+ Pyetp is a library implementing an ETP v1.2 client with utilities and support
32
+ for working with RESQML v2.0.1 models.
33
+
34
+ > The following Energistics (c) products were used in the creation of this work:
35
+ > Energistics Transfer Protocol (ETP) v1.2 and RESQML v2.0.1
36
+
32
37
  # Installing the library
33
38
  This package is published to PyPI, and can be installed via:
34
39
  ```bash
@@ -22,6 +22,7 @@ src/resqml_objects/__init__.py
22
22
  src/resqml_objects/epc_readers.py
23
23
  src/resqml_objects/parsers.py
24
24
  src/resqml_objects/serializers.py
25
+ src/resqml_objects/surface_helpers.py
25
26
  src/resqml_objects/v201/__init__.py
26
27
  src/resqml_objects/v201/generated.py
27
28
  src/resqml_objects/v201/utils.py
@@ -1,11 +1,11 @@
1
1
  import logging
2
2
  import pathlib
3
- import sys
4
3
  import zipfile
5
4
 
6
5
  import h5py
7
6
  import numpy as np
8
7
  import numpy.typing as npt
8
+ from xsdata.exceptions import ParserError
9
9
 
10
10
  import resqml_objects.v201 as ro_201
11
11
 
@@ -46,12 +46,6 @@ def get_resqml_v201_objects(
46
46
  get_arrays_and_paths_in_hdf_file
47
47
  """
48
48
 
49
- if sys.version_info.major == 3 and sys.version_info.minor == 10:
50
- raise NotImplementedError(
51
- "The epc-reader 'get_resqml_v201_objects'-function does not work for "
52
- "Python 3.10. Consider switching to Python 3.11 or up."
53
- )
54
-
55
49
  robjs = []
56
50
  fail = {}
57
51
 
@@ -64,6 +58,8 @@ def get_resqml_v201_objects(
64
58
  robjs.append(parse_resqml_v201_object(c))
65
59
  except AttributeError as e:
66
60
  fail[zi.filename] = e
61
+ except ParserError as e:
62
+ fail[zi.filename] = e
67
63
 
68
64
  if log_failed_objects:
69
65
  logger.info(f"Failed to parse: {fail}")