mainsequence 9.0.6.dev40__py3-none-any.whl → 9.0.6.dev41__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.
@@ -0,0 +1,154 @@
1
+ """One bounded retry policy for SDK HTTP sessions."""
2
+
3
+ import math
4
+ import time
5
+ from contextlib import contextmanager
6
+ from contextvars import ContextVar
7
+
8
+ import requests
9
+ from requests.adapters import HTTPAdapter
10
+ from urllib3.exceptions import InvalidHeader
11
+ from urllib3.util import Retry, Timeout
12
+
13
+ DEFAULT_ALLOWED_METHODS = frozenset({"HEAD", "GET", "OPTIONS"})
14
+ DEFAULT_STATUS_FORCELIST = (429, 500, 502, 503, 504)
15
+ DEFAULT_TIMEOUT = (5.0, 120.0)
16
+
17
+
18
+ class RequestBudget:
19
+ def __init__(self, timeout):
20
+ if timeout is None:
21
+ timeout = DEFAULT_TIMEOUT
22
+ if isinstance(timeout, Timeout):
23
+ connect, read = timeout.connect_timeout, timeout.read_timeout
24
+ total = timeout.total
25
+ elif isinstance(timeout, tuple):
26
+ connect, read = timeout
27
+ total = None
28
+ else:
29
+ connect = read = total = timeout
30
+ self.connect = self._seconds(connect)
31
+ self.read = self._seconds(read)
32
+ self.deadline = time.monotonic() + self._seconds(
33
+ self.connect + self.read if total is None else total
34
+ )
35
+
36
+ @staticmethod
37
+ def _seconds(value):
38
+ seconds = float(value)
39
+ if not math.isfinite(seconds) or seconds <= 0:
40
+ raise ValueError("Request timeouts must be positive, finite seconds.")
41
+ return seconds
42
+
43
+ @property
44
+ def remaining(self):
45
+ return self.deadline - time.monotonic()
46
+
47
+ def timeout(self):
48
+ remaining = self.remaining
49
+ if remaining <= 0:
50
+ raise requests.Timeout("The SDK request's total timeout budget is exhausted.")
51
+ return Timeout(
52
+ total=remaining,
53
+ connect=min(self.connect, remaining),
54
+ read=min(self.read, remaining),
55
+ )
56
+
57
+
58
+ _ACTIVE_BUDGET: ContextVar[RequestBudget | None] = ContextVar("sdk_request_budget", default=None)
59
+
60
+
61
+ @contextmanager
62
+ def request_budget(timeout):
63
+ budget = RequestBudget(timeout)
64
+ parent = _ACTIVE_BUDGET.get()
65
+ if parent is not None:
66
+ budget.deadline = min(budget.deadline, parent.deadline)
67
+ token = _ACTIVE_BUDGET.set(budget)
68
+ try:
69
+ yield budget
70
+ finally:
71
+ _ACTIVE_BUDGET.reset(token)
72
+
73
+
74
+ def remaining_timeout(timeout):
75
+ budget = _ACTIVE_BUDGET.get()
76
+ if budget is None:
77
+ return timeout
78
+ connect, read = timeout
79
+ remaining = budget.timeout().total
80
+ return Timeout(total=remaining, connect=min(connect, remaining), read=min(read, remaining))
81
+
82
+
83
+ class DeadlineHTTPAdapter(HTTPAdapter):
84
+ def __init__(self, *, retries, backoff_factor):
85
+ if not isinstance(retries, int) or retries < 0:
86
+ raise ValueError("retries must be a non-negative integer.")
87
+ if not math.isfinite(backoff_factor) or backoff_factor < 0:
88
+ raise ValueError("backoff_factor must be non-negative and finite.")
89
+ # urllib3 must never add a second retry loop or resend a write on connect failure.
90
+ super().__init__(
91
+ max_retries=Retry(
92
+ total=0,
93
+ connect=0,
94
+ read=False,
95
+ status=0,
96
+ other=0,
97
+ allowed_methods=DEFAULT_ALLOWED_METHODS,
98
+ )
99
+ )
100
+ self.retries = retries
101
+ self.backoff_factor = backoff_factor
102
+
103
+ def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None):
104
+ budget = RequestBudget(timeout)
105
+ parent = _ACTIVE_BUDGET.get()
106
+ if parent is not None:
107
+ budget.deadline = min(budget.deadline, parent.deadline)
108
+ retryable = request.method in DEFAULT_ALLOWED_METHODS
109
+ for attempt in range(self.retries + 1):
110
+ response = None
111
+ error = None
112
+ try:
113
+ response = super().send(
114
+ request,
115
+ stream=stream,
116
+ timeout=budget.timeout(),
117
+ verify=verify,
118
+ cert=cert,
119
+ proxies=proxies,
120
+ )
121
+ if not stream:
122
+ # Read before deciding to retry, so body failures share the same budget.
123
+ _ = response.content
124
+ if not retryable or response.status_code not in DEFAULT_STATUS_FORCELIST:
125
+ return response
126
+ except requests.exceptions.SSLError:
127
+ raise
128
+ except (
129
+ requests.ConnectionError,
130
+ requests.Timeout,
131
+ requests.exceptions.ChunkedEncodingError,
132
+ ) as exc:
133
+ error = exc
134
+
135
+ delay = min(self.backoff_factor * 2**attempt, Retry.DEFAULT_BACKOFF_MAX)
136
+ if error is None:
137
+ try:
138
+ retry_after = self.max_retries.get_retry_after(response)
139
+ except InvalidHeader:
140
+ retry_after = None
141
+ if retry_after is not None:
142
+ delay = max(delay, retry_after)
143
+
144
+ if not retryable or attempt == self.retries or delay >= budget.remaining:
145
+ if error is not None:
146
+ if response is not None:
147
+ response.close()
148
+ raise error
149
+ return response
150
+ if response is not None:
151
+ response.close()
152
+ time.sleep(delay)
153
+
154
+ raise AssertionError("Unreachable retry state")
@@ -9,7 +9,7 @@ import requests
9
9
  from mainsequence.defaults import resolve_backend_endpoint
