pypsrp 0.9.0rc1__py3-none-any.whl → 0.9.0rc2__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,142 @@
1
+ # Copyright: (c) 2026, Jordan Borean (@jborean93) <jborean93@gmail.com>
2
+ # MIT License (see LICENSE or https://opensource.org/licenses/MIT)
3
+
4
+ from __future__ import annotations
5
+
6
+ import contextvars
7
+ import functools
8
+ import types
9
+ import typing as t
10
+
11
+ import requests.adapters
12
+ from requests.packages.urllib3.util.retry import Retry
13
+
14
+ T = t.TypeVar("T")
15
+
16
+
17
+ class NewConnectionDisabled(Exception):
18
+ """Raised when new connections are being made but have been disabled in the current context."""
19
+
20
+
21
+ class DisableNewConnectionsContext:
22
+ """Context manager to disable new connections from being created."""
23
+
24
+ __slots__ = "_contextvar_token"
25
+
26
+ _contextvar: t.ClassVar[contextvars.ContextVar] = contextvars.ContextVar("NewConnectionContext")
27
+ _contextvar_token: contextvars.Token
28
+
29
+ @classmethod
30
+ def current(cls) -> DisableNewConnectionsContext | None:
31
+ """Returns the current context or None if not set."""
32
+ try:
33
+ return cls._contextvar.get()
34
+ except LookupError:
35
+ return None
36
+
37
+ def __enter__(self) -> DisableNewConnectionsContext:
38
+ self._contextvar_token = self.__class__._contextvar.set(self)
39
+ return self
40
+
41
+ def __exit__(
42
+ self,
43
+ exc_type: type[BaseException] | None,
44
+ exc_val: BaseException | None,
45
+ exc_tb: types.TracebackType | None,
46
+ ) -> bool | None:
47
+ self.__class__._contextvar.reset(self._contextvar_token)
48
+ del self._contextvar_token
49
+
50
+ return None
51
+
52
+
53
+ class HTTPSAdapterWithKeyPassword(requests.adapters.HTTPAdapter):
54
+
55
+ def __init__(
56
+ self,
57
+ *args: t.Any,
58
+ _pypsrp_key_password: str | None = None,
59
+ **kwargs: t.Any,
60
+ ) -> None:
61
+ self.__key_password = _pypsrp_key_password
62
+ super().__init__(*args, **kwargs)
63
+
64
+ def init_poolmanager(
65
+ self,
66
+ connections,
67
+ maxsize,
68
+ block=False,
69
+ **pool_kwargs,
70
+ ):
71
+ return super().init_poolmanager(
72
+ connections,
73
+ maxsize,
74
+ block,
75
+ key_password=self.__key_password,
76
+ **pool_kwargs,
77
+ )
78
+
79
+
80
+ def _wrap(
81
+ func: t.Callable[..., T],
82
+ *,
83
+ before: t.Callable[[], None] | None = None,
84
+ after: t.Callable[[T], None] | None = None,
85
+ ) -> t.Callable[..., T]:
86
+ """Wraps a function with before and after callables."""
87
+
88
+ @functools.wraps(func)
89
+ def wrapper(*args: t.Any, **kwargs: t.Any) -> T:
90
+ if before:
91
+ before()
92
+
93
+ res = func(*args, **kwargs)
94
+
95
+ if after:
96
+ after(res)
97
+
98
+ return res
99
+
100
+ return wrapper
101
+
102
+
103
+ def _raise_if_connections_disabled():
104
+ if DisableNewConnectionsContext.current() is not None:
105
+ raise NewConnectionDisabled()
106
+
107
+
108
+ def _create_new_connection_with_failure(connection):
109
+ connection.connect = _wrap(connection.connect, before=_raise_if_connections_disabled)
110
+
111
+
112
+ def _create_connection_pool(pool):
113
+ # This is called in the urllib3 pool when a new connection is needed. We
114
+ # wrap it so we can wrap the connect method when a new connection is
115
+ # created.
116
+ pool.ConnectionCls = _wrap(pool.ConnectionCls, after=_create_new_connection_with_failure)
117
+
118
+
119
+ def create_request_adapter( # type: ignore[no-any-unimported] # requests does not have typing stubs for urllib3
120
+ *,
121
+ max_retries: Retry,
122
+ key_password: str | None = None,
123
+ ) -> requests.adapters.HTTPAdapter:
124
+ """Creates a HTTPAdapter with support for disabling new connections via context."""
125
+ adapter: requests.adapters.HTTPAdapter
126
+ if key_password is not None:
127
+ adapter = HTTPSAdapterWithKeyPassword(
128
+ max_retries=max_retries,
129
+ _pypsrp_key_password=key_password,
130
+ )
131
+ else:
132
+ adapter = requests.adapters.HTTPAdapter(max_retries=max_retries)
133
+
134
+ # pool_classes_by_scheme stores the urllib3 pool types used when creating
135
+ # the connection pool for http/https. We wrap the __init__ method so we can
136
+ # inject our custom ConnectionCls that wraps the connect method when
137
+ # created.
138
+ pool_classes = adapter.poolmanager.pool_classes_by_scheme
139
+ for scheme, pool_cls in list(pool_classes.items()):
140
+ pool_classes[scheme] = _wrap(pool_cls, after=_create_connection_pool)
141
+
142
+ return adapter
pypsrp/negotiate.py CHANGED
@@ -36,7 +36,7 @@ class HTTPNegotiateAuth(AuthBase):
36
36
  password: typing.Optional[str] = None,
