freeplay 0.2.30__py3-none-any.whl → 0.2.32__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.
freeplay/record.py CHANGED
@@ -1,14 +1,18 @@
1
1
  import logging
2
2
  from dataclasses import dataclass
3
- from typing import Any, Dict, Optional
3
+ from typing import Dict, Optional, Union
4
4
  from abc import abstractmethod, ABC
5
+ from dataclasses import dataclass
6
+ from typing import Optional
5
7
 
6
- from .llm_parameters import LLMParameters
7
- from .completions import PromptTemplateWithMetadata
8
8
  from . import api_support
9
+ from .completions import PromptTemplateWithMetadata
10
+ from .llm_parameters import LLMParameters
11
+ from .model import InputVariables
9
12
 
10
13
  logger = logging.getLogger(__name__)
11
14
 
15
+
12
16
  @dataclass
13
17
  class RecordCallFields:
14
18
  completion_content: str
@@ -18,13 +22,15 @@ class RecordCallFields:
18
22
  session_id: str
19
23
  start: float
20
24
  target_template: PromptTemplateWithMetadata
21
- variables: Dict[str, str]
25
+ variables: InputVariables
22
26
  tag: str
23
27
  test_run_id: Optional[str]
28
+ test_case_id: Optional[str]
24
29
  record_format_type: Optional[str]
25
30
  model: Optional[str]
26
31
  provider: Optional[str]
27
32
  llm_parameters: Optional[LLMParameters]
33
+ custom_metadata: Optional[Dict[str, Union[str,int,float]]] = None
28
34
 
29
35
 
30
36
  class RecordProcessor(ABC):
@@ -35,6 +41,7 @@ class RecordProcessor(ABC):
35
41
  ) -> None:
36
42
  pass
37
43
 
44
+
38
45
  class NoOpRecorder(RecordProcessor):
39
46
  def record_call(
40
47
  self,
@@ -42,8 +49,10 @@ class NoOpRecorder(RecordProcessor):
42
49
  ) -> None:
43
50
  pass
44
51
 
52
+
45
53
  no_op_recorder = NoOpRecorder()
46
54
 
55
+
47
56
  class DefaultRecordProcessor(RecordProcessor):
48
57
 