10
10
 
11
11
  from .exceptions import raise_for_response
12
- from .utils import AuthLoaders, build_session
12
+ from .utils import AuthLoaders, build_session, make_request
13
13
  from .utils import loaders as _global_loaders
14
14
  from .utils import session as _global_session
15
15
 
@@ -35,6 +35,7 @@ class MainSequenceClient:
35
35
  - Uses ONE shared loaders + session (from utils) by default
36
36
  - Can be reconfigured once at startup
37
37
  """
38
+
38
39
  _default: MainSequenceClient | None = None
39
40
 
40
41
  def __init__(
@@ -72,15 +73,18 @@ class MainSequenceClient:
72
73
  timeout: tuple[float, float] | float | None = None,
73
74
  ) -> Any:
74
75
  url = self.config.api_root.rstrip("/") + "/" + path.lstrip("/")
75
- to = timeout or self.config.timeout
76
-
77
- r = self.session.request(method.upper(), url, params=params, json=json, files=files, timeout=to)
78
-
79
- # Refresh auth once if needed
80
- if r.status_code in (401, 403):
81
- self.loaders.refresh_headers()
82
- self.session.headers.update(self.loaders.auth_headers)
83
- r = self.session.request(method.upper(), url, params=params, json=json, files=files, timeout=to)
76
+ to = self.config.timeout if timeout is None else timeout
77
+ payload = {"params": params, "json": json}
78
+ if files is not None:
79
+ payload["files"] = files
80
+ r = make_request(
81
+ self.session,
82
+ method,
83
+ url,
84
+ self.loaders,
85
+ payload=payload,
86
+ time_out=to,
87
+ )
84
88
 
85
89
  raise_for_response(r, payload={"params": params, "json": json})
86
90
 
@@ -95,7 +99,9 @@ class MainSequenceClient:
95
99
  """
96
100
  if cls._default is None:
97
101
  # Reuse the existing global singletons from utils to avoid two sessions in prod.
98
- cls._default = cls(MainSequenceClientConfig(), loaders=_global_loaders, session=_global_session)
102
+ cls._default = cls(
103
+ MainSequenceClientConfig(), loaders=_global_loaders, session=_global_session
104
+ )
99
105
  return cls._default
100
106
 
101
107
  @classmethod
@@ -69,8 +69,8 @@ class InferenceClient:
69
69
  if self.organization_environment_uid is not None:
70
70
  query["organization_environment_uid"] = self.organization_environment_uid
71
71
  url = f"{utils.API_ENDPOINT.rstrip('/')}/{path}"