37
37
  auth_provider: str = "negotiate",
38
38
  send_cbt: bool = True,
39
- service: str = "WSMAN",
39
+ service: str = "host",
40
40
  delegate: bool = False,
41
41
  hostname_override: typing.Optional[str] = None,
42
42
  wrap_required: bool = False,
@@ -60,7 +60,7 @@ class HTTPNegotiateAuth(AuthBase):
60
60
  :param send_cbt: Try to bind the channel token (HTTPS only) to the auth
61
61
  process, default is True
62
62
  :param service: The service part of the SPN to authenticate with,
63
- defaults to HTTP
63
+ defaults to host
64
64
  :param delegate: Whether to get an auth token that allows the token to
65
65
  be delegated to other servers, this is only used with Kerberos and
66
66
  defaults to False
pypsrp/powershell.py CHANGED
@@ -1,6 +1,8 @@
1
1
  # Copyright: (c) 2018, Jordan Borean (@jborean93) <jborean93@gmail.com>
2
2
  # MIT License (see LICENSE or https://opensource.org/licenses/MIT)
3
3
 
4
+ from __future__ import annotations
5
+
4
6
  import base64
5
7
  import logging
6
8
  import struct
@@ -11,9 +13,11 @@ import uuid
11
13
  import warnings
12
14
  import xml.etree.ElementTree as ET
13
15
 
16
+ import requests.exceptions
14
17
  from cryptography.hazmat.primitives.asymmetric import padding, rsa
15
18
  from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
16
19
 
20
+ from pypsrp import _pool_manager
17
21
  from pypsrp._utils import version_equal_or_newer
18
22
  from pypsrp.complex_objects import (
19
23
  ApartmentState,
@@ -104,6 +108,7 @@ class RunspacePool(object):
104
108
  session_key_timeout_ms: int = 60000,
105
109
  *,
106
110
  no_profile: bool = False,
111
+ idle_timeout: int | None = None,
107
112
  ) -> None:
108
113
  """
109
114
  Represents a Runspace pool on a remote host. This pool can contain
@@ -132,6 +137,9 @@ class RunspacePool(object):
132
137
  key transfer from the server
133
138
  :param no_profile: If True, the user profile will not be loaded and
134
139
  will use the machine defaults.
140
+ :param idle_timeout: The idle timeout, in seconds, for the runspace. The
141
+ runspace will be closed if no operations are performed in the time
142
+ specified. If None the server default will be used.
135
143
  """