49
58
  def __init__(
@@ -55,8 +64,8 @@ class DefaultRecordProcessor(RecordProcessor):
55
64
  self.freeplay_api_key = freeplay_api_key
56
65
 
57
66
  def record_call(
58
- self,
59
- record_call: RecordCallFields
67
+ self,
68
+ record_call: RecordCallFields
60
69
  ) -> None:
61
70
  record_payload = {
62
71
  "session_id": record_call.session_id,
@@ -72,12 +81,16 @@ class DefaultRecordProcessor(RecordProcessor):
72
81
  "is_complete": record_call.completion_is_complete,
73
82
  "model": record_call.model,
74
83
  "provider": record_call.provider,
75
- "llm_parameters": record_call.llm_parameters
84
+ "llm_parameters": record_call.llm_parameters,
85
+ "custom_metadata": record_call.custom_metadata,
76
86
  }
77
87
 
78
88
  if record_call.test_run_id is not None:
79
89
  record_payload['test_run_id'] = record_call.test_run_id
80
90
 
91
+ if record_call.test_case_id is not None:
92
+ record_payload['test_case_id'] = record_call.test_case_id
93
+
81
94
  try:
82
95
  recorded_response = api_support.post_raw(
83
96
  api_key=self.freeplay_api_key,
freeplay/support.py ADDED
@@ -0,0 +1,316 @@
1
+ import json
2
+ import time
3
+ from copy import copy
4
+ from typing import Dict, Any, Optional, Union, List, Generator
5
+ from uuid import uuid4
6
+
7
+ from freeplay import api_support
8
+ from freeplay.api_support import try_decode
9
+ from freeplay.completions import PromptTemplates, PromptTemplateWithMetadata, ChatMessage, ChatCompletionResponse, \
10
+ CompletionChunk, CompletionResponse
11
+ from freeplay.errors import FreeplayConfigurationError, freeplay_response_error, FreeplayServerError
12
+ from freeplay.flavors import ChatFlavor, Flavor, pick_flavor_from_config
13
+ from freeplay.llm_parameters import LLMParameters
14
+ from freeplay.model import InputVariables
15
+ from freeplay.provider_config import ProviderConfig
16
+ from freeplay.record import RecordProcessor, RecordCallFields
17
+
18
+ JsonDom = Dict[str, Any]
19
+
20
+
21
+ class TestCaseTestRunResponse:
22
+ def __init__(self, test_case: JsonDom):
23
+ self.variables: InputVariables = test_case['variables']
24
+ self.id: str = test_case['id']
25
+
26
+
27
+ class TestRunResponse:
28
+ def __init__(
29
+ self,
30
+ test_run_id: str,
31
+ test_cases: list[JsonDom]
32
+ ):
33
+ self.test_cases = [
34
+ TestCaseTestRunResponse(test_case)
35
+ for test_case in test_cases
36
+ ]
37
+ self.test_run_id = test_run_id
38
+
39
+
40
+ class CallSupport:
41
+ def __init__(
42
+ self,
43
+ freeplay_api_key: str,
44
+ api_base: str,
45
+ record_processor: RecordProcessor,
46
+ **kwargs: Any
47
+ ) -> None:
48
+ self.api_base = api_base
49
+ self.freeplay_api_key = freeplay_api_key
50
+ self.client_params = LLMParameters(kwargs)
51
+ self.record_processor = record_processor
52
+
53
+ @staticmethod
54
+ def find_template_by_name(prompts: PromptTemplates, template_name: str) -> PromptTemplateWithMetadata:
55
+ templates = [t for t in prompts.templates if t.name == template_name]
56
+ if len(templates) == 0:
57
+ raise FreeplayConfigurationError(f'Could not find template with name "{template_name}"')
58
+ return templates[0]
59
+
60
+ def create_session_id(self) -> str:
61
+ return str(uuid4())
62
+
63
+ def check_all_values_string_or_number(self, metadata: Optional[Dict[str, Union[str, int, float]]]) -> None:
64
+ if metadata:
65
+ for key, value in metadata.items():
66
+ if not isinstance(value, (str, int, float)):
67
+ raise FreeplayConfigurationError(f"Invalid value for key {key}: Value must be a string or number.")
68
+
69
+ def get_prompt(self, project_id: str, template_name: str, environment: str) -> PromptTemplateWithMetadata:
70
+ prompt_templates = self.get_prompts(project_id, environment)
71
+ return self.find_template_by_name(prompt_templates, template_name)
72
+
73
+ def get_prompts(self, project_id: str, tag: str) -> PromptTemplates:
74
+ response = api_support.get_raw(
75
+ api_key=self.freeplay_api_key,
76
+ url=f'{self.api_base}/projects/{project_id}/templates/all/{tag}'
77
+ )
78
+
79
+ if response.status_code != 200:
80
+ raise freeplay_response_error("Error getting prompt templates", response)
81
+
82
+ maybe_prompts = try_decode(PromptTemplates, response.content)
83
+ if maybe_prompts is None:
84
+ raise FreeplayServerError(f'Failed to parse prompt templates from server')
85
+
86
+ return maybe_prompts
87
+
88
+ def create_test_run(self, project_id: str, testlist: str) -> TestRunResponse:
89
+ response = api_support.post_raw(
90
+ api_key=self.freeplay_api_key,
91
+ url=f'{self.api_base}/projects/{project_id}/test-runs-cases',
92
+ payload={'testlist_name': testlist},
93
+ )
94
+
95
+ if response.status_code != 201:
96
+ raise freeplay_response_error('Error while creating a test run.', response)
97
+
98
+ json_dom = response.json()
99
+
100
+ return TestRunResponse(json_dom['test_run_id'], json_dom['test_cases'])
101
+
102
+ # noinspection PyUnboundLocalVariable
103
+ def prepare_and_make_chat_call(
104
+ self,
105
+ session_id: str,
106
+ flavor: ChatFlavor,
107
+ provider_config: ProviderConfig,
108
+ tag: str,
109
+ target_template: PromptTemplateWithMetadata,
110
+ variables: InputVariables,
111
+ message_history: List[ChatMessage],
112
+ new_messages: Optional[List[ChatMessage]],
113
+ test_run_id: Optional[str] = None,
114
+ completion_parameters: Optional[LLMParameters] = None,
115
+ metadata: Optional[Dict[str, Union[str, int, float]]] = None
116
+ ) -> ChatCompletionResponse:
117
+ # make call
118
+ start = time.time()
119
+ params = target_template.get_params() \
120
+ .merge_and_override(self.client_params) \
121
+ .merge_and_override(completion_parameters)
122
+ prompt_messages = copy(message_history)
123
+ if new_messages is not None:
124
+ prompt_messages.extend(new_messages)
125
+ completion_response = flavor.continue_chat(messages=prompt_messages,
126
+ provider_config=provider_config,
127
+ llm_parameters=params)
128
+ end = time.time()
129
+
130
+ model = flavor.get_model_params(params).get('model')
131
+ formatted_prompt = json.dumps(prompt_messages)
132
+ # record data
133
+ record_call_fields = RecordCallFields(
134
+ completion_content=completion_response.content,
135
+ completion_is_complete=completion_response.is_complete,
136
+ end=end,
137
+ formatted_prompt=formatted_prompt,
138
+ session_id=session_id,
139
+ start=start,
140
+ target_template=target_template,
141
+ variables=variables,
142
+ record_format_type=flavor.record_format_type,
143
+ tag=tag,
144
+ test_run_id=test_run_id,
145
+ test_case_id=None,
146
+ model=model,
147
+ provider=flavor.provider,
148
+ llm_parameters=params,
149
+ custom_metadata=metadata,
150
+ )
151
+ self.record_processor.record_call(record_call_fields)
152
+
153
+ return completion_response
154
+
155
+ # noinspection PyUnboundLocalVariable
156
+ def prepare_and_make_chat_call_stream(
157
+ self,
158
+ session_id: str,
159
+ flavor: ChatFlavor,
160
+ provider_config: ProviderConfig,
161
+ tag: str,
162
+ target_template: PromptTemplateWithMetadata,
163
+ variables: InputVariables,
164
+ message_history: List[ChatMessage],
165
+ test_run_id: Optional[str] = None,
166
+ completion_parameters: Optional[LLMParameters] = None,
167
+ metadata: Optional[Dict[str, Union[str, int, float]]] = None
168
+ ) -> Generator[CompletionChunk, None, None]:
169
+ # make call
170
+ start = time.time()
171
+ prompt_messages = copy(message_history)
172
+ params = target_template.get_params() \
173
+ .merge_and_override(self.client_params) \
174
+ .merge_and_override(completion_parameters)
175
+ completion_response = flavor.continue_chat_stream(prompt_messages, provider_config, llm_parameters=params)
176
+
177
+ str_content = ''
178
+ last_is_complete = False
179
+ for chunk in completion_response:
180
+ str_content += chunk.text or ''
181
+ last_is_complete = chunk.is_complete
182
+ yield chunk
183
+ # End time must be logged /after/ streaming the response above, or else OpenAI latency will not be captured.
184
+ end = time.time()
185
+
186
+ model = flavor.get_model_params(params).get('model')
187
+ formatted_prompt = json.dumps(prompt_messages)
188
+ record_call_fields = RecordCallFields(
189
+ completion_content=str_content,
190
+ completion_is_complete=last_is_complete,
191
+ end=end,
192
+ formatted_prompt=formatted_prompt,
193
+ session_id=session_id,
194
+ start=start,
195
+ target_template=target_template,
196
+ variables=variables,
197
+ record_format_type=flavor.record_format_type,
198
+ tag=tag,
199
+ test_run_id=test_run_id,
200
+ test_case_id=None,
201
+ model=model,
202
+ provider=flavor.provider,
203
+ llm_parameters=params,
204
+ custom_metadata=metadata,
205
+ )
206
+ self.record_processor.record_call(record_call_fields)
207
+
208
+ # noinspection PyUnboundLocalVariable
209
+ def prepare_and_make_call(
210
+ self,
211
+ session_id: str,
212
+ prompts: PromptTemplates,
213
+ template_name: str,
214
+ variables: InputVariables,
215
+ flavor: Optional[Flavor],
216
+ provider_config: ProviderConfig,
217
+ tag: str,
218
+ test_run_id: Optional[str] = None,
219
+ completion_parameters: Optional[LLMParameters] = None,
220
+ metadata: Optional[Dict[str, Union[str, int, float]]] = None
221
+ ) -> CompletionResponse:
222
+ target_template = self.find_template_by_name(prompts, template_name)
223
+ params = target_template.get_params() \
224
+ .merge_and_override(self.client_params) \
225
+ .merge_and_override(completion_parameters)
226
+
227
+ final_flavor = pick_flavor_from_config(flavor, target_template.flavor_name)
228
+ formatted_prompt = final_flavor.format(target_template, variables)
229
+
230
+ # make call
231
+ start = time.time()
232
+ completion_response = final_flavor.call_service(formatted_prompt=formatted_prompt,
233
+ provider_config=provider_config,
234
+ llm_parameters=params)
235
+ end = time.time()
236
+
237
+ model = final_flavor.get_model_params(params).get('model')
238
+
239
+ # record data
240
+ record_call_fields = RecordCallFields(
241
+ completion_content=completion_response.content,
242
+ completion_is_complete=completion_response.is_complete,
243
+ end=end,
244
+ formatted_prompt=formatted_prompt,
245
+ session_id=session_id,
246
+ start=start,
247
+ target_template=target_template,
248
+ variables=variables,
249
+ record_format_type=final_flavor.record_format_type,
250
+ tag=tag,
251
+ test_run_id=test_run_id,
252
+ test_case_id=None,
253
+ model=model,
254
+ provider=final_flavor.provider,
255
+ llm_parameters=params,
256
+ custom_metadata=metadata,
257
+ )
258
+ self.record_processor.record_call(record_call_fields)
259
+
260
+ return completion_response
261
+
262
+ def prepare_and_make_call_stream(
263
+ self,
264
+ session_id: str,
265
+ prompts: PromptTemplates,
266
+ template_name: str,
267
+ variables: InputVariables,
268
+ flavor: Optional[Flavor],
269
+ provider_config: ProviderConfig,
270
+ tag: str,
271
+ test_run_id: Optional[str] = None,
272
+ completion_parameters: Optional[LLMParameters] = None,
273
+ metadata: Optional[Dict[str, Union[str, int, float]]] = None
274
+ ) -> Generator[CompletionChunk, None, None]:
275
+ target_template = self.find_template_by_name(prompts, template_name)
276
+ params = target_template.get_params() \
277
+ .merge_and_override(self.client_params) \
278
+ .merge_and_override(completion_parameters)
279
+
280
+ final_flavor = pick_flavor_from_config(flavor, target_template.flavor_name)
281
+ formatted_prompt = final_flavor.format(target_template, variables)
282
+
283
+ # make call
284
+ start = int(time.time())
285
+ completion_response = final_flavor.call_service_stream(
286
+ formatted_prompt=formatted_prompt, provider_config=provider_config, llm_parameters=params)
287
+ text_chunks = []
288
+ last_is_complete = False
289
+ for chunk in completion_response:
290
+ text_chunks.append(chunk.text)
291
+ last_is_complete = chunk.is_complete
292
+ yield chunk
293
+ # End time must be logged /after/ streaming the response above, or else OpenAI latency will not be captured.
294
+ end = int(time.time())
295
+
296
+ model = final_flavor.get_model_params(params).get('model')
297
+
298
+ record_call_fields = RecordCallFields(
299
+ completion_content=''.join(text_chunks),
300
+ completion_is_complete=last_is_complete,
301
+ end=end,
302
+ formatted_prompt=formatted_prompt,
303
+ session_id=session_id,
304
+ start=start,
305
+ target_template=target_template,
306
+ variables=variables,
307
+ record_format_type=final_flavor.record_format_type,
308
+ tag=tag,
309
+ test_run_id=test_run_id,
310
+ test_case_id=None,
311
+ model=model,
312
+ provider=final_flavor.provider,
313
+ llm_parameters=params,
314
+ custom_metadata=metadata,
315
+ )
316
+ self.record_processor.record_call(record_call_fields)
freeplay/utils.py CHANGED
@@ -1,21 +1,51 @@
1
+ from typing import Dict, Union, Optional
2
+ import importlib.metadata
3
+ import platform
4
+
1
5
  import pystache # type: ignore
2
- from pydantic import RootModel, ValidationError
3
- from typing import Any, Dict, List, Union
6
+ from pydantic import ValidationError
4
7
 
5
- from .errors import FreeplayError
8
+ from .errors import FreeplayError, FreeplayConfigurationError
9
+ from .model import PydanticInputVariables, InputVariables
6
10
 
7
- InputVariable = RootModel[Union[Dict[str, "InputVariable"], List["InputVariable"], str, int, bool, float]]
8
- InputVariable.model_rebuild()
9
- InputVariables = Dict[str, Union[str, int, bool, Dict[str, Any], List[Any]]]
10
- PydanticInputVariables = RootModel[Dict[str, InputVariable]]
11
11
 
12
- def format_template_variables(template: str, variables: Any) -> str:
12
+ def bind_template_variables(template: str, variables: InputVariables) -> str:
13
13
  # Validate that the variables are of the correct type, and do not include functions or None values.
14
14
  try:
15
15
  PydanticInputVariables.model_validate(variables)
16
16
  except ValidationError as err:
17
- raise FreeplayError('Variables must be a string, number, bool, or a possibly nested list or dict of strings, numbers and booleans.')
17
+ raise FreeplayError(
18
+ 'Variables must be a string, number, bool, or a possibly nested'
19
+ ' list or dict of strings, numbers and booleans.'
20
+ )
18
21
 
19
22
  # When rendering mustache, do not escape HTML special characters.
20
23
  rendered: str = pystache.Renderer(escape=lambda s: s).render(template, variables)
21
24
  return rendered
25
+
26
+
27
+ def check_all_values_string_or_number(metadata: Optional[Dict[str, Union[str, int, float]]]) -> None:
28
+ if metadata:
29
+ for key, value in metadata.items():
30
+ if not isinstance(value, (str, int, float)):
31
+ raise FreeplayConfigurationError(f"Invalid value for key {key}: Value must be a string or number.")
32
+
33
+
34
+ def build_request_header(api_key: str) -> Dict[str, str]:
35
+ return {
36
+ 'Authorization': f'Bearer {api_key}',
37
+ 'User-Agent': get_user_agent()
38
+ }
39
+
40
+
41
+ def get_user_agent() -> str:
42
+ sdk_name = 'Freeplay'
43
+ sdk_version = importlib.metadata.version('Freeplay')
44
+ language = 'Python'
45
+ language_version = platform.python_version()
46
+ os_name = platform.system()
47
+ os_version = platform.release()
48
+
49
+ # Output format
50
+ # Freeplay/0.2.30 (Python/3.11.4; Darwin/23.2.0)
51
+ return f"{sdk_name}/{sdk_version} ({language}/{language_version}; {os_name}/{os_version})"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: freeplay
3
- Version: 0.2.30
3
+ Version: 0.2.32
4
4
  Summary:
5
5
  License: MIT
6
6
  Author: FreePlay Engineering
@@ -0,0 +1,20 @@
1
+ freeplay/__init__.py,sha256=74A9S9hmLq9BNHsdx0-37yDxlSukudNl9bJ0TE60Z30,61
2
+ freeplay/api_support.py,sha256=E4Mxa3Lx31TEw_X6o4s9eAR1TSxs0PhFYYaWax6dH2I,2027
3
+ freeplay/completions.py,sha256=F1rMBtQaCtn0rBQqvCurkV25g8gLtwnEod5rRvf-txY,1176
4
+ freeplay/errors.py,sha256=bPqsw32YX-xSr7O-G49M0sSFF7mq-YF1WGq928UV47s,631
5
+ freeplay/flavors.py,sha256=XroBKT8Nf92VTsuC261Nceo9f9stkkpC5CXbi8y4NEs,17236
6
+ freeplay/freeplay.py,sha256=BLlr4YXh3a624xcM93KvZGsiIGSMrIMFM07lLyVCIao,17089
7
+ freeplay/freeplay_cli.py,sha256=nasbc_ckSr5-YtUKfg_w-6X1geQZ9s5u79VzRULGsbs,3868
8
+ freeplay/freeplay_thin.py,sha256=WHoVCkS30mDM7ghjDadgfj48BNL5EvaXktUsGfeXlIY,867
9
+ freeplay/llm_parameters.py,sha256=bQbfuC8EICF0XMZQa5pwI3FkQqxmCUVqHO3gYHy3Tg8,898
10
+ freeplay/model.py,sha256=kVARXUEJKbhOdWz7T8eR7npTPXlpaY9wWaEzJvbIvOU,534
11
+ freeplay/provider_config.py,sha256=hruf3Khusrwb76_-hv7ouuxmvJuaRyC1UxIw7XlJx8A,1416
12
+ freeplay/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ freeplay/record.py,sha256=_0uo0rUFZz8kwgWC0JhxFg13JyXgMfAVCkzZsuRLzBA,3354
14
+ freeplay/support.py,sha256=OOnUZOMYnLSEG_3toycWfHb2XZ8meF77uQRllYexXeo,12616
15
+ freeplay/utils.py,sha256=cRCCIzVqWNDKlTI-DDhXGyCkplbd-X4qzDs__aUpvww,1840
16
+ freeplay-0.2.32.dist-info/LICENSE,sha256=_jzIw45hB1XHGxiQ8leZ0GH_X7bR_a8qgxaqnHbCUOo,1064
17
+ freeplay-0.2.32.dist-info/METADATA,sha256=o9MiS7BrkV0M2Erp7bKomeRUBgveiqW6XQf7LLyVA4s,1633
18
+ freeplay-0.2.32.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
19
+ freeplay-0.2.32.dist-info/entry_points.txt,sha256=32s3rf2UUCqiJT4jnClEXZhdXlvl30uwpcxz-Gsy4UU,54
20
+ freeplay-0.2.32.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.7.0
2
+ Generator: poetry-core 1.6.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,17 +0,0 @@
1
- freeplay/__init__.py,sha256=74A9S9hmLq9BNHsdx0-37yDxlSukudNl9bJ0TE60Z30,61
2
- freeplay/api_support.py,sha256=FJCm7yb1JhY0Z1Hn-JIVM7cCfBvWfCrqMQHRzzBzGLE,2012
3
- freeplay/completions.py,sha256=JQtwe5THpkWqywUvOLyQFNa9t8kzNhIVkJni67U3HyI,1165
4
- freeplay/errors.py,sha256=bPqsw32YX-xSr7O-G49M0sSFF7mq-YF1WGq928UV47s,631
5
- freeplay/flavors.py,sha256=AUVAjZaP8VtK5F3NtQOa7_LD9YIs84gUafbUIqt6U84,18492
6
- freeplay/freeplay.py,sha256=ObfW0QnSzuagfF5aMC44NPAZIokED3BUALuhqUY5-SA,28760
7
- freeplay/freeplay_cli.py,sha256=nasbc_ckSr5-YtUKfg_w-6X1geQZ9s5u79VzRULGsbs,3868
8
- freeplay/freeplay_thin.py,sha256=2YFfqFCYimUboRg_umasvHqi2-tzWTvpuf5b9-mQK3I,827
9
- freeplay/llm_parameters.py,sha256=bQbfuC8EICF0XMZQa5pwI3FkQqxmCUVqHO3gYHy3Tg8,898
10
- freeplay/provider_config.py,sha256=hruf3Khusrwb76_-hv7ouuxmvJuaRyC1UxIw7XlJx8A,1416
11
- freeplay/record.py,sha256=pNLu5vPw6HyyPz5u-32pDfElY3hxeujaM1h7Xzah-VE,2961
12
- freeplay/utils.py,sha256=SxZYKF5H6uotQa0m2B92CTk3ADr5N5SsUS_a6HQ0fnk,1026
13
- freeplay-0.2.30.dist-info/LICENSE,sha256=_jzIw45hB1XHGxiQ8leZ0GH_X7bR_a8qgxaqnHbCUOo,1064
14
- freeplay-0.2.30.dist-info/METADATA,sha256=XE8Y8IZibHed27Gi74EUrlfacpqVxWGxF7eyjE5mQZs,1633
15
- freeplay-0.2.30.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
16
- freeplay-0.2.30.dist-info/entry_points.txt,sha256=32s3rf2UUCqiJT4jnClEXZhdXlvl30uwpcxz-Gsy4UU,54
17
- freeplay-0.2.30.dist-info/RECORD,,