72
- # The shared make_request loop retries network failures. Inference uses
73
- # the same auth loader with a dedicated zero-retry transport instead.
72
+ # Inference opts out of safe-read retries as well: an uncertain outcome
73
+ # must preserve the caller's explicit idempotency key.
74
74
  with utils.build_session(loaders=utils.loaders, retries=0) as session:
75
75
  headers = dict(utils.loaders.refresh_headers(force=False, session=session))
76
76
  if key is not None:
@@ -17,26 +17,21 @@ from uuid import UUID, getnode
17
17
 
18
18
  import psutil
19
19
  import requests
20
- from requests.adapters import HTTPAdapter
21
20
  from requests.structures import CaseInsensitiveDict
22
- from urllib3.util.retry import Retry
23
21
 
24
22
  from mainsequence.defaults import resolve_backend_endpoint
25
23
  from mainsequence.logconf import logger
26
24
 
25
+ from ._transport import DEFAULT_ALLOWED_METHODS as DEFAULT_ALLOWED_METHODS
26
+ from ._transport import DEFAULT_STATUS_FORCELIST as DEFAULT_STATUS_FORCELIST
27
+ from ._transport import DEFAULT_TIMEOUT as DEFAULT_TIMEOUT
28
+ from ._transport import DeadlineHTTPAdapter, remaining_timeout, request_budget
29
+
27
30
  # ---- Backend defaults (single source of truth) ----
28
31
  MAINSEQUENCE_ENDPOINT = resolve_backend_endpoint()
29
32
  API_ENDPOINT = f"{MAINSEQUENCE_ENDPOINT}/api/v1"
30
33
  AUTH_ENDPOINT = MAINSEQUENCE_ENDPOINT.rstrip("/")
31
34
 
32
- DEFAULT_STATUS_FORCELIST = (429, 500, 502, 503, 504)
33
- DEFAULT_ALLOWED_METHODS = frozenset(["HEAD", "GET", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"])
34
-
35
- # requests supports either a float or (connect, read). Preserve previous ~120s read behavior,
36
- # but add a sane connect timeout.
37
- DEFAULT_TIMEOUT: tuple[float, float] = (5.0, 120.0)
38
-
39
-
40
35
  DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
41
36
 
42
37
 
@@ -383,7 +378,7 @@ class RuntimeCredentialAuthProvider(BaseAuthProvider):
383
378
  "credential_secret": credential_secret,
384
379
  },
385
380
  headers={"Content-Type": "application/json"},
386
- timeout=self.timeout,
381
+ timeout=remaining_timeout(self.timeout),
387
382
  allow_redirects=False,
388
383
  )
389
384
  if response.status_code < 200 or response.status_code >= 300:
@@ -515,7 +510,7 @@ class JWTAuthProvider(BaseAuthProvider):
515
510
  self.refresh_url,
516
511
  json={"refresh": self.refresh_token},
517
512
  headers={"Content-Type": "application/json"},
518
- timeout=self.timeout,
513
+ timeout=remaining_timeout(self.timeout),
519
514
  )
520
515
 
521
516
  if r.status_code in _REFUSED_RENEWAL_STATUSES:
@@ -564,7 +559,7 @@ class JWTAuthProvider(BaseAuthProvider):
564
559
  self.obtain_url,
565
560
  json={"username": username, "password": password},
566
561
  headers={"Content-Type": "application/json"},
567
- timeout=self.timeout,
562
+ timeout=remaining_timeout(self.timeout),
568
563
  )
569
564
  r.raise_for_status()
570
565
  data = r.json()
@@ -671,63 +666,46 @@ def make_request(
671
666
  ):
672
667
  from requests.models import Response
673
668
 
