port-ocean 0.4.9__py3-none-any.whl → 0.4.10__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 port-ocean might be problematic. Click here for more details.

@@ -4,7 +4,6 @@ define run_checks
4
4
  exit_code=0; \
5
5
  cd $1; \
6
6
  poetry check || exit_code=$$?;\
7
- poetry lock --check || exit_code=$$?;\
8
7
  mypy . || exit_code=$$?; \
9
8
  ruff . || exit_code=$$?; \
10
9
  black --check . || exit_code=$$?; \
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  from urllib.parse import quote_plus
2
3
 
3
4
  import httpx
@@ -5,7 +6,10 @@ from loguru import logger
5
6
 
6
7
  from port_ocean.clients.port.authentication import PortAuthentication
7
8
  from port_ocean.clients.port.types import RequestOptions, UserAgentType
8
- from port_ocean.clients.port.utils import handle_status_code
9
+ from port_ocean.clients.port.utils import (
10
+ handle_status_code,
11
+ PORT_HTTP_MAX_CONNECTIONS_LIMIT,
12
+ )
9
13
  from port_ocean.core.models import Entity
10
14
 
11
15
 
@@ -13,6 +17,10 @@ class EntityClientMixin:
13
17
  def __init__(self, auth: PortAuthentication, client: httpx.AsyncClient):
14
18
  self.auth = auth
15
19
  self.client = client
20
+ # Semaphore is used to limit the number of concurrent requests to port, to avoid overloading it.
21
+ # The number of concurrent requests is set to 90% of the max connections limit, to leave some room for other
22
+ # requests that are not related to entities.
23
+ self.semaphore = asyncio.Semaphore(round(0.9 * PORT_HTTP_MAX_CONNECTIONS_LIMIT))
16
24
 
17
25
  async def upsert_entity(
18
26
  self,
@@ -22,24 +30,24 @@ class EntityClientMixin:
22
30
  should_raise: bool = True,
23
31
  ) -> None:
24
32
  validation_only = request_options["validation_only"]
25
- logger.info(
26
- f"{'Validating' if validation_only else 'Upserting'} entity: {entity.identifier} of blueprint: {entity.blueprint}"
27
- )
28
- headers = await self.auth.headers(user_agent_type)
29
-
30
- response = await self.client.post(
31
- f"{self.auth.api_url}/blueprints/{entity.blueprint}/entities",
32
- json=entity.dict(exclude_unset=True, by_alias=True),
33
- headers=headers,
34
- params={
35
- "upsert": "true",
36
- "merge": str(request_options["merge"]).lower(),
37
- "create_missing_related_entities": str(
38
- request_options["create_missing_related_entities"]
39
- ).lower(),
40
- "validation_only": str(validation_only).lower(),
41
- },
42
- )
33
+ async with self.semaphore:
34
+ logger.info(
35
+ f"{'Validating' if validation_only else 'Upserting'} entity: {entity.identifier} of blueprint: {entity.blueprint}"
36
+ )
37
+ headers = await self.auth.headers(user_agent_type)
38
+ response = await self.client.post(
39
+ f"{self.auth.api_url}/blueprints/{entity.blueprint}/entities",
40
+ json=entity.dict(exclude_unset=True, by_alias=True),
41
+ headers=headers,
42
+ params={
43
+ "upsert": "true",
44
+ "merge": str(request_options["merge"]).lower(),
45
+ "create_missing_related_entities": str(
46
+ request_options["create_missing_related_entities"]
47
+ ).lower(),
48
+ "validation_only": str(validation_only).lower(),
49
+ },
50
+ )
43
51
 
44
52
  if response.is_error:
45
53
  logger.error(
@@ -49,34 +57,81 @@ class EntityClientMixin:
49
57
  )
50
58
  handle_status_code(response, should_raise)
51
59
 