136
144
  log.info("Initialising RunspacePool object for configuration %s" % configuration_name)
137
145
  # The below are defined in some way at
@@ -147,6 +155,7 @@ class RunspacePool(object):
147
155
  input_streams="stdin pr",
148
156
  output_streams="stdout",
149
157
  no_profile=no_profile,
158
+ idle_time_out=idle_timeout,
150
159
  )
151
160
  self.ci_table: typing.Dict = {}
152
161
  self.pipelines: typing.Dict[str, "PowerShell"] = {}
@@ -659,6 +668,47 @@ class RunspacePool(object):
659
668
  """
660
669
  return self._serializer.serialize(obj, metadata=metadata)
661
670
 
671
+ def is_alive(
672
+ self,
673
+ *,
674
+ timeout: int | None = None,
675
+ ) -> bool:
676
+ """Checks whether the RunspacePool is still alive.
677
+
678
+ :param timeout: Override the connection timeout defaults for the
679
+ request. The WSMan operation timeout will be set to this value
680
+ and the HTTP connect/read timeouts will be this value + 2 seconds.
681
+ :return: A bool True if the pool is still alive, False otherwise.
682
+ """
683
+ is_closed = False
684
+
685
+ try:
686
+ # We don't want to try and open a new connection if the socket
687
+ # was closed, we treat it as the pool is not alive.
688
+ with _pool_manager.DisableNewConnectionsContext():
689
+ state = self.shell._get_shell(timeout=timeout)
690
+
691
+ is_closed = state.get("State", "") == "Disconnected"
692
+ except WSManFaultError as exc:
693
+ if exc.code in [
694
+ 0x80338029, # ERROR_WSMAN_OPERATION_TIMEDOUT
695
+ 0x8033805B, # ERROR_WSMAN_UNEXPECTED_SELECTORS
696
+ ]:
697
+ is_closed = True
698
+ else:
699
+ raise
700
+
701
+ except (_pool_manager.NewConnectionDisabled, TimeoutError, requests.exceptions.ConnectionError):
702
+ # If a timeout or connection error occurs, treat the pool as closed.
703
+ is_closed = True
704
+
705
+ if is_closed:
706
+ # Ensures that close() doesn't try and close an already closed pool
707
+ self.state = RunspacePoolState.CLOSED
708
+ return False
709
+
710
+ return True
711
+
662
712
  def _receive(
663
713
  self,
664
714
  id: typing.Optional[str] = None,
pypsrp/shell.py CHANGED
@@ -1,6 +1,8 @@
1
1
  # Copyright: (c) 2018, Jordan Borean (@jborean93) <jborean93@gmail.com>
2
2
  # MIT License (see LICENSE or https://opensource.org/licenses/MIT)
3
3
 
4
+ from __future__ import annotations
5
+
4
6
  import base64
5
7
  import logging
6
8
  import types
@@ -315,6 +317,42 @@ class WinRS(object):
315
317
  ET.SubElement(signal, "{%s}Code" % rsp).text = code
316
318
  return self.wsman.signal(self.resource_uri, signal, selector_set=self._selector_set)
317
319
 
320
+ def _get_shell(
321
+ self,
322
+ *,
323
+ timeout: int | None = None,
324
+ ) -> dict[str, str | None]:
325
+ """Queries the WSMan host for the current shell state.
326
+
327
+ Returns the WSMan GetResponse fields as a dictionary.
328
+
329
+ The timeout parameter can be used to override the default WSMan timeout
330
+ for this operation. If set the HTTP connect and read timeout will be
331
+ set to this value + 2 seconds to ensure the request does not block for
332
+ a longer time.
333
+
334
+ :param timeout: Sets the WSMan operational timeout to this value in
335
+ seconds. Overrides the default timeout set on the WSMan instance.
336
+ :return: A dictionary containing the shell state fields from the raw XML
337
+ response.
338
+ """
339
+ rsp = NAMESPACES["rsp"]
340
+
341
+ resp = self.wsman.get(
342
+ "http://schemas.microsoft.com/wbem/wsman/1/windows/shell",
343
+ selector_set=self._selector_set,
344
+ timeout=timeout,
345
+ )
346
+
347
+ state = {}
348
+ shell_items = resp.find("rsp:Shell", namespaces=NAMESPACES)
349
+ if shell_items is not None:
350
+ for child in shell_items:
351
+ field_name = child.tag.replace(f"{{{rsp}}}", "")
352
+ state[field_name] = child.text
353
+
354
+ return state
355
+
318
356
  def _parse_shell_create(
319
357
  self,
320
358
  response: ET.Element,
pypsrp/wsman.py CHANGED
@@ -1,7 +1,7 @@
1
1
  # Copyright: (c) 2018, Jordan Borean (@jborean93) <jborean93@gmail.com>
2
2
  # MIT License (see LICENSE or https://opensource.org/licenses/MIT)
3
3
 
4
- from __future__ import division
4
+ from __future__ import annotations
5
5
 
6
6
  import ipaddress
7
7
  import logging
@@ -15,6 +15,7 @@ import xml.etree.ElementTree as ET
15
15
  import requests
16
16
  from requests.packages.urllib3.util.retry import Retry
17
17
 
18
+ from pypsrp import _pool_manager
18
19
  from pypsrp._utils import get_hostname, to_string, to_unicode
19
20
  from pypsrp.encryption import WinRMEncryption
20
21
  from pypsrp.exceptions import (
@@ -40,7 +41,7 @@ log = logging.getLogger(__name__)
40
41
  SUPPORTED_AUTHS = ["basic", "certificate", "credssp", "kerberos", "negotiate", "ntlm"]
41
42
 
42
43
  AUTH_KWARGS: typing.Dict[str, typing.List[str]] = {
43
- "certificate": ["certificate_key_pem", "certificate_pem"],
44
+ "certificate": ["certificate_key_pem", "certificate_pem", "certificate_key_password"],
44
45
  "credssp": ["credssp_auth_mechanism", "credssp_disable_tlsv1_2", "credssp_minimum_version"],
45
46
  "negotiate": ["negotiate_delegate", "negotiate_hostname_override", "negotiate_send_cbt", "negotiate_service"],
46
47
  }
@@ -471,6 +472,14 @@ class WSMan(object):
471
472
  s = NAMESPACES["s"]
472
473
  envelope = ET.Element("{%s}Envelope" % s)
473
474
 
475
+ http_timeout = None
476
+ if timeout:
477
+ # Failsafe if the operation timeout exceeds we don't want the
478
+ # request to hang indefinitely. The HTTP timeout needs to be more
479
+ # than the WSMan timeout as we cannot guarantee a WSMan response
480
+ # until the operation timeout is hit.
481
+ http_timeout = timeout + 2
482
+
474
483
  message_id, header = self._create_header(action, resource_uri, option_set, selector_set, timeout)
475
484
  message_id = f"uuid:{message_id}"
476
485
  envelope.append(header)
@@ -482,7 +491,11 @@ class WSMan(object):
482
491
  xml = ET.tostring(envelope, encoding="utf-8", method="xml")
483
492
 
484
493
  try:
485
- response = self.transport.send(xml, retries_on_read_timeout=retries_on_read_timeout)
494
+ response = self.transport.send(
495
+ xml,
496
+ retries_on_read_timeout=retries_on_read_timeout,
497
+ timeout=http_timeout,
498
+ )
486
499
  except WinRMTransportError as err:
487
500
  try:
488
501
  # try and parse the XML and get the WSManFault
@@ -787,6 +800,7 @@ class _TransportHTTP(object):
787
800
 
788
801
  self.certificate_key_pem: typing.Optional[str] = None
789
802
  self.certificate_pem: typing.Optional[str] = None
803
+ self.certificate_key_password: typing.Optional[str] = None
790
804
  for kwarg_list in AUTH_KWARGS.values():
791
805
  for kwarg in kwarg_list:
792
806
  setattr(self, kwarg, kwargs.get(kwarg, None))
@@ -810,6 +824,7 @@ class _TransportHTTP(object):
810
824
  message: bytes,
811
825
  *,
812
826
  retries_on_read_timeout: int = 0,
827
+ timeout: int | float | tuple[int | float, int | float] | None = None,
813
828
  ) -> bytes:
814
829
  hostname = get_hostname(self.endpoint)
815
830
  if self.session is None:
@@ -824,9 +839,9 @@ class _TransportHTTP(object):
824
839
  prep_request = self.session.prepare_request(request)
825
840
 
826
841
  try:
827
- self._send_request(prep_request)
842
+ self._send_request(prep_request, timeout=timeout)
828
843
  except (requests.ReadTimeout, requests.ConnectTimeout) as e:
829
- log.exception("%s during initial authentication request - attempt %d", (type(e).__name__, attempt))
844
+ log.exception("%s during initial authentication request - attempt %d", type(e).__name__, attempt)
830
845
  if attempt == retries_on_read_timeout:
831
846
  raise
832
847
 
@@ -867,9 +882,12 @@ class _TransportHTTP(object):
867
882
  request = requests.Request("POST", self.endpoint, data=payload, headers=headers)
868
883
  prep_request = self.session.prepare_request(request)
869
884
  try:
870
- return self._send_request(prep_request)
885
+ return self._send_request(
886
+ prep_request,
887
+ timeout=timeout,
888
+ )
871
889
  except (requests.ReadTimeout, requests.ConnectTimeout) as e:
872
- log.exception("%s during WSMan request - attempt %d", (type(e).__name__, attempt))
890
+ log.exception("%s during WSMan request - attempt %d", type(e).__name__, attempt)
873
891
  if attempt == retries_on_read_timeout:
874
892
  raise
875
893
 
@@ -880,8 +898,15 @@ class _TransportHTTP(object):
880
898
  attempt += 1
881
899
  time.sleep(self.reconnection_backoff * (2**attempt - 1))
882
900
 
883
- def _send_request(self, request: requests.PreparedRequest) -> bytes:
884
- response = self.session.send(request, timeout=(self.connection_timeout, self.read_timeout)) # type: ignore[union-attr] # This should not happen
901
+ def _send_request(
902
+ self,
903
+ request: requests.PreparedRequest,
904
+ timeout: int | float | tuple[int | float, int | float] | None = None,
905
+ ) -> bytes:
906
+ response = self.session.send( # type: ignore[union-attr] # This should not happen
907
+ request,
908
+ timeout=timeout if timeout is not None else (self.connection_timeout, self.read_timeout),
909
+ )
885
910
 
886
911
  content_type = response.headers.get("content-type", "")
887
912
  if content_type.startswith("multipart/encrypted;") or content_type.startswith("multipart/x-multi-encrypted;"):
@@ -959,8 +984,14 @@ class _TransportHTTP(object):
959
984
  del retry_kwargs["status"]
960
985
  retries = Retry(**retry_kwargs)
961
986
 
962
- session.mount("http://", requests.adapters.HTTPAdapter(max_retries=retries))
963
- session.mount("https://", requests.adapters.HTTPAdapter(max_retries=retries))
987
+ session.mount(
988
+ "http://",
989
+ _pool_manager.create_request_adapter(max_retries=retries),
990
+ )
991
+ session.mount(
992
+ "https://",
993
+ _pool_manager.create_request_adapter(max_retries=retries, key_password=self.certificate_key_password),
994
+ )
964
995
 
965
996
  # set cert validation config
966
997
  session.verify = self.cert_validation
@@ -998,6 +1029,9 @@ class _TransportHTTP(object):
998
1029
  if self.ssl is False:
999
1030
  raise ValueError("For certificate auth, SSL must be used")
1000
1031
 
1032
+ # requests does not expose the password through the cert tuple. If set
1033
+ # it'll be passed to urllib3 through the custom adapter created for the
1034
+ # session.
1001
1035
  session.cert = (self.certificate_pem, self.certificate_key_pem)
1002
1036
  session.headers["Authorization"] = "http://schemas.dmtf.org/wbem/wsman/1/wsman/secprofile/https/mutual"
1003
1037
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypsrp
3
- Version: 0.9.0rc1
3
+ Version: 0.9.0rc2
4
4
  Summary: PowerShell Remoting Protocol and WinRM for Python
5
5
  Author-email: Jordan Borean <jborean93@gmail.com>
6
6
  License-Expression: MIT
@@ -18,7 +18,7 @@ Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: cryptography>=3.1
20
20
  Requires-Dist: pyspnego<1.0.0,>=0.7.0
21
- Requires-Dist: requests>=2.9.1
21
+ Requires-Dist: requests>=2.27.0
22
22
  Provides-Extra: credssp
23
23
  Requires-Dist: requests-credssp>=2.0.0; extra == "credssp"
24
24
  Provides-Extra: kerberos
@@ -224,7 +224,8 @@ These are the options that can be used to setup `WSMan`;
224
224
  * `data_locale`: The `wsmv:DataLocale` value to set on each WSMan request. This specifies the format in which numerical data is presented in the response text, default is the value of `locale`
225
225
  * `reconnection_retries`: Number of retries on a connection problem, default is `0`
226
226
  * `reconnection_backoff`: Number of seconds to backoff in between reconnection attempts (first sleeps X, then sleeps 2*X, 4*X, 8*X, ...), default is `2.0`
227
- * `certificate_key_pem`: The path to the certificate key used in `certificate` authentication
227
+ * `certificate_key_pem`: The path to the certificate key used in `certificate` authentication. The key can be in either a `PKCS#1` or `PKCS#8` format
228
+ * `certificate_key_password`: The password for `certificate_key_pem` if it is encrypted
228
229
  * `certificate_pem`: The path to the certificate used in `certificate` authentication
229
230
  * `credssp_auth_mechanism`: The sub-auth mechanism used in CredSSP, default is `auto`, choices are `auto`, `ntlm`, or `kerberos`
230
231
  * `credssp_disable_tlsv1_2`: Whether to used CredSSP auth over the insecure TLSv1.0, default is `False`
@@ -1,4 +1,5 @@
1
1
  pypsrp/__init__.py,sha256=9aButMuBNIEph0LfHsodNd8uSS7XKZj9IPHJsPd8hjY,958
2
+ pypsrp/_pool_manager.py,sha256=yKUzdzkE4hEo2mqIC-kKPL9uWpIk9mixCQ1AP5rgn_o,4222
2
3
  pypsrp/_utils.py,sha256=y9sezXQPl0vXnr1beyZ8xcn-Q_zdg_7cg_lPk9QmjVQ,3540
3
4
  pypsrp/client.py,sha256=iJERbEXwwE4wswpqAL0Dcz8ACqLvXUT9Dcl_Qqa3830,13892
4
5
  pypsrp/complex_objects.py,sha256=PkJ1F1Byw5RI_Vpfy1KfcMFJdOfioOSsIbi50S_fZ3g,62666
@@ -6,17 +7,17 @@ pypsrp/encryption.py,sha256=4wOontH-eF8BBxTyHTX9iAF0uIM6m-z3N9poatspP1A,3870
6
7
  pypsrp/exceptions.py,sha256=SWdQ9UAPOIwr7B4twGqDoXLCEtXFK_5u3PYIVLvEIbA,4034
7
8
  pypsrp/host.py,sha256=XQgnkwzsZprNmAr1TPJpEBmj3xRSWFrYQFSIi870aNE,45065
8
9
  pypsrp/messages.py,sha256=mCuie1ywOQmkXltISPvrb6ITS6GUA0oSld2jfNLdBf4,34169
9
- pypsrp/negotiate.py,sha256=YU6mGuDd8krOapiRcY6qID8v-GMDbIypCYUc7eTkzmw,10952
10
- pypsrp/powershell.py,sha256=SjmD8LfbMtyoGfXpQmSV10ded4FO14J8vM5Qn37R0dI,67359
10
+ pypsrp/negotiate.py,sha256=iVX2J-slABamesMajjTfmfqucDS4n5vVTTde_IDqpuE,10951
11
+ pypsrp/powershell.py,sha256=NRDoFtOsYjIrJHMQ6r6vRq6uL2EtPhhem5ZA_bWoDnw,69296
11
12
  pypsrp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
13
  pypsrp/serializer.py,sha256=49rBxfo3fh511mU3uzoK9vFvIOrBtyX3J-CUugbezc0,33767
13
- pypsrp/shell.py,sha256=EoGtElIYvNFPjBvAIMuLaHPyX0NgGtBN265Vt6YqkhQ,16748
14
- pypsrp/wsman.py,sha256=V90G93O2bi9fPP09fPhyheYN4_G_8zo_Sxo1ea2pqPI,47500
14
+ pypsrp/shell.py,sha256=80mh35c-UXjsy_MFG9J3GGYLYqvk2FppUgrFHkWh6xg,18065
15
+ pypsrp/wsman.py,sha256=KkRg9UDDq1yxY4TKK9mGI-vS6px6ftQDPhmeojjItKg,48728
15
16
  pypsrp/pwsh_scripts/__init__.py,sha256=VckRBWWDUdh_LTwPspE2FXSo8TaT50QnZ1aZ-yvxJmU,139
16
17
  pypsrp/pwsh_scripts/copy.ps1,sha256=sTy4jcnh0cV5Y0l2-dd1znc-eM_kgE-okk51xZzdIQk,5374
17
18
  pypsrp/pwsh_scripts/fetch.ps1,sha256=7gvPxURnksZetYgjWRjbIDIWSJYv2xGXZjRymvNUj6w,1989
18
- pypsrp-0.9.0rc1.dist-info/licenses/LICENSE,sha256=8uJLgs8Wb_Us_8XaTvOOn7Yf0dwzFi9pcFHVxfgJKoE,1079
19
- pypsrp-0.9.0rc1.dist-info/METADATA,sha256=RsbvzIn44QmLDG7Pi5QCULyxtJ8Z2kZByhI7dkU6Fmo,23485
20
- pypsrp-0.9.0rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
- pypsrp-0.9.0rc1.dist-info/top_level.txt,sha256=9sFQD0PMIG07xbaR4j-1Fq7NaD9kwInn_HTc3Gqznq0,7
22
- pypsrp-0.9.0rc1.dist-info/RECORD,,
19
+ pypsrp-0.9.0rc2.dist-info/licenses/LICENSE,sha256=8uJLgs8Wb_Us_8XaTvOOn7Yf0dwzFi9pcFHVxfgJKoE,1079
20
+ pypsrp-0.9.0rc2.dist-info/METADATA,sha256=_zCveXjKqq1_OiCmjLtge2Zr3v1G-S2EFrF33PTd6QA,23630
21
+ pypsrp-0.9.0rc2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
22
+ pypsrp-0.9.0rc2.dist-info/top_level.txt,sha256=9sFQD0PMIG07xbaR4j-1Fq7NaD9kwInn_HTc3Gqznq0,7
23
+ pypsrp-0.9.0rc2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5