weheat 2025.1.14rc1__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 weheat might be problematic. Click here for more details.

weheat/rest.py ADDED
@@ -0,0 +1,327 @@
1
+ # coding: utf-8
2
+
3
+ """
4
+ Weheat Backend
5
+
6
+ This is the backend for the Weheat project
7
+
8
+ The version of the OpenAPI document: v1
9
+ Generated by OpenAPI Generator (https://openapi-generator.tech)
10
+
11
+ Do not edit the class manually.
12
+ """ # noqa: E501
13
+
14
+
15
+ import io
16
+ import json
17
+ import logging
18
+ import re
19
+ import ssl
20
+
21
+ from urllib.parse import urlencode, quote_plus
22
+ import urllib3
23
+
24
+ from weheat.exceptions import ApiException, UnauthorizedException, ForbiddenException, NotFoundException, ServiceException, ApiValueError, BadRequestException
25
+
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ SUPPORTED_SOCKS_PROXIES = {"socks5", "socks5h", "socks4", "socks4a"}
30
+
31
+
32
+ def is_socks_proxy_url(url):
33
+ if url is None:
34
+ return False
35
+ split_section = url.split("://")
36
+ if len(split_section) < 2:
37
+ return False
38
+ else:
39
+ return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES
40
+
41
+
42
+ class RESTResponse(io.IOBase):
43
+
44
+ def __init__(self, resp) -> None:
45
+ self.urllib3_response = resp
46
+ self.status = resp.status
47
+ self.reason = resp.reason
48
+ self.data = resp.data
49
+
50
+ def getheaders(self):
51
+ """Returns a dictionary of the response headers."""
52
+ return self.urllib3_response.headers
53
+
54
+ def getheader(self, name, default=None):
55
+ """Returns a given response header."""
56
+ return self.urllib3_response.headers.get(name, default)
57
+
58
+
59
+ class RESTClientObject:
60
+
61
+ def __init__(self, configuration, pools_size=4, maxsize=None) -> None:
62
+ # urllib3.PoolManager will pass all kw parameters to connectionpool
63
+ # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/poolmanager.py#L75 # noqa: E501
64
+ # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/connectionpool.py#L680 # noqa: E501
65
+ # maxsize is the number of requests to host that are allowed in parallel # noqa: E501
66
+ # Custom SSL certificates and client certificates: http://urllib3.readthedocs.io/en/latest/advanced-usage.html # noqa: E501
67
+
68
+ # cert_reqs
69
+ if configuration.verify_ssl:
70
+ cert_reqs = ssl.CERT_REQUIRED
71
+ else:
72
+ cert_reqs = ssl.CERT_NONE
73
+
74
+ addition_pool_args = {}
75
+ if configuration.assert_hostname is not None:
76
+ addition_pool_args['assert_hostname'] = configuration.assert_hostname # noqa: E501
77
+
78
+ if configuration.retries is not None:
79
+ addition_pool_args['retries'] = configuration.retries
80
+
81
+ if configuration.tls_server_name:
82
+ addition_pool_args['server_hostname'] = configuration.tls_server_name
83
+
84
+
85
+ if configuration.socket_options is not None:
86
+ addition_pool_args['socket_options'] = configuration.socket_options
87
+
88
+ if maxsize is None:
89
+ if configuration.connection_pool_maxsize is not None:
90
+ maxsize = configuration.connection_pool_maxsize
91
+ else:
92
+ maxsize = 4
93
+
94
+ # https pool manager
95
+ if configuration.proxy:
96
+ if is_socks_proxy_url(configuration.proxy):
97
+ from urllib3.contrib.socks import SOCKSProxyManager
98
+ self.pool_manager = SOCKSProxyManager(
99
+ cert_reqs=cert_reqs,
100
+ ca_certs=configuration.ssl_ca_cert,
101
+ cert_file=configuration.cert_file,
102
+ key_file=configuration.key_file,
103
+ proxy_url=configuration.proxy,
104
+ headers=configuration.proxy_headers,
105
+ **addition_pool_args
106
+ )
107
+ else:
108
+ self.pool_manager = urllib3.ProxyManager(
109
+ num_pools=pools_size,
110
+ maxsize=maxsize,
111
+ cert_reqs=cert_reqs,
112
+ ca_certs=configuration.ssl_ca_cert,
113
+ cert_file=configuration.cert_file,
114
+ key_file=configuration.key_file,
115
+ proxy_url=configuration.proxy,
116
+ proxy_headers=configuration.proxy_headers,
117
+ **addition_pool_args
118
+ )
119
+ else:
120
+ self.pool_manager = urllib3.PoolManager(
121
+ num_pools=pools_size,
122
+ maxsize=maxsize,
123
+ cert_reqs=cert_reqs,
124
+ ca_certs=configuration.ssl_ca_cert,
125
+ cert_file=configuration.cert_file,
126
+ key_file=configuration.key_file,
127
+ **addition_pool_args
128
+ )
129
+
130
+ def request(self, method, url, query_params=None, headers=None,
131
+ body=None, post_params=None, _preload_content=True,
132
+ _request_timeout=None):
133
+ """Perform requests.
134
+
135
+ :param method: http request method
136
+ :param url: http request url
137
+ :param query_params: query parameters in the url
138
+ :param headers: http request headers
139
+ :param body: request json body, for `application/json`
140
+ :param post_params: request post parameters,
141
+ `application/x-www-form-urlencoded`
142
+ and `multipart/form-data`
143
+ :param _preload_content: if False, the urllib3.HTTPResponse object will
144
+ be returned without reading/decoding response
145
+ data. Default is True.
146
+ :param _request_timeout: timeout setting for this request. If one
147
+ number provided, it will be total request
148
+ timeout. It can also be a pair (tuple) of
149
+ (connection, read) timeouts.
150
+ """
151
+ method = method.upper()
152
+ assert method in ['GET', 'HEAD', 'DELETE', 'POST', 'PUT',
153
+ 'PATCH', 'OPTIONS']
154
+
155
+ if post_params and body:
156
+ raise ApiValueError(
157
+ "body parameter cannot be used with post_params parameter."
158
+ )
159
+
160
+ post_params = post_params or {}
161
+ headers = headers or {}
162
+ # url already contains the URL query string
163
+ # so reset query_params to empty dict
164
+ query_params = {}
165
+
166
+ timeout = None
167
+ if _request_timeout:
168
+ if isinstance(_request_timeout, (int,float)): # noqa: E501,F821
169
+ timeout = urllib3.Timeout(total=_request_timeout)
170
+ elif (isinstance(_request_timeout, tuple) and
171
+ len(_request_timeout) == 2):
172
+ timeout = urllib3.Timeout(
173
+ connect=_request_timeout[0], read=_request_timeout[1])
174
+
175
+ try:
176
+ # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`
177
+ if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']:
178
+
179
+ # no content type provided or payload is json
180
+ if not headers.get('Content-Type') or re.search('json', headers['Content-Type'], re.IGNORECASE):
181
+ request_body = None
182
+ if body is not None:
183
+ request_body = json.dumps(body)
184
+ r = self.pool_manager.request(
185
+ method, url,
186
+ body=request_body,
187
+ preload_content=_preload_content,
188
+ timeout=timeout,
189
+ headers=headers)
190
+ elif headers['Content-Type'] == 'application/x-www-form-urlencoded': # noqa: E501
191
+ r = self.pool_manager.request(
192
+ method, url,
193
+ fields=post_params,
194
+ encode_multipart=False,
195
+ preload_content=_preload_content,
196
+ timeout=timeout,
197
+ headers=headers)
198
+ elif headers['Content-Type'] == 'multipart/form-data':
199
+ # must del headers['Content-Type'], or the correct
200
+ # Content-Type which generated by urllib3 will be
201
+ # overwritten.
202
+ del headers['Content-Type']
203
+ r = self.pool_manager.request(
204
+ method, url,
205
+ fields=post_params,
206
+ encode_multipart=True,
207
+ preload_content=_preload_content,
208
+ timeout=timeout,
209
+ headers=headers)
210
+ # Pass a `string` parameter directly in the body to support
211
+ # other content types than Json when `body` argument is
212
+ # provided in serialized form
213
+ elif isinstance(body, str) or isinstance(body, bytes):
214
+ request_body = body
215
+ r = self.pool_manager.request(
216
+ method, url,
217
+ body=request_body,
218
+ preload_content=_preload_content,
219
+ timeout=timeout,
220
+ headers=headers)
221
+ else:
222
+ # Cannot generate the request from given parameters
223
+ msg = """Cannot prepare a request message for provided
224
+ arguments. Please check that your arguments match
225
+ declared content type."""
226
+ raise ApiException(status=0, reason=msg)
227
+ # For `GET`, `HEAD`
228
+ else:
229
+ r = self.pool_manager.request(method, url,
230
+ fields={},
231
+ preload_content=_preload_content,
232
+ timeout=timeout,
233
+ headers=headers)
234
+ except urllib3.exceptions.SSLError as e:
235
+ msg = "{0}\n{1}".format(type(e).__name__, str(e))
236
+ raise ApiException(status=0, reason=msg)
237
+
238
+ if _preload_content:
239
+ r = RESTResponse(r)
240
+
241
+ # log response body
242
+ logger.debug("response body: %s", r.data)
243
+
244
+ if not 200 <= r.status <= 299:
245
+ if r.status == 400:
246
+ raise BadRequestException(http_resp=r)
247
+
248
+ if r.status == 401:
249
+ raise UnauthorizedException(http_resp=r)
250
+
251
+ if r.status == 403:
252
+ raise ForbiddenException(http_resp=r)
253
+
254
+ if r.status == 404:
255
+ raise NotFoundException(http_resp=r)
256
+
257
+ if 500 <= r.status <= 599:
258
+ raise ServiceException(http_resp=r)
259
+
260
+ raise ApiException(http_resp=r)
261
+
262
+ return r
263
+
264
+ def get_request(self, url, headers=None, query_params=None, _preload_content=True,
265
+ _request_timeout=None):
266
+ return self.request("GET", url,
267
+ headers=headers,
268
+ _preload_content=_preload_content,
269
+ _request_timeout=_request_timeout,
270
+ query_params=query_params)
271
+
272
+ def head_request(self, url, headers=None, query_params=None, _preload_content=True,
273
+ _request_timeout=None):
274
+ return self.request("HEAD", url,
275
+ headers=headers,
276
+ _preload_content=_preload_content,
277
+ _request_timeout=_request_timeout,
278
+ query_params=query_params)
279
+
280
+ def options_request(self, url, headers=None, query_params=None, post_params=None,
281
+ body=None, _preload_content=True, _request_timeout=None):
282
+ return self.request("OPTIONS", url,
283
+ headers=headers,
284
+ query_params=query_params,
285
+ post_params=post_params,
286
+ _preload_content=_preload_content,
287
+ _request_timeout=_request_timeout,
288
+ body=body)
289
+
290
+ def delete_request(self, url, headers=None, query_params=None, body=None,
291
+ _preload_content=True, _request_timeout=None):
292
+ return self.request("DELETE", url,
293
+ headers=headers,
294
+ query_params=query_params,
295
+ _preload_content=_preload_content,
296
+ _request_timeout=_request_timeout,
297
+ body=body)
298
+
299
+ def post_request(self, url, headers=None, query_params=None, post_params=None,
300
+ body=None, _preload_content=True, _request_timeout=None):
301
+ return self.request("POST", url,
302
+ headers=headers,
303
+ query_params=query_params,
304
+ post_params=post_params,
305
+ _preload_content=_preload_content,
306
+ _request_timeout=_request_timeout,
307
+ body=body)
308
+
309
+ def put_request(self, url, headers=None, query_params=None, post_params=None,
310
+ body=None, _preload_content=True, _request_timeout=None):
311
+ return self.request("PUT", url,
312
+ headers=headers,
313
+ query_params=query_params,
314
+ post_params=post_params,
315
+ _preload_content=_preload_content,
316
+ _request_timeout=_request_timeout,
317
+ body=body)
318
+
319
+ def patch_request(self, url, headers=None, query_params=None, post_params=None,
320
+ body=None, _preload_content=True, _request_timeout=None):
321
+ return self.request("PATCH", url,
322
+ headers=headers,
323
+ query_params=query_params,
324
+ post_params=post_params,
325
+ _preload_content=_preload_content,
326
+ _request_timeout=_request_timeout,
327
+ body=body)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Weheat B.V.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.2
2
+ Name: weheat
3
+ Version: 2025.1.14rc1
4
+ Summary: Weheat Backend client
5
+ Home-page: https://github.com/wefabricate/wh-python
6
+ Author: Jesper Raemaekers
7
+ Author-email: jesper.raemaekers@wefabricate.com
8
+ License: MIT
9
+ Keywords: OpenAPI,OpenAPI-Generator,Weheat Backend
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: urllib3<2.1.0,>=1.25.3
13
+ Requires-Dist: python-dateutil
14
+ Requires-Dist: pydantic<3,>=1.10.5
15
+ Requires-Dist: aenum
16
+ Requires-Dist: aiohttp
17
+ Dynamic: author
18
+ Dynamic: author-email
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: keywords
23
+ Dynamic: license
24
+ Dynamic: requires-dist
25
+ Dynamic: summary
26
+
27
+ # Weheat backend client
28
+
29
+ This is a client for the Weheat backend. It is automatically generated from the OpenAPI specification.
30
+
31
+ ## Requirements.
32
+
33
+ Python 3.7+
34
+
35
+ ## Installation & Usage
36
+
37
+ You can install directly using:
38
+
39
+ ```sh
40
+ pip install weheat
41
+ ```
42
+
43
+ Then import the package:
44
+
45
+ ```python
46
+ import weheat
47
+ ```
48
+
49
+
50
+
51
+ ## Getting Started
52
+
53
+ After installation, you can now use the client to interact with the Weheat backend.
54
+
55
+ ```python
56
+ import datetime
57
+ from keycloak import KeycloakOpenID # install with pip install python-keycloak
58
+ from weheat import ApiClient, Configuration, HeatPumpApi, HeatPumpLogApi, EnergyLogApi
59
+
60
+ # input your information here
61
+ auth_url = 'https://auth.weheat.nl/auth/'
62
+ api_url = 'https://api.weheat.nl'
63
+ realm_name = 'Weheat'
64
+ my_client_id = 'WeheatCommunityAPI' # client ID and secret provided by Weheat
65
+ my_client_secret = ''
66
+ username = '' # username and password used for the online portal
67
+ password = ''
68
+ my_heat_pump_id = '' # your heat pump UUID
69
+
70
+ # Get the access token from keycloak
71
+ keycloak_open_id = KeycloakOpenID(server_url=auth_url,
72
+ client_id=my_client_id,
73
+ realm_name=realm_name,
74
+ client_secret_key=my_client_secret)
75
+
76
+ token_response = keycloak_open_id.token(username, password)
77
+ keycloak_open_id.logout(token_response['refresh_token'])
78
+
79
+ # Create the cinfiguration object
80
+ config = Configuration(host=api_url, access_token=token_response['access_token'])
81
+
82
+ # with the client the APIs can be accessed
83
+ with ApiClient(configuration=config) as client:
84
+ # Getting all heat pumps that the user has access to
85
+ response = HeatPumpApi(client).api_v1_heat_pumps_get_with_http_info()
86
+
87
+ if response.status_code == 200:
88
+ print(f'My heat pump: {response.data}')
89
+
90
+ # Getting the latest log of the heat pump
91
+ response = HeatPumpLogApi(client).api_v1_heat_pumps_heat_pump_id_logs_latest_get_with_http_info(
92
+ heat_pump_id=my_heat_pump_id)
93
+
94
+ if response.status_code == 200:
95
+ print(f'My heat pump logs: {response.data}')
96
+
97
+ # Getting the energy logs of the heat pump in a specific period
98
+ # interval can be "Minute", "FiveMinute", "FifteenMinute", "Hour", "Day", "Week", "Month", "Year"
99
+ response = EnergyLogApi(client).api_v1_energy_logs_heat_pump_id_get_with_http_info(heat_pump_id=my_heat_pump_id,
100
+ start_time=datetime.datetime(
101
+ 2024, 6,
102
+ 22, 0, 0,
103
+ 0),
104
+ end_time=datetime.datetime(2024,
105
+ 6, 22,
106
+ 15, 0,
107
+ 0),
108
+ interval='Hour')
109
+
110
+ if response.status_code == 200:
111
+ print(f'My energy logs: {response.data}')
112
+
113
+
114
+ ```
115
+
116
+
117
+
@@ -0,0 +1,36 @@
1
+ weheat/__init__.py,sha256=dk7tMBQ9jtkNiWeZW5xWCJoaGIrZVQ-tm5NCZhi2qWU,1440
2
+ weheat/api_client.py,sha256=QNUo_W5MMBajpjN2sWFUIRhOszAE93KECNxdvhVns84,29516
3
+ weheat/api_response.py,sha256=n-M3QVNIkmh-cQazukKeiw_nuvVpWBPAMgr2ryj7vaM,938
4
+ weheat/configuration.py,sha256=FerGmYva4hSzbtPV8hASrSF4Fd1zRIFezPzBhHQHV5c,14562
5
+ weheat/exceptions.py,sha256=P4L9gEdzeIo8YMAh27FeP_-55lEgV1TUFTKq4dmqTVQ,5365
6
+ weheat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ weheat/rest.py,sha256=hLgJ0CYAR1Dr_LppodDNNC_chj5hEQUmzDmbxvID1ao,13808
8
+ weheat/abstractions/__init__.py,sha256=cRdA_kyTIooo39I13_mqShSfZMqdzNGHbmrnITqgx6A,161
9
+ weheat/abstractions/auth.py,sha256=VCAxJ4OIj7bsYttqJl5-juU0VUlSd3xPu7kUjtHZr3U,979
10
+ weheat/abstractions/discovery.py,sha256=yCO_YtyGvr4rolLAc1KNfhojcFuUrbZdSsYJ8fSD7l4,2352
11
+ weheat/abstractions/heat_pump.py,sha256=fOfj3d2yhmZYkrbvmW5mHmajtarIYuzX48N0fF4xTgg,10014
12
+ weheat/abstractions/user.py,sha256=YLS8C3hTJU5tjVcTw18vVlF6Z0l8E9SX_Gcyo9ylm7g,727
13
+ weheat/api/__init__.py,sha256=DQnnRs5Z29Nf5sGdFd3f96xM6p_FMym-_-dvQC2VzdU,243
14
+ weheat/api/energy_log_api.py,sha256=p1JB9BxWch86c2GAJ6Q0OSNxJjaUY9-uR96cjAUU3Xo,11431
15
+ weheat/api/heat_pump_api.py,sha256=2k0FkxAJbnFJaGx6zYTleaHLgeNBpu9wsyeBDRmIEbs,24436
16
+ weheat/api/heat_pump_log_api.py,sha256=vbc10M4_6jDvFP5Bs-goLtbHjHf4Ve0w3GPOOuIXCiA,28658
17
+ weheat/api/user_api.py,sha256=V2QrdGya7kMJiODqUfz4iB8UquGUajF3O4PGBtIyT9c,7823
18
+ weheat/models/__init__.py,sha256=VJsiySw4ndbqm5WCTxehHg-gmjaB_m2czi2FO-AQZn0,766
19
+ weheat/models/boiler_type.py,sha256=jZF4sXhR7SEhj3bEfvmeH48l1APu_gtSlpQUj45vP7o,906
20
+ weheat/models/device_state.py,sha256=Di_i-8IOWQaK25eYGNsgXv1OZh9s8RAH-6vcdacAObo,1035
21
+ weheat/models/dhw_type.py,sha256=IamPcF02M_BDadVxfD7poh1C00mPXTJ9Wbjwh6TUfV8,817
22
+ weheat/models/energy_view_dto.py,sha256=bNxtr29Yob06ffa-yB8Y8hAeK_pAaPp65Pr7tw3jFN0,8856
23
+ weheat/models/heat_pump_log_view_dto.py,sha256=ryyyK7VaWEANJaMV10kw9m0VDbMI7G4fyI447Wal6o4,63897
24
+ weheat/models/heat_pump_model.py,sha256=8E4VhmRZwLh6I_WfckpWMQ2GbQDZqyszimc58ODOaf0,1079
25
+ weheat/models/heat_pump_status_enum.py,sha256=rv8xkSvnbegS6EQMNL-tayA_-4sr1rIkS6Hoq3yoHCA,1017
26
+ weheat/models/heat_pump_type.py,sha256=Kft4cXvenK27miFDcsBOHv7p2svAW18iptT_GUcXWeU,818
27
+ weheat/models/raw_heat_pump_log_dto.py,sha256=-GG-hVvSRE-iikJ3QpBpWdRWjvh8_Jfz5Vkv1VwIhtQ,34507
28
+ weheat/models/read_all_heat_pump_dto.py,sha256=SSuXCZ6ycQixGE4HX9l7NJD6vEEmupngZGCJZ1qmUMU,5181
29
+ weheat/models/read_heat_pump_dto.py,sha256=ETXsM2imYycxDbk8-6R4_wvR0_LyLgsgNKZO14R2oA8,4607
30
+ weheat/models/read_user_dto.py,sha256=S5UbvBtTGmMybsJMsKznrVGaUuJFRtFbMME8n7assR8,3550
31
+ weheat/models/role.py,sha256=eF6nawkz8mmCGQEmJx26Y2MPFmlKdpOOtJ2Q70b-Qtc,938
32
+ weheat-2025.1.14rc1.dist-info/LICENSE,sha256=rWmFUq0uth2jpet-RQ2QPd2VhZkcPSUs6Dxfmbqkbis,1068
33
+ weheat-2025.1.14rc1.dist-info/METADATA,sha256=SYaYEnrtYS7KcMS82zuq6bMX9Z6jZgrAfRj133hnb7o,4112
34
+ weheat-2025.1.14rc1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
35
+ weheat-2025.1.14rc1.dist-info/top_level.txt,sha256=hLzdyvGZ9rs4AqK7U48mdHx_-FcP5sDuTSleDUvGAZw,7
36
+ weheat-2025.1.14rc1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ weheat