52
- async def delete_entity(
60
+ async def batch_upsert_entities(
53
61
  self,
54
- entity: Entity,
62
+ entities: list[Entity],
55
63
  request_options: RequestOptions,
56
64
  user_agent_type: UserAgentType | None = None,
57
65
  should_raise: bool = True,
58
66
  ) -> None:
59
- logger.info(
60
- f"Delete entity: {entity.identifier} of blueprint: {entity.blueprint}"
61
- )
62
- response = await self.client.delete(
63
- f"{self.auth.api_url}/blueprints/{entity.blueprint}/entities/{quote_plus(entity.identifier)}",
64
- headers=await self.auth.headers(user_agent_type),
65
- params={
66
- "delete_dependents": str(
67
- request_options["delete_dependent_entities"]
68
- ).lower()
69
- },
67
+ await asyncio.gather(
68
+ *(
69
+ self.upsert_entity(
70
+ entity,
71
+ request_options,
72
+ user_agent_type,
73
+ should_raise=should_raise,
74
+ )
75
+ for entity in entities
76
+ ),
77
+ return_exceptions=True,
70
78
  )
71
79
 
72
- if response.is_error:
73
- logger.error(
74
- f"Error deleting "
75
- f"entity: {entity.identifier} of "
76
- f"blueprint: {entity.blueprint}"
80
+ async def delete_entity(
81
+ self,
82
+ entity: Entity,
83
+ request_options: RequestOptions,
84
+ user_agent_type: UserAgentType | None = None,
85
+ should_raise: bool = True,
86
+ ) -> None:
87
+ async with self.semaphore:
88
+ logger.info(
89
+ f"Delete entity: {entity.identifier} of blueprint: {entity.blueprint}"
90
+ )
91
+ response = await self.client.delete(
92
+ f"{self.auth.api_url}/blueprints/{entity.blueprint}/entities/{quote_plus(entity.identifier)}",
93
+ headers=await self.auth.headers(user_agent_type),
94
+ params={
95
+ "delete_dependents": str(
96
+ request_options["delete_dependent_entities"]
97
+ ).lower()
98
+ },
77
99
  )
78
100
 
79
- handle_status_code(response, should_raise)
101
+ if response.is_error:
102
+ if response.status_code == 404:
103
+ logger.info(
104
+ f"Weren't able to delete entity: {entity.identifier} of blueprint: {entity.blueprint},"
105
+ f" as it was already deleted from port"
106
+ )
107
+ return
108
+ logger.error(
109
+ f"Error deleting "
110
+ f"entity: {entity.identifier} of "
111
+ f"blueprint: {entity.blueprint}"
112
+ )
113
+
114
+ handle_status_code(response, should_raise)
115
+
116
+ async def batch_delete_entities(
117
+ self,
118
+ entities: list[Entity],
119
+ request_options: RequestOptions,
120
+ user_agent_type: UserAgentType | None = None,
121
+ should_raise: bool = True,
122
+ ) -> None:
123
+ await asyncio.gather(
124
+ *(
125
+ self.delete_entity(
126
+ entity,
127
+ request_options,
128
+ user_agent_type,
129
+ should_raise=should_raise,
130
+ )
131
+ for entity in entities
132
+ ),
133
+ return_exceptions=True,
134
+ )
80
135
 
81
136
  async def validate_entity_exist(self, identifier: str, blueprint: str) -> None:
82
137
  logger.info(f"Validating entity {identifier} of blueprint {blueprint} exists")
@@ -11,8 +11,8 @@ if TYPE_CHECKING:
11
11
 
12
12
 
13
13
  class TokenRetryTransport(RetryTransport):
14
- def __init__(self, port_client: "PortClient", *args: Any, **kwargs: Any) -> None:
15
- super().__init__(*args, **kwargs)
14
+ def __init__(self, port_client: "PortClient", **kwargs: Any) -> None:
15
+ super().__init__(**kwargs)
16
16
  self.port_client = port_client
17
17
 
18
18
  def _is_retryable_method(self, request: httpx.Request) -> bool:
@@ -5,13 +5,11 @@ from loguru import logger
5
5
  from werkzeug.local import LocalStack, LocalProxy
6
6
 
7
7
  from port_ocean.clients.port.retry_transport import TokenRetryTransport
8
+ from port_ocean.helpers.async_client import OceanAsyncClient
8
9
 
9
10
  if TYPE_CHECKING:
10
11
  from port_ocean.clients.port.client import PortClient
11
12
 
12
- _http_client: LocalStack[httpx.AsyncClient] = LocalStack()
13
-
14
-
15
13
  # In case the framework sends more requests to port in parallel then allowed by the limits, a PoolTimeout exception will
16
14
  # be raised.
17
15
  # Raising defaults for the timeout, in addition to the limits, will allow request to wait for a connection for a longer
@@ -19,23 +17,25 @@ _http_client: LocalStack[httpx.AsyncClient] = LocalStack()
19
17
  # We don't want to set the max_connections too highly, as it will cause the application to run out of memory.
20
18
  # We also don't want to set the max_keepalive_connections too highly, as it will cause the application to run out of
21
19
  # available connections.
22
- PORT_HTTP_CLIENT_TIMEOUT = httpx.Timeout(10.0)
23
- PORT_HTTP_CLIENT_CONNECTIONS_LIMIT = httpx.Limits(
24
- max_connections=200, max_keepalive_connections=50
25
- )
20
+ PORT_HTTP_MAX_CONNECTIONS_LIMIT = 200
21
+ PORT_HTTP_MAX_KEEP_ALIVE_CONNECTIONS = 50
22
+ PORT_HTTP_TIMEOUT = 10.0
23
+
24
+
25
+ _http_client: LocalStack[httpx.AsyncClient] = LocalStack()
26
26
 
27
27
 
28
28
  def _get_http_client_context(port_client: "PortClient") -> httpx.AsyncClient:
29
29
  client = _http_client.top
30
30
  if client is None:
31
- client = httpx.AsyncClient(
32
- transport=TokenRetryTransport(
33
- port_client,
34
- httpx.AsyncHTTPTransport(),
35
- logger=logger,
31
+ client = OceanAsyncClient(
32
+ TokenRetryTransport,
33
+ transport_kwargs={"port_client": port_client},
34
+ timeout=httpx.Timeout(PORT_HTTP_TIMEOUT),
35
+ limits=httpx.Limits(
36
+ max_connections=PORT_HTTP_MAX_CONNECTIONS_LIMIT,
37
+ max_keepalive_connections=PORT_HTTP_MAX_KEEP_ALIVE_CONNECTIONS,
36
38
  ),
37
- timeout=PORT_HTTP_CLIENT_TIMEOUT,
38
- limits=PORT_HTTP_CLIENT_CONNECTIONS_LIMIT,
39
39
  )
40
40
  _http_client.push(client)
41
41
 
@@ -158,17 +158,11 @@ class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
158
158
  ) -> None:
159
159
  logger.info(f"Upserting {len(entities)} entities")
160
160
  if event.port_app_config.create_missing_related_entities:
161
- await asyncio.gather(
162
- *(
163
- self.context.port_client.upsert_entity(
164
- entity,
165
- event.port_app_config.get_port_request_options(),
166
- user_agent_type,
167
- should_raise=False,
168
- )
169
- for entity in entities
170
- ),
171
- return_exceptions=True,
161
+ await self.context.port_client.batch_upsert_entities(
162
+ entities,
163
+ event.port_app_config.get_port_request_options(),
164
+ user_agent_type,
165
+ should_raise=False,
172
166
  )
173
167
  else:
174
168
  ordered_created_entities = reversed(
@@ -188,16 +182,11 @@ class HttpEntitiesStateApplier(BaseEntitiesStateApplier):
188
182
  ) -> None:
189
183
  logger.info(f"Deleting {len(entities)} entities")
190
184
  if event.port_app_config.delete_dependent_entities:
191
- await asyncio.gather(
192
- *(
193
- self.context.port_client.delete_entity(
194
- entity,
195
- event.port_app_config.get_port_request_options(),
196
- user_agent_type,
197
- should_raise=False,
198
- )
199
- for entity in entities
200
- )
185
+ await self.context.port_client.batch_delete_entities(
186
+ entities,
187
+ event.port_app_config.get_port_request_options(),
188
+ user_agent_type,
189
+ should_raise=False,
201
190
  )
202
191
  else:
203
192
  ordered_deleted_entities = order_by_entities_dependencies(entities)
@@ -0,0 +1,53 @@
1
+ from typing import Any, Callable, Type
2
+
3
+ import httpx
4
+ from loguru import logger
5
+
6
+ from port_ocean.helpers.retry import RetryTransport
7
+
8
+
9
+ class OceanAsyncClient(httpx.AsyncClient):
10
+ """
11
+ This class is a wrapper around httpx.AsyncClient that uses a custom transport class.
12
+ This is done to allow passing our custom transport class to the AsyncClient constructor while still allowing
13
+ all the default AsyncClient behavior that is changed when passing a custom transport instance.
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ transport_class: Type[RetryTransport] = RetryTransport,
19
+ transport_kwargs: dict[str, Any] | None = None,
20
+ **kwargs: Any,
21
+ ):
22
+ self._transport_kwargs = transport_kwargs
23
+ self._transport_class = transport_class
24
+ super().__init__(**kwargs)
25
+
26
+ def _init_transport( # type: ignore[override]
27
+ self,
28
+ transport: httpx.AsyncBaseTransport | None = None,
29
+ app: Callable[..., Any] | None = None,
30
+ **kwargs: Any,
31
+ ) -> httpx.AsyncBaseTransport:
32
+ if transport is not None or app is not None:
33
+ return super()._init_transport(transport=transport, app=app, **kwargs)
34
+
35
+ return self._transport_class(
36
+ wrapped_transport=httpx.AsyncHTTPTransport(
37
+ **kwargs,
38
+ ),
39
+ logger=logger,
40
+ **(self._transport_kwargs or {}),
41
+ )
42
+
43
+ def _init_proxy_transport( # type: ignore[override]
44
+ self, proxy: httpx.Proxy, **kwargs: Any
45
+ ) -> httpx.AsyncBaseTransport:
46
+ return self._transport_class(
47
+ wrapped_transport=httpx.AsyncHTTPTransport(
48
+ proxy=proxy,
49
+ **kwargs,
50
+ ),
51
+ logger=logger,
52
+ **(self._transport_kwargs or {}),
53
+ )
port_ocean/utils.py CHANGED
@@ -3,6 +3,7 @@ import inspect
3
3
  from asyncio import ensure_future
4
4
  from functools import wraps
5
5
  from importlib.util import module_from_spec, spec_from_file_location
6
+ from pathlib import Path
6
7
  from time import time
7
8
  from traceback import format_exception
8
9
  from types import ModuleType
@@ -13,10 +14,10 @@ import httpx
13
14
  import tomli
14
15
  import yaml
15
16
  from loguru import logger
16
- from pathlib import Path
17
17
  from starlette.concurrency import run_in_threadpool
18
18
  from werkzeug.local import LocalStack, LocalProxy
19
19
 
20
+ from port_ocean.helpers.async_client import OceanAsyncClient
20
21
  from port_ocean.helpers.retry import RetryTransport
21
22
 
22
23
  _http_client: LocalStack[httpx.AsyncClient] = LocalStack()
@@ -25,12 +26,7 @@ _http_client: LocalStack[httpx.AsyncClient] = LocalStack()
25
26
  def _get_http_client_context() -> httpx.AsyncClient:
26
27
  client = _http_client.top
27
28
  if client is None:
28
- client = httpx.AsyncClient(
29
- transport=RetryTransport(
30
- httpx.AsyncHTTPTransport(),
31
- logger=logger,
32
- )
33
- )
29
+ client = OceanAsyncClient(RetryTransport)
34
30
  _http_client.push(client)
35
31
 
36
32
  return client
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: port-ocean
3
- Version: 0.4.9
3
+ Version: 0.4.10
4
4
  Summary: Port Ocean is a CLI tool for managing your Port projects.
5
5
  Home-page: https://app.getport.io
6
6
  Keywords: ocean,port-ocean,port
@@ -23,7 +23,7 @@ port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/.g
23
23
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/spec.yaml,sha256=Yq06gYoC6jFWES5mxlvJGFTGXbfD7E9R8j_PDbVIM3M,497
24
24
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CHANGELOG.md,sha256=nzAmB0Bjnd2eZo79OjrlyVOdpTBHTmTxvO7c2C8Q-VQ,292
25
25
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Dockerfile,sha256=Hh1dBnL959V2n28pmqFpXSrNvSMQjX6fDCUos8ITiu0,326
26
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Makefile,sha256=uTm8iPDPwf7bmhNkIi8cyHxoTnlBREDfiXNqOEPjgoo,1815
26
+ port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Makefile,sha256=HdDAoHiL0unYoMubrfngUA9GrweJny8jdTDqYFcNnTY,1773
27
27
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/README.md,sha256=5VZmgDRW9gO4d8UuzkujslOIDfIDBiAGL2Hd74HK770,468
28
28
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/changelog/.gitignore,sha256=kCpRPdl3S_jqYYZaOrc0-xa6-l3KqVjNRXc6jCkd_-Q,12
29
29
  port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/config.yaml,sha256=zKaBQNCbWEE3MFxDHaMn9NIeBGVPiZUV6cBjH35f2kw,906
@@ -39,12 +39,12 @@ port_ocean/clients/port/authentication.py,sha256=DXqZQaYUb2mhWRZXoTivteEyQPTsHtJ
39
39
  port_ocean/clients/port/client.py,sha256=3GYCM0ZkX3pB6sNoOb-7_6dm0Jr5_vqhflD9iltf_As,2640
40
40
  port_ocean/clients/port/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
41
  port_ocean/clients/port/mixins/blueprints.py,sha256=vp7kVzr704fCcp6Cvuwv4LR1GeM_Jh6FpaycLe9mv20,3794
42
- port_ocean/clients/port/mixins/entities.py,sha256=VA-pVLYBPP933EivK-YhOmCMTG0UhEP85SZta4ifWjA,5810
42
+ port_ocean/clients/port/mixins/entities.py,sha256=j1E3I_opymxUisP1FMpj-sbOcMV0Mhc7_YnfyGpcYzU,7908
43
43
  port_ocean/clients/port/mixins/integrations.py,sha256=FuDoQfQnfJvhK-nKpftQ8k7VsB9odNRlkMkGCmxwaWY,4280
44
44
  port_ocean/clients/port/mixins/migrations.py,sha256=M93i_aryfDazRHjkpTS3-sV3UshLCxmsv6yAIOziFl8,1463
45
- port_ocean/clients/port/retry_transport.py,sha256=6p64Ek8vpe0hHVcrNIXij8KQ9K2zwPuMUnJp2R8ZYUI,2075
45
+ port_ocean/clients/port/retry_transport.py,sha256=J25q5QpSYSVsVa24Sr1IvXu-KpYL88Pd9xe3xvYWYsg,2056
46
46
  port_ocean/clients/port/types.py,sha256=nvlgiAq4WH5_F7wQbz_GAWl-faob84LVgIjZ2Ww5mTk,451
47
- port_ocean/clients/port/utils.py,sha256=x20rtGgnYodklU8AcndF93fI4R58ggExxc9gvpiVeq0,2273
47
+ port_ocean/clients/port/utils.py,sha256=G4xvkjZYCPWaxh96ylVmZzDkfULkcw9K9ebxUQhgLMA,2360
48
48
  port_ocean/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
49
  port_ocean/config/base.py,sha256=e1oW66ynjo7Sp2PcLBp9aCzj59JpTaiKUXVJK8VB8TA,5540
50
50
  port_ocean/config/dynamic.py,sha256=CIRDnqFUPSnNMLZ-emRCMVAjEQNBlIujhZ7OGwi_aKs,1816
@@ -74,7 +74,7 @@ port_ocean/core/handlers/base.py,sha256=cTarblazu8yh8xz2FpB-dzDKuXxtoi143XJgPbV_
74
74
  port_ocean/core/handlers/entities_state_applier/__init__.py,sha256=kgLZDCeCEzi4r-0nzW9k78haOZNf6PX7mJOUr34A4c8,173
75
75
  port_ocean/core/handlers/entities_state_applier/base.py,sha256=FMsrBOVgaO4o7B1klLDY8fobTUDvyrerCKCICyYtkXs,2193
76
76
  port_ocean/core/handlers/entities_state_applier/port/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
77
- port_ocean/core/handlers/entities_state_applier/port/applier.py,sha256=SFdj1elXDMnoxqfGQDu5osiTX40k6vxhcBF9-ljklow,8197
77
+ port_ocean/core/handlers/entities_state_applier/port/applier.py,sha256=PyBtze8Xvhu2994B8YAxoB0ci-FPGAuYblvw2Wnxrok,7836
78
78
  port_ocean/core/handlers/entities_state_applier/port/get_related_entities.py,sha256=1zncwCbE-Gej0xaWKlzZgoXxOBe9bgs_YxlZ8QW3NdI,1751
79
79
  port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py,sha256=82BvU8t5w9uhsxX8hbnwuRPuWhW3cMeuT_5sVIkip1I,1550
80
80
  port_ocean/core/handlers/entities_state_applier/port/validate_entity_relations.py,sha256=nKuQ-RlalGG07olxm6l5NHeOuQT9dEZLoMpD-AN5nq0,1392
@@ -104,16 +104,17 @@ port_ocean/exceptions/context.py,sha256=qC1OLppiv6XZuXSHRO2P_VPFSzrc_uUQNvfSYLeA
104
104
  port_ocean/exceptions/core.py,sha256=ygxtPQ9IG8NzIrzZok5OqkefVrqcC4bjZ-2Vf9IPZuA,790
105
105
  port_ocean/exceptions/port_defaults.py,sha256=R3ufJcfllb7NZSwHOpBs8kbjsIZVQLM6vKO6dz4w-EE,407
106
106
  port_ocean/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
107
+ port_ocean/helpers/async_client.py,sha256=SRlP6o7_FCSY3UHnRlZdezppePVxxOzZ0z861vE3K40,1783
107
108
  port_ocean/helpers/retry.py,sha256=xrkXeVBHk1yIM6yVse5DpDZJMUj310ZpXqHFZFzCFRM,13171
108
109
  port_ocean/logger_setup.py,sha256=dOA58ZBAfe9dcAoBybXFWgDfsJG-uIX1ABI0r2CyUG8,1160
109
110
  port_ocean/middlewares.py,sha256=8rGu9XSKvbNCQGzWvfaijDrp-0ATJrWAQfBji2CnSck,2475
110
111
  port_ocean/ocean.py,sha256=9dFBf46N6fht6ibQUjnDDizY-p0qa0Uw-415JTDQmus,3626
111
112
  port_ocean/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
112
113
  port_ocean/run.py,sha256=xvYZlfi-J-IcqsAg8tNVnvl1mEUr6wPSya_-Bbf6jAU,1811
113
- port_ocean/utils.py,sha256=ZQCdOk1DzAA3hwUxFAzmbxQurnNYGpl1bDsPocJSkko,6129
114
+ port_ocean/utils.py,sha256=VcDhnqFwUYCPr9EZFtg5M2CIUC5Y4nN1Bv7gmCF4VYc,6067
114
115
  port_ocean/version.py,sha256=2ugCk8TXPsRIuFviZ8j3RPaszSw2HE-KuaW8vhgWJVM,172
115
- port_ocean-0.4.9.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
116
- port_ocean-0.4.9.dist-info/METADATA,sha256=r0Wm43wriHPQmVlQVnafGaTrP7p-8oMQynYV6WY8s7M,6490
117
- port_ocean-0.4.9.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
118
- port_ocean-0.4.9.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
119
- port_ocean-0.4.9.dist-info/RECORD,,
116
+ port_ocean-0.4.10.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
117
+ port_ocean-0.4.10.dist-info/METADATA,sha256=KZ3bZbJt_-KBrbM6NHjppTX0r2wpbhaMuXEJ8W-xFiI,6491
118
+ port_ocean-0.4.10.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
119
+ port_ocean-0.4.10.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
120
+ port_ocean-0.4.10.dist-info/RECORD,,