674
- TIMEOFF = 0.25
675
- TRIES = int(15 // TIMEOFF)
676
669
  timeout = DEFAULT_TIMEOUT if time_out is None else time_out
677
670
  payload = {} if payload is None else payload
678
-
679
- def get_req(session):
680
- if r_type == "GET":
681
- return session.get
682
- elif r_type == "POST":
683
- return session.post
684
- elif r_type == "PUT":
685
- return session.put
686
- elif r_type == "PATCH":
687
- return session.patch
688
- elif r_type == "DELETE":
689
- return session.delete
690
- else:
691
- raise NotImplementedError(f"Unsupported method: {r_type}")
671
+ r_type = r_type.upper()
672
+ if r_type not in DEFAULT_ALLOWED_METHODS | {"POST", "PUT", "PATCH", "DELETE"}:
673
+ raise NotImplementedError(f"Unsupported method: {r_type}")
692
674
 
693
675
  request_kwargs = {}
694
676
  if r_type in ("POST", "PATCH") and "files" in payload:
695
- request_kwargs["data"] = payload.get("json", {})
696
- request_kwargs["files"] = payload["files"]
677
+ request_kwargs = dict(payload)
678
+ request_kwargs["data"] = request_kwargs.pop("json", {})
697
679
  s.headers.pop("Content-Type", None)
698
680
  else:
699
- request_kwargs = payload
700
-
701
- req = get_req(session=s)
702
- keep_request = True
703
- counter = 0
704
- auth_retried = False
681
+ request_kwargs = dict(payload)
682
+ request_kwargs.setdefault("allow_redirects", False)
683
+ req = getattr(s, r_type.lower())
705
684
 
706
685
  if accept_gzip:
707
686
  s.headers.setdefault("Accept-Encoding", "gzip")
708
687
 
709
- while keep_request:
688
+ with request_budget(timeout) as budget:
710
689
  try:
711
- if loaders is not None:
712
- s.headers.update(loaders.refresh_headers(force=False, session=s))
713
-
714
- start_time = time.perf_counter()
715
- logger.debug(f"Requesting {r_type} from {url}")
716
- r = req(url, timeout=timeout, **request_kwargs)
717
- duration = time.perf_counter() - start_time
718
- logger.debug(f"{url} took {duration:.4f} seconds.")
719
-
720
- if r.status_code == 401 and loaders is not None and not auth_retried:
721
- logger.warning(f"Error {r.status_code}; forcing auth refresh once")
722
- # A refused renewal raises AuthError: it is reported below with the
723
- # reason and the repair, not as a traceback.
724
- s.headers.update(loaders.refresh_headers(force=True, session=s))
725
- req = get_req(session=s)
726
- auth_retried = True
727
- continue
728
-
729
- keep_request = False
730
- break
690
+ for auth_attempt in range(2):
691
+ if loaders is not None:
692
+ budget.timeout()
693
+ s.headers.update(loaders.refresh_headers(force=bool(auth_attempt), session=s))
694
+
695
+ start_time = time.perf_counter()
696
+ logger.debug(f"Requesting {r_type} from {url}")
697
+ r = req(url, timeout=budget.timeout(), **request_kwargs)
698
+ duration = time.perf_counter() - start_time
699
+ logger.debug(f"{url} took {duration:.4f} seconds.")
700
+ if (
701
+ r.status_code != 401
702
+ or loaders is None
703
+ or auth_attempt
704
+ or r_type not in DEFAULT_ALLOWED_METHODS
705
+ ):
706
+ return r
707
+ r.close()
708
+ logger.warning("Error 401; forcing auth refresh once within the request budget")
731
709
 
732
710
  except AuthError as e:
733
711
  logger.warning(f"Auth error for {url}: {e}")
@@ -736,32 +714,15 @@ def make_request(
736
714
  r.error_type = "auth_error"
737
715
  r.status_code = 401
738
716
  r._content = str(e).encode("utf-8")
739
- keep_request = False
740
- break
741
-
742
- except requests.exceptions.ConnectionError:
743
- logger.exception(f"Error connecting {url}")
744
- except TypeError as e:
745
- logger.exception(f"Type error for {url} exception {e}")
746
- raise e
747
- except Exception as e:
748
- logger.exception(f"Error connecting {url} exception {e}")
749
-
750
- counter += 1
751
- if counter >= TRIES:
752
- keep_request = False
717
+ return r
718
+ except requests.RequestException as e:
719
+ logger.warning(f"Request {r_type} to {url} failed: {e}")
753
720
  r = Response()
754
721
  r.code = "expired"
755
722
  r.error_type = "expired"
756
723
  r.status_code = 500
757
- break
758
-
759
- logger.debug(
760
- f"Trying request again after {TIMEOFF}s - Counter: {counter}/{TRIES} - URL: {url}"
761
- )
762
- time.sleep(TIMEOFF)
763
-
764
- return r
724
+ r._content = str(e).encode("utf-8")
725
+ return r
765
726
 
766
727
 
767
728
  def build_session(
@@ -779,23 +740,7 @@ def build_session(
779
740
  if accept_gzip:
780
741
  s.headers.setdefault("Accept-Encoding", "gzip")
781
742
 
782
- retry_kwargs = dict(
783
- total=retries,
784
- connect=retries,
785
- read=retries,
786
- status=retries,
787
- backoff_factor=backoff_factor,
788
- status_forcelist=DEFAULT_STATUS_FORCELIST,
789
- respect_retry_after_header=True,
790
- raise_on_status=False,
791
- )
792
-
793
- try:
794
- retry_cfg = Retry(allowed_methods=DEFAULT_ALLOWED_METHODS, **retry_kwargs)
795
- except TypeError:
796
- retry_cfg = Retry(method_whitelist=DEFAULT_ALLOWED_METHODS, **retry_kwargs)
797
-
798
- adapter = HTTPAdapter(max_retries=retry_cfg)
743
+ adapter = DeadlineHTTPAdapter(retries=retries, backoff_factor=backoff_factor)
799
744
  s.mount("https://", adapter)
800
745
  s.mount("http://", adapter)
801
746
  return s
@@ -929,24 +874,7 @@ def _install_retry_adapters_in_place(
929
874
  Configure retry adapters on an EXISTING session object (do not rebind 'session').
930
875
  This is critical so 'from utils import session' users still get the updated behavior.
931
876
  """
932
- retry_kwargs = dict(
933
- total=retries,
934
- connect=retries,
935
- read=retries,
936
- status=retries,
937
- backoff_factor=backoff_factor,
938
- status_forcelist=DEFAULT_STATUS_FORCELIST,
939
- respect_retry_after_header=True,
940
- raise_on_status=False,
941
- )
942
-
943
- # urllib3 compatibility across versions
944
- try:
945
- retry_cfg = Retry(allowed_methods=DEFAULT_ALLOWED_METHODS, **retry_kwargs)
946
- except TypeError:
947
- retry_cfg = Retry(method_whitelist=DEFAULT_ALLOWED_METHODS, **retry_kwargs)
948
-
949
- adapter = HTTPAdapter(max_retries=retry_cfg)
877
+ adapter = DeadlineHTTPAdapter(retries=retries, backoff_factor=backoff_factor)
950
878
 
951
879
  # Close old adapters' pools (best-effort), then mount new ones
952
880
  for prefix in ("https://", "http://"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 9.0.6.dev40
3
+ Version: 9.0.6.dev41
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: MIT
@@ -34,27 +34,28 @@ mainsequence/cli/sdk_utils.py,sha256=SFuSzh8WDNf21mLiDa51K8Fqe5XiLWh2KhHj8zUjCXo
34
34
  mainsequence/cli/ssh_utils.py,sha256=m7HgFHZQ545sxY9igfQLpaNjWHm4SiN9VwCkAuGy2yw,11124
35
35
  mainsequence/cli/ui.py,sha256=memd6pmjALfGj8zndGCx_z80ztQeqTcOvcofU0un34U,2277
36
36
  mainsequence/client/__init__.py,sha256=lsr9JWxeVqOG0KRoHErW5M40ACPSR7SAEE508dTN5XM,672
37
+ mainsequence/client/_transport.py,sha256=l2xkbJ21SbrJhzgOf58Yikxg-uzOQPyo-H5hENdCBo8,5374
37
38
  mainsequence/client/agent_runtime_models.py,sha256=lay0DN3lN5p6-3ajVsaHvPgcmrURFwrNfTb6upoImm4,66642
38
39
  mainsequence/client/base.py,sha256=T3k3xptM09TreiVogrxAPWoIy8zM6Dtn3Cbwsnb33ag,42678
39
- mainsequence/client/client.py,sha256=sUsJhjwKq4oZsEX2Ozm-7owcyQq0kgFNKUrtEYwf8cU,3907
40
+ mainsequence/client/client.py,sha256=Jaw9RYObdVDS5J2O0pln99-XpuW9e4vW-SsbJRNldfw,3866
40
41
  mainsequence/client/compute_validation.py,sha256=ByeX0CkIE7qq06ggZFfN92U3Qp2e9cvo2RTIGWa5_Vw,6662
41
42
  mainsequence/client/exceptions.py,sha256=xx2L4l3OMNsTybbYkiTKK398ahnutEascM0It35tGr0,3815
42
43
  mainsequence/client/github_issues.py,sha256=pSTUw3UPrmE6wqnpoJctartTaNSq6iu6PQRrd8hip6I,14568
43
- mainsequence/client/inference.py,sha256=iMg3l5DGW6PQeVkz38Z3lTpxryMO14CTK6gIRHLYUBE,7368
44
+ mainsequence/client/inference.py,sha256=g24H9LWumMRABIevho8FS7BBOuyYvf80pRwCmJZpLeY,7353
44
45
  mainsequence/client/models_foundry.py,sha256=iN1jJzDokfniEQF0Ac5eepAB7L5pVjJnKavaHLP7BHA,36310
45
46
  mainsequence/client/models_helpers.py,sha256=y7cgcsH8yO3dfIWPhRdcYam7ItWHw3Ps63-WYDe4YHE,46262
46
47
  mainsequence/client/models_user.py,sha256=BU_hfn4-4KqYz3nsaPlXuYZmUG6_-7YM892mkM92Zyo,37613
47
48
  mainsequence/client/observability.py,sha256=_WrDKCaNk_TXmKElKCXC1inork6Exxxe8FRY4m1GI9s,16008
48
- mainsequence/client/utils.py,sha256=fQur4p-ojLWTMcNWlYWx7uZKbGNEc4c2kJq1Wl_w070,31925
49
+ mainsequence/client/utils.py,sha256=c4JjMIqKfwSz8DdkwLGmGtQ9lSmVn_KEw9KWGDwz_T0,30133
49
50
  mainsequence/client/value_sets.py,sha256=7TA-mpuAd3hOduNgO1WTQQTuNOGaFgue-VqCsDWKaC8,6181
50
51
  mainsequence/instrumentation/__init__.py,sha256=J_9XmfjprElvz0u2t9rTyJT0kFdyKpxdbkdQiaw28_U,316
51
52
  mainsequence/instrumentation/utils.py,sha256=b-ihjSioAepeLfMpudM2SqCtvnxMqIXileHx3klBCQU,3377
52
53
  mainsequence/server/__init__.py,sha256=BHUAhtdWPDRdxX07NYO_6wrBv5mf0GsXflecO52PmUc,73
53
54
  mainsequence/server/caller_assertions.py,sha256=hZW1pkkvQ9gaFL075bxUbbctTRrgMMQRTSc2Da0vKCM,9705
54
55
  mainsequence/server/fastapi.py,sha256=tD1aVuJOgoBTu7H8bs9IldPNYRT4fhkr1leN3KZP1pI,10521
55
- mainsequence-9.0.6.dev40.dist-info/licenses/LICENSE,sha256=b6Ecsk8wjsEby3lbSzdljR6hHiFGHBizt9iWV2p1c_I,1074
56
- mainsequence-9.0.6.dev40.dist-info/METADATA,sha256=qtgjSWqmCAC819Qw7KGhUW9lv_HTqZ0O3Mv-5hDfcqo,7402
57
- mainsequence-9.0.6.dev40.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
58
- mainsequence-9.0.6.dev40.dist-info/entry_points.txt,sha256=XGAOq3-mF2lWWpeRJvoaWyMarRcK167mH1g124aGdUs,88
59
- mainsequence-9.0.6.dev40.dist-info/top_level.txt,sha256=BFeblM_Q9q0cUWBvTxnfWcWQEHtf5XZi9nOS-Gdfwus,28
60
- mainsequence-9.0.6.dev40.dist-info/RECORD,,
56
+ mainsequence-9.0.6.dev41.dist-info/licenses/LICENSE,sha256=b6Ecsk8wjsEby3lbSzdljR6hHiFGHBizt9iWV2p1c_I,1074
57
+ mainsequence-9.0.6.dev41.dist-info/METADATA,sha256=0Dok8XfsCUQJJDt_13aGFAKOU8jLBJzFUbqHm-yh7Uo,7402
58
+ mainsequence-9.0.6.dev41.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
59
+ mainsequence-9.0.6.dev41.dist-info/entry_points.txt,sha256=XGAOq3-mF2lWWpeRJvoaWyMarRcK167mH1g124aGdUs,88
60
+ mainsequence-9.0.6.dev41.dist-info/top_level.txt,sha256=BFeblM_Q9q0cUWBvTxnfWcWQEHtf5XZi9nOS-Gdfwus,28
61
+ mainsequence-9.0.6.dev41.dist-info/RECORD,,