foundry-local-sdk 0.3.0__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.
- foundry_local/__init__.py +23 -0
- foundry_local/api.py +333 -0
- foundry_local/client.py +117 -0
- foundry_local/logging.py +92 -0
- foundry_local/models.py +144 -0
- foundry_local/service.py +61 -0
- foundry_local_sdk-0.3.0.dist-info/METADATA +142 -0
- foundry_local_sdk-0.3.0.dist-info/RECORD +11 -0
- foundry_local_sdk-0.3.0.dist-info/WHEEL +5 -0
- foundry_local_sdk-0.3.0.dist-info/licenses/LICENSE.txt +21 -0
- foundry_local_sdk-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# -------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
# Licensed under the MIT License.
|
|
4
|
+
# --------------------------------------------------------------------------
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from foundry_local.api import FoundryLocalManager
|
|
9
|
+
|
|
10
|
+
_logger = logging.getLogger(__name__)
|
|
11
|
+
_logger.setLevel(logging.WARNING)
|
|
12
|
+
|
|
13
|
+
_sc = logging.StreamHandler(stream=sys.stdout)
|
|
14
|
+
_formatter = logging.Formatter(
|
|
15
|
+
"[foundry-local] | %(asctime)s | %(levelname)-8s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
|
16
|
+
)
|
|
17
|
+
_sc.setFormatter(_formatter)
|
|
18
|
+
_logger.addHandler(_sc)
|
|
19
|
+
_logger.propagate = False
|
|
20
|
+
|
|
21
|
+
__all__ = ["FoundryLocalManager"]
|
|
22
|
+
|
|
23
|
+
__version__ = "0.3.0"
|
foundry_local/api.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# -------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
# Licensed under the MIT License.
|
|
4
|
+
# --------------------------------------------------------------------------
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
|
|
11
|
+
from httpx import Timeout
|
|
12
|
+
|
|
13
|
+
from foundry_local.client import HttpResponseError, HttpxClient
|
|
14
|
+
from foundry_local.models import ExecutionProvider, FoundryModelInfo
|
|
15
|
+
from foundry_local.service import assert_foundry_installed, get_service_uri, start_service
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FoundryLocalManager:
|
|
21
|
+
"""Manager for Foundry Local SDK operations."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self, alias_or_model_id: str | None = None, bootstrap: bool = True, timeout: float | Timeout | None = None
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
Initialize the Foundry Local SDK.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
alias_or_model_id (str | None): Alias or Model ID to download and load. Only used if bootstrap is True.
|
|
31
|
+
bootstrap (bool): If True, start the service if it is not running.
|
|
32
|
+
timeout (float | Timeout | None): Timeout for the HTTP client. Default is None.
|
|
33
|
+
"""
|
|
34
|
+
assert_foundry_installed()
|
|
35
|
+
self._timeout = timeout
|
|
36
|
+
self._service_uri = None
|
|
37
|
+
self._httpx_client = None
|
|
38
|
+
self._set_service_uri_and_client(get_service_uri())
|
|
39
|
+
self._catalog_list = None
|
|
40
|
+
self._catalog_dict = None
|
|
41
|
+
if bootstrap:
|
|
42
|
+
self.start_service()
|
|
43
|
+
if alias_or_model_id is not None:
|
|
44
|
+
self.download_model(alias_or_model_id)
|
|
45
|
+
self.load_model(alias_or_model_id)
|
|
46
|
+
|
|
47
|
+
def _set_service_uri_and_client(self, service_uri: str | None):
|
|
48
|
+
"""
|
|
49
|
+
Set the service URI and HTTP client.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
service_uri (str | None): URI of the Foundry service.
|
|
53
|
+
"""
|
|
54
|
+
self._service_uri = service_uri
|
|
55
|
+
self._httpx_client = HttpxClient(service_uri, timeout=self._timeout) if service_uri else None
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def service_uri(self) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Get the service URI.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
str: URI of the Foundry service.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
RuntimeError: If the service URI is not set.
|
|
67
|
+
"""
|
|
68
|
+
if self._service_uri is None:
|
|
69
|
+
raise RuntimeError("Service URI is not set. Please start the service first.")
|
|
70
|
+
return self._service_uri
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def httpx_client(self) -> HttpxClient:
|
|
74
|
+
"""
|
|
75
|
+
Get the HTTP client.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
HttpxClient: HTTP client instance.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
RuntimeError: If the HTTP client is not set.
|
|
82
|
+
"""
|
|
83
|
+
if self._httpx_client is None:
|
|
84
|
+
raise RuntimeError("Httpx client is not set. Please start the service first.")
|
|
85
|
+
return self._httpx_client
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def endpoint(self) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Get the endpoint for the service.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
str: Endpoint URL.
|
|
94
|
+
"""
|
|
95
|
+
return f"{self.service_uri}/v1"
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def api_key(self) -> str:
|
|
99
|
+
"""
|
|
100
|
+
Get the API key for authentication.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
str: API key.
|
|
104
|
+
"""
|
|
105
|
+
return os.getenv("OPENAI_API_KEY") or "OPENAI_API_KEY"
|
|
106
|
+
|
|
107
|
+
# Service management api
|
|
108
|
+
def is_service_running(self) -> bool:
|
|
109
|
+
"""
|
|
110
|
+
Check if the service is running. Will also set the service URI if it is not set.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
bool: True if the service is running, False otherwise.
|
|
114
|
+
"""
|
|
115
|
+
self._set_service_uri_and_client(get_service_uri())
|
|
116
|
+
return self._service_uri is not None
|
|
117
|
+
|
|
118
|
+
def start_service(self):
|
|
119
|
+
"""Start the service."""
|
|
120
|
+
self._set_service_uri_and_client(start_service())
|
|
121
|
+
|
|
122
|
+
# Catalog api
|
|
123
|
+
def list_catalog_models(self) -> list[FoundryModelInfo]:
|
|
124
|
+
"""
|
|
125
|
+
Get a list of available models in the catalog.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
list[FoundryModelInfo]: List of catalog models.
|
|
129
|
+
"""
|
|
130
|
+
if self._catalog_list is None:
|
|
131
|
+
self._catalog_list = [
|
|
132
|
+
FoundryModelInfo.from_list_response(model) for model in self.httpx_client.get("/foundry/list")
|
|
133
|
+
]
|
|
134
|
+
return self._catalog_list
|
|
135
|
+
|
|
136
|
+
def _get_catalog_dict(self) -> dict[str, FoundryModelInfo]:
|
|
137
|
+
"""
|
|
138
|
+
Get a dictionary of available models. Keyed by model ID and alias. Alias points to the most preferred model.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
dict[str, FoundryModelInfo]: Dictionary of catalog models.
|
|
142
|
+
"""
|
|
143
|
+
if self._catalog_dict is not None:
|
|
144
|
+
return self._catalog_dict
|
|
145
|
+
|
|
146
|
+
catalog_models = self.list_catalog_models()
|
|
147
|
+
self._catalog_dict = {model.id: model for model in catalog_models}
|
|
148
|
+
alias_candidates = {}
|
|
149
|
+
|
|
150
|
+
# Group models by alias
|
|
151
|
+
for model in catalog_models:
|
|
152
|
+
alias_candidates.setdefault(model.alias, []).append(model)
|
|
153
|
+
|
|
154
|
+
# Define the preferred order of execution providers
|
|
155
|
+
preferred_order = [
|
|
156
|
+
ExecutionProvider.QNN,
|
|
157
|
+
ExecutionProvider.CUDA,
|
|
158
|
+
ExecutionProvider.CPU,
|
|
159
|
+
ExecutionProvider.WEBGPU,
|
|
160
|
+
]
|
|
161
|
+
if platform.system() != "Windows":
|
|
162
|
+
# Adjust order for non-Windows platforms
|
|
163
|
+
preferred_order.remove(ExecutionProvider.CPU)
|
|
164
|
+
preferred_order.append(ExecutionProvider.CPU)
|
|
165
|
+
|
|
166
|
+
priority_map = {provider: index for index, provider in enumerate(preferred_order)}
|
|
167
|
+
|
|
168
|
+
# Choose the preferred model for each alias
|
|
169
|
+
for alias, candidates in alias_candidates.items():
|
|
170
|
+
self._catalog_dict[alias] = min(candidates, key=lambda model: priority_map.get(model.runtime, float("inf")))
|
|
171
|
+
|
|
172
|
+
return self._catalog_dict
|
|
173
|
+
|
|
174
|
+
def refresh_catalog(self):
|
|
175
|
+
"""Refresh the catalog."""
|
|
176
|
+
self._catalog_list = None
|
|
177
|
+
self._catalog_dict = None
|
|
178
|
+
|
|
179
|
+
def get_model_info(self, alias_or_model_id: str, raise_on_not_found: bool = False) -> FoundryModelInfo | None:
|
|
180
|
+
"""
|
|
181
|
+
Get the model information by alias or ID.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
alias_or_model_id (str): Alias or Model ID. If it is an alias, the most preferred model will be returned.
|
|
185
|
+
raise_on_not_found (bool): If True, raise an error if the model is not found. Default is False.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
FoundryModelInfo | None: Model information or None if not found.
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
ValueError: If the model is not found and raise_on_not_found is True.
|
|
192
|
+
"""
|
|
193
|
+
model_info = self._get_catalog_dict().get(alias_or_model_id)
|
|
194
|
+
if model_info is None and raise_on_not_found:
|
|
195
|
+
raise ValueError(f"Model {alias_or_model_id} not found in the catalog.")
|
|
196
|
+
return model_info
|
|
197
|
+
|
|
198
|
+
# Cache management api
|
|
199
|
+
def get_cache_location(self):
|
|
200
|
+
"""
|
|
201
|
+
Get the cache location.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
str: Path to the cache location.
|
|
205
|
+
"""
|
|
206
|
+
return self.httpx_client.get("/openai/status")["modelDirPath"]
|
|
207
|
+
|
|
208
|
+
def _fetch_model_infos(self, model_ids: list[str]) -> list[FoundryModelInfo]:
|
|
209
|
+
"""
|
|
210
|
+
Fetch model information for a list of model IDs.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
model_ids (list[str]): List of model IDs.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
list[FoundryModelInfo]: List of model information.
|
|
217
|
+
"""
|
|
218
|
+
model_infos = []
|
|
219
|
+
for model_id in model_ids:
|
|
220
|
+
if (model_info := self.get_model_info(model_id)) is not None:
|
|
221
|
+
model_infos.append(model_info)
|
|
222
|
+
else:
|
|
223
|
+
logger.debug("Model %s not found in the catalog.", model_id)
|
|
224
|
+
return model_infos
|
|
225
|
+
|
|
226
|
+
def list_cached_models(self) -> list[FoundryModelInfo]:
|
|
227
|
+
"""
|
|
228
|
+
Get a list of cached models.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
list[FoundryModelInfo]: List of models downloaded to the cache.
|
|
232
|
+
"""
|
|
233
|
+
return self._fetch_model_infos(self.httpx_client.get("/openai/models"))
|
|
234
|
+
|
|
235
|
+
# Model management api
|
|
236
|
+
def download_model(self, alias_or_model_id: str, token: str | None = None, force: bool = False) -> FoundryModelInfo:
|
|
237
|
+
"""
|
|
238
|
+
Download a model.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
alias_or_model_id (str): Alias or Model ID. If it is an alias, the most preferred model will be downloaded.
|
|
242
|
+
token (str | None): Optional token for authentication.
|
|
243
|
+
force (bool): If True, force download the model even if it is already downloaded.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
FoundryModelInfo: Model information.
|
|
247
|
+
|
|
248
|
+
Raises:
|
|
249
|
+
RuntimeError: If the model download fails.
|
|
250
|
+
"""
|
|
251
|
+
model_info = self.get_model_info(alias_or_model_id, raise_on_not_found=True)
|
|
252
|
+
if model_info in self.list_cached_models() and not force:
|
|
253
|
+
logger.info(
|
|
254
|
+
"Model with alias '%s' and ID '%s' is already downloaded. Use force=True to download it again.",
|
|
255
|
+
model_info.alias,
|
|
256
|
+
model_info.id,
|
|
257
|
+
)
|
|
258
|
+
return model_info
|
|
259
|
+
logger.info("Downloading model with alias '%s' and ID '%s'...", model_info.alias, model_info.id)
|
|
260
|
+
response_body = self.httpx_client.post_with_progress(
|
|
261
|
+
"/openai/download",
|
|
262
|
+
body={
|
|
263
|
+
"model": model_info.to_download_body(),
|
|
264
|
+
"token": token,
|
|
265
|
+
"IgnorePipeReport": True,
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
if not response_body.get("success", False):
|
|
269
|
+
raise RuntimeError(
|
|
270
|
+
f"Failed to download model with error: {response_body.get('errorMessage', 'Unknown error')}"
|
|
271
|
+
)
|
|
272
|
+
return model_info
|
|
273
|
+
|
|
274
|
+
def load_model(self, alias_or_model_id: str, ttl: int = 600) -> FoundryModelInfo:
|
|
275
|
+
"""
|
|
276
|
+
Load a model.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
alias_or_model_id (str): Alias or Model ID. If it is an alias, the most preferred model will be loaded.
|
|
280
|
+
ttl (int): Time to live for the model in seconds. Default is 600 seconds (10 minutes).
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
FoundryModelInfo: Model information.
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
ValueError: If the model is not in the catalog or has not been downloaded yet.
|
|
287
|
+
"""
|
|
288
|
+
model_info = self.get_model_info(alias_or_model_id, raise_on_not_found=True)
|
|
289
|
+
logger.info("Loading model with alias '%s' and ID '%s'...", model_info.alias, model_info.id)
|
|
290
|
+
query_params = {"ttl": ttl}
|
|
291
|
+
if model_info.runtime in {ExecutionProvider.WEBGPU, ExecutionProvider.CUDA}:
|
|
292
|
+
# these models might have empty ep or dml ep in the genai config
|
|
293
|
+
# use cuda if available, otherwise use the model's runtime
|
|
294
|
+
has_cuda_support = any(mi.runtime == ExecutionProvider.CUDA for mi in self.list_catalog_models())
|
|
295
|
+
query_params["ep"] = (
|
|
296
|
+
ExecutionProvider.CUDA.get_alias() if has_cuda_support else model_info.runtime.get_alias()
|
|
297
|
+
)
|
|
298
|
+
try:
|
|
299
|
+
self.httpx_client.get(f"/openai/load/{model_info.id}", query_params=query_params)
|
|
300
|
+
except HttpResponseError as e:
|
|
301
|
+
if "No OpenAIService provider found for modelName" in str(e):
|
|
302
|
+
raise ValueError(
|
|
303
|
+
f"Model {alias_or_model_id} has not been downloaded yet. Please download it first."
|
|
304
|
+
) from None
|
|
305
|
+
raise
|
|
306
|
+
return model_info
|
|
307
|
+
|
|
308
|
+
def unload_model(self, alias_or_model_id: str, force: bool = False):
|
|
309
|
+
"""
|
|
310
|
+
Unload a model.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
alias_or_model_id (str): Alias or Model ID.
|
|
314
|
+
force (bool): If True, force unload a model with TTL.
|
|
315
|
+
"""
|
|
316
|
+
model_info = self.get_model_info(alias_or_model_id, raise_on_not_found=True)
|
|
317
|
+
if model_info not in self.list_loaded_models():
|
|
318
|
+
# safest since unload fails if model is not downloaded, easier to check if loaded
|
|
319
|
+
logger.info(
|
|
320
|
+
"Model with alias '%s' and ID '%s' is not loaded. No need to unload.", model_info.alias, model_info.id
|
|
321
|
+
)
|
|
322
|
+
return
|
|
323
|
+
logger.info("Unloading model with alias '%s' and ID '%s'...", model_info.alias, model_info.id)
|
|
324
|
+
self.httpx_client.get(f"/openai/unload/{model_info.id}", query_params={"force": force})
|
|
325
|
+
|
|
326
|
+
def list_loaded_models(self) -> list[FoundryModelInfo]:
|
|
327
|
+
"""
|
|
328
|
+
Get a list of loaded models.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
list[FoundryModelInfo]: List of loaded models.
|
|
332
|
+
"""
|
|
333
|
+
return self._fetch_model_infos(self.httpx_client.get("/openai/loadedmodels"))
|
foundry_local/client.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# -------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
# Licensed under the MIT License.
|
|
4
|
+
# --------------------------------------------------------------------------
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
from tqdm import tqdm
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HttpResponseError(Exception):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HttpxClient:
|
|
22
|
+
"""
|
|
23
|
+
Client for Foundry Local SDK.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
_client (httpx.Client): HTTP client instance.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, host: str, timeout: float | httpx.Timeout | None = None) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Initialize the HttpxClient with the host.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
host (str): Base URL of the host.
|
|
35
|
+
timeout (float | httpx.Timeout | None): Timeout for the HTTP client.
|
|
36
|
+
"""
|
|
37
|
+
self._client = httpx.Client(base_url=host, timeout=timeout)
|
|
38
|
+
|
|
39
|
+
def _request(self, *args, **kwargs) -> httpx.Response:
|
|
40
|
+
"""
|
|
41
|
+
Send an HTTP request.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
*args: Positional arguments for the request.
|
|
45
|
+
**kwargs: Keyword arguments for the request.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
httpx.Response: HTTP response object.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
RuntimeError: If an HTTP error or connection error occurs.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
response = self._client.request(*args, **kwargs)
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
except httpx.HTTPStatusError as e:
|
|
57
|
+
raise HttpResponseError(f"{e.response.status_code} - {e.response.text}") from None
|
|
58
|
+
except httpx.ConnectError:
|
|
59
|
+
raise ConnectionError(
|
|
60
|
+
"Could not connect to Foundry Local! Please check if the Foundry Local service is running and the host"
|
|
61
|
+
" URL is correct."
|
|
62
|
+
) from None
|
|
63
|
+
return response
|
|
64
|
+
|
|
65
|
+
def get(self, path: str, query_params: dict[str, str] | None = None) -> dict | list | None:
|
|
66
|
+
"""
|
|
67
|
+
Send a GET request to the specified path with optional query parameters.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
path (str): Path for the GET request.
|
|
71
|
+
query_params (dict[str, str] | None): Query parameters for the request.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
dict | list | None: JSON response or None if no content.
|
|
75
|
+
"""
|
|
76
|
+
response = self._request("GET", path, params=query_params)
|
|
77
|
+
return response.json() if response.text else None
|
|
78
|
+
|
|
79
|
+
def post_with_progress(self, path: str, body: dict | None = None) -> dict:
|
|
80
|
+
"""
|
|
81
|
+
Send a POST request to the specified path with optional request body and show progress.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
path (str): Path for the POST request.
|
|
85
|
+
body (dict | None): Request body in JSON format.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
dict: JSON response.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ValueError: If the JSON response is invalid.
|
|
92
|
+
"""
|
|
93
|
+
with self._client.stream("POST", path, json=body, timeout=None) as response:
|
|
94
|
+
progress_bar = None
|
|
95
|
+
prev_percent = 0.0
|
|
96
|
+
if logger.isEnabledFor(logging.INFO):
|
|
97
|
+
progress_bar = tqdm(total=100.0)
|
|
98
|
+
final_json = ""
|
|
99
|
+
for line in response.iter_lines():
|
|
100
|
+
if final_json or line.startswith("{"):
|
|
101
|
+
final_json += line
|
|
102
|
+
continue
|
|
103
|
+
if not progress_bar:
|
|
104
|
+
continue
|
|
105
|
+
if match := re.search(r"(\d+(?:\.\d+)?)%", line):
|
|
106
|
+
percent = min(float(match.group(1)), 100.0)
|
|
107
|
+
delta = percent - prev_percent
|
|
108
|
+
if delta > 0:
|
|
109
|
+
progress_bar.update(delta)
|
|
110
|
+
prev_percent = percent
|
|
111
|
+
if progress_bar:
|
|
112
|
+
progress_bar.close()
|
|
113
|
+
|
|
114
|
+
if not final_json.endswith("}"):
|
|
115
|
+
raise ValueError(f"Invalid JSON response: {final_json}")
|
|
116
|
+
|
|
117
|
+
return json.loads(final_json)
|
foundry_local/logging.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# -------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
# Licensed under the MIT License.
|
|
4
|
+
# --------------------------------------------------------------------------
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_logger():
|
|
9
|
+
"""
|
|
10
|
+
Get the logger for the sdk.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
logging.Logger: Logger instance for the module.
|
|
14
|
+
"""
|
|
15
|
+
return logging.getLogger(__name__.split(".", maxsplit=1)[0])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def set_verbosity(verbose):
|
|
19
|
+
"""
|
|
20
|
+
Set the verbosity level for the logger.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
verbose (int): Verbosity level (e.g., logging.INFO, logging.DEBUG).
|
|
24
|
+
"""
|
|
25
|
+
get_logger().setLevel(verbose)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def set_verbosity_info():
|
|
29
|
+
"""Set the verbosity level to INFO."""
|
|
30
|
+
set_verbosity(logging.INFO)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def set_verbosity_warning():
|
|
34
|
+
"""Set the verbosity level to WARNING."""
|
|
35
|
+
set_verbosity(logging.WARNING)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def set_verbosity_debug():
|
|
39
|
+
"""Set the verbosity level to DEBUG."""
|
|
40
|
+
set_verbosity(logging.DEBUG)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def set_verbosity_error():
|
|
44
|
+
"""Set the verbosity level to ERROR."""
|
|
45
|
+
set_verbosity(logging.ERROR)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def set_verbosity_critical():
|
|
49
|
+
"""Set the verbosity level to CRITICAL."""
|
|
50
|
+
set_verbosity(logging.CRITICAL)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_verbosity() -> int:
|
|
54
|
+
"""
|
|
55
|
+
Get the current verbosity level of the logger.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
int: Verbosity level as an integer.
|
|
59
|
+
"""
|
|
60
|
+
return get_logger().getEffectiveLevel()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_logger_level(level):
|
|
64
|
+
"""
|
|
65
|
+
Get Python logging level for the integer level.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
level (int): Verbosity level (0: DEBUG, 1: INFO, 2: WARNING, 3: ERROR, 4: CRITICAL).
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
int: Corresponding Python logging level.
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
ValueError: If the level is invalid.
|
|
75
|
+
"""
|
|
76
|
+
level_map = {0: logging.DEBUG, 1: logging.INFO, 2: logging.WARNING, 3: logging.ERROR, 4: logging.CRITICAL}
|
|
77
|
+
# check if level is valid
|
|
78
|
+
if level not in level_map:
|
|
79
|
+
raise ValueError(f"Invalid level {level}, should be one of {list(level_map.keys())}")
|
|
80
|
+
|
|
81
|
+
return level_map[level]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def set_default_logger_severity(level):
|
|
85
|
+
"""
|
|
86
|
+
Set the default log level for the logger.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
level (int): Verbosity level (0: DEBUG, 1: INFO, 2: WARNING, 3: ERROR, 4: CRITICAL).
|
|
90
|
+
"""
|
|
91
|
+
# set logger level
|
|
92
|
+
set_verbosity(get_logger_level(level))
|
foundry_local/models.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# -------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
# Licensed under the MIT License.
|
|
4
|
+
# --------------------------------------------------------------------------
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
if sys.version_info >= (3, 11):
|
|
12
|
+
from enum import StrEnum
|
|
13
|
+
|
|
14
|
+
else:
|
|
15
|
+
from enum import Enum
|
|
16
|
+
|
|
17
|
+
class StrEnum(str, Enum):
|
|
18
|
+
def __str__(self) -> str:
|
|
19
|
+
return self.value
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ruff: noqa: N815
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DeviceType(StrEnum):
|
|
26
|
+
"""Enumeration of devices supported by the model."""
|
|
27
|
+
|
|
28
|
+
CPU = "CPU"
|
|
29
|
+
GPU = "GPU"
|
|
30
|
+
NPU = "NPU"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ExecutionProvider(StrEnum):
|
|
34
|
+
"""Enumeration of execution providers supported by the model."""
|
|
35
|
+
|
|
36
|
+
CPU = "CPUExecutionProvider"
|
|
37
|
+
WEBGPU = "WebGpuExecutionProvider"
|
|
38
|
+
CUDA = "CUDAExecutionProvider"
|
|
39
|
+
QNN = "QNNExecutionProvider"
|
|
40
|
+
|
|
41
|
+
def get_alias(self) -> str:
|
|
42
|
+
"""
|
|
43
|
+
Get the alias for the execution provider.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
str: Alias of the execution provider.
|
|
47
|
+
"""
|
|
48
|
+
return self.value.replace("ExecutionProvider", "").lower()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ModelRuntime(BaseModel):
|
|
52
|
+
"""Model runtime information."""
|
|
53
|
+
|
|
54
|
+
deviceType: DeviceType = Field(..., description="Device type supported by the model")
|
|
55
|
+
executionProvider: ExecutionProvider = Field(
|
|
56
|
+
...,
|
|
57
|
+
description="Execution provider supported by the model",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class FoundryListResponseModel(BaseModel):
|
|
62
|
+
"""Response model for listing models."""
|
|
63
|
+
|
|
64
|
+
name: str = Field(..., description="Name of the model")
|
|
65
|
+
displayName: str = Field(..., description="Display name of the model")
|
|
66
|
+
modelType: str = Field(..., description="Type of the model")
|
|
67
|
+
providerType: str = Field(..., description="Provider type of the model")
|
|
68
|
+
uri: str = Field(..., description="URI of the model")
|
|
69
|
+
version: str = Field(..., description="Version of the model")
|
|
70
|
+
promptTemplate: dict = Field(..., description="Prompt template for the model")
|
|
71
|
+
publisher: str = Field(..., description="Publisher of the model")
|
|
72
|
+
task: str = Field(..., description="Task of the model")
|
|
73
|
+
runtime: ModelRuntime = Field(..., description="Runtime information of the model")
|
|
74
|
+
fileSizeMb: int = Field(..., description="File size of the model in MB")
|
|
75
|
+
modelSettings: dict = Field(..., description="Model settings")
|
|
76
|
+
alias: str = Field(..., description="Alias name of the model")
|
|
77
|
+
supportsToolCalling: bool = Field(..., description="Whether the model supports tool calling")
|
|
78
|
+
license: str = Field(..., description="License of the model")
|
|
79
|
+
licenseDescription: str = Field(..., description="License description of the model")
|
|
80
|
+
parentModelUri: str = Field(..., description="Parent model URI of the model")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class FoundryModelInfo(BaseModel):
|
|
84
|
+
"""Model information."""
|
|
85
|
+
|
|
86
|
+
alias: str = Field(..., description="Alias of the model")
|
|
87
|
+
id: str = Field(..., description="Unique identifier of the model")
|
|
88
|
+
version: str = Field(..., description="Version of the model")
|
|
89
|
+
runtime: ExecutionProvider = Field(..., description="Execution provider of the model")
|
|
90
|
+
uri: str = Field(..., description="URI of the model")
|
|
91
|
+
model_size: int = Field(..., description="Size of the model on disk in MB")
|
|
92
|
+
prompt_template: dict = Field(..., description="Prompt template for the model")
|
|
93
|
+
provider: str = Field(..., description="Provider of the model")
|
|
94
|
+
publisher: str = Field(..., description="Publisher of the model")
|
|
95
|
+
license: str = Field(..., description="License of the model")
|
|
96
|
+
task: str = Field(..., description="Task of the model")
|
|
97
|
+
|
|
98
|
+
def __repr__(self) -> str:
|
|
99
|
+
return (
|
|
100
|
+
f"FoundryModelInfo(alias={self.alias}, id={self.id}, runtime={self.runtime.get_alias()},"
|
|
101
|
+
f" model_size={self.model_size} MB, license={self.license})"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def from_list_response(cls, response: dict | FoundryListResponseModel) -> FoundryModelInfo:
|
|
106
|
+
"""
|
|
107
|
+
Create a FoundryModelInfo object from a FoundryListResponseModel object.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
response (dict | FoundryListResponseModel): Response data.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
FoundryModelInfo: Instance of FoundryModelInfo.
|
|
114
|
+
"""
|
|
115
|
+
if isinstance(response, dict):
|
|
116
|
+
response = FoundryListResponseModel.model_validate(response)
|
|
117
|
+
return cls(
|
|
118
|
+
alias=response.alias,
|
|
119
|
+
id=response.name,
|
|
120
|
+
version=response.version,
|
|
121
|
+
runtime=response.runtime.executionProvider,
|
|
122
|
+
uri=response.uri,
|
|
123
|
+
model_size=response.fileSizeMb,
|
|
124
|
+
prompt_template=response.promptTemplate,
|
|
125
|
+
provider=response.providerType,
|
|
126
|
+
publisher=response.publisher,
|
|
127
|
+
license=response.license,
|
|
128
|
+
task=response.task,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def to_download_body(self) -> dict:
|
|
132
|
+
"""
|
|
133
|
+
Convert the FoundryModelInfo object to a dictionary for download.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
dict: Dictionary representation for download.
|
|
137
|
+
"""
|
|
138
|
+
return {
|
|
139
|
+
"Name": self.id,
|
|
140
|
+
"Uri": self.uri,
|
|
141
|
+
"Publisher": self.publisher,
|
|
142
|
+
"ProviderType": f"{self.provider}Local" if self.provider == "AzureFoundry" else self.provider,
|
|
143
|
+
"PromptTemplate": self.prompt_template,
|
|
144
|
+
}
|
foundry_local/service.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# -------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
# Licensed under the MIT License.
|
|
4
|
+
# --------------------------------------------------------------------------
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def assert_foundry_installed():
|
|
17
|
+
"""
|
|
18
|
+
Assert that Foundry is installed.
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
RuntimeError: If Foundry is not installed or not on PATH.
|
|
22
|
+
"""
|
|
23
|
+
if shutil.which("foundry") is None:
|
|
24
|
+
raise RuntimeError("Foundry is not installed or not on PATH!")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_service_uri() -> str | None:
|
|
28
|
+
"""
|
|
29
|
+
Get the service URI if the service is running.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
str | None: URI of the running Foundry service, or None if not running.
|
|
33
|
+
"""
|
|
34
|
+
with subprocess.Popen(["foundry", "service", "status"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
|
|
35
|
+
stdout, _ = proc.communicate()
|
|
36
|
+
match = re.search(r"http://(?:[a-zA-Z0-9.-]+|\d{1,3}(\.\d{1,3}){3}):\d+", stdout.decode())
|
|
37
|
+
if match:
|
|
38
|
+
return match.group(0)
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def start_service() -> str | None:
|
|
43
|
+
"""
|
|
44
|
+
Start the Foundry service.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
str | None: URI of the started Foundry service, or None if it failed to start.
|
|
48
|
+
"""
|
|
49
|
+
if (service_url := get_service_uri()) is not None:
|
|
50
|
+
logger.info("Foundry service is already running at %s", service_url)
|
|
51
|
+
return service_url
|
|
52
|
+
|
|
53
|
+
with subprocess.Popen(["foundry", "service", "start"]):
|
|
54
|
+
# not checking the process output since it never finishes communication
|
|
55
|
+
for _ in range(10):
|
|
56
|
+
if (service_url := get_service_uri()) is not None:
|
|
57
|
+
logger.info("Foundry service started successfully at %s", service_url)
|
|
58
|
+
return service_url
|
|
59
|
+
time.sleep(0.1)
|
|
60
|
+
logger.warning("Foundry service did not start within the expected time. May not be running.")
|
|
61
|
+
return None
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: foundry-local-sdk
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Foundry Local Manager Python SDK: Control-plane SDK for Foundry Local.
|
|
5
|
+
Author: Microsoft Corporation
|
|
6
|
+
Author-email: foundrylocaldevs@microsoft.com
|
|
7
|
+
License: MIT License
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
12
|
+
Classifier: Topic :: Software Development
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE.txt
|
|
25
|
+
Requires-Dist: httpx
|
|
26
|
+
Requires-Dist: pydantic
|
|
27
|
+
Requires-Dist: tqdm
|
|
28
|
+
Dynamic: author
|
|
29
|
+
Dynamic: author-email
|
|
30
|
+
Dynamic: classifier
|
|
31
|
+
Dynamic: description
|
|
32
|
+
Dynamic: description-content-type
|
|
33
|
+
Dynamic: license
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
Dynamic: requires-dist
|
|
36
|
+
Dynamic: requires-python
|
|
37
|
+
Dynamic: summary
|
|
38
|
+
|
|
39
|
+
# Foundry Local Python SDK
|
|
40
|
+
The Foundry Local SDK simplifies AI model management in local environments by providing control-plane operations separate from data-plane inferencing code.
|
|
41
|
+
|
|
42
|
+
## Prerequisites
|
|
43
|
+
Foundry Local must be installed and findable in your PATH.
|
|
44
|
+
|
|
45
|
+
## Getting Started
|
|
46
|
+
```
|
|
47
|
+
pip install foundry-local-sdk
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
The SDK provides a simple interface to interact with the Foundry Local API. You can use it to manage models, check the status of the service, and make requests to the models.
|
|
53
|
+
|
|
54
|
+
### Bootstrapping
|
|
55
|
+
|
|
56
|
+
The SDK can *bootstrap* Foundry Local, which will initiate the following sequence:
|
|
57
|
+
|
|
58
|
+
1. Start the Foundry Local service, if it is not already running.
|
|
59
|
+
1. Automatically detect the hardware and software requirements for the model.
|
|
60
|
+
1. Download the highest-performance model for the detected hardware, if it is not already downloaded.
|
|
61
|
+
1. Load the model into memory.
|
|
62
|
+
|
|
63
|
+
To use the SDK with bootstrapping, you can use the following code:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from foundry_local import FoundryLocalManager
|
|
67
|
+
|
|
68
|
+
# Provide the alias of the model you want to use
|
|
69
|
+
# The alias is a string that identifies the model in the Foundry Local catalog.
|
|
70
|
+
# The alias can be found in the Foundry Local catalog.
|
|
71
|
+
# For example, "phi-3.5-mini" is the alias for the Phi 3.5 model.
|
|
72
|
+
# Foundry local will automatically download the most suitable model for your hardware.
|
|
73
|
+
alias = "phi-3.5-mini"
|
|
74
|
+
fl_manager = FoundryLocalManager(alias)
|
|
75
|
+
|
|
76
|
+
# check that the service is running
|
|
77
|
+
print(fl_manager.is_service_running())
|
|
78
|
+
|
|
79
|
+
# list all available models in the catalog
|
|
80
|
+
print(fl_manager.list_catalog_models())
|
|
81
|
+
|
|
82
|
+
# list all downloaded models
|
|
83
|
+
print(fl_manager.list_cached_models())
|
|
84
|
+
|
|
85
|
+
# get information on the selected model
|
|
86
|
+
model_info = fl_manager.get_model_info(alias)
|
|
87
|
+
print(model_info)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Alternatively, you can use the `FoundryLocalManager` class to manage the service and models manually. This is useful if you want to control the service and models without bootstrapping. For example, you want to present to the end user what is happening in the background.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from foundry_local import FoundryLocalManager
|
|
94
|
+
|
|
95
|
+
alias = "phi-3.5-mini"
|
|
96
|
+
fl_manager = FoundryLocalManager()
|
|
97
|
+
|
|
98
|
+
# start the service
|
|
99
|
+
fl_manager.start_service()
|
|
100
|
+
|
|
101
|
+
# download the model
|
|
102
|
+
fl_manager.download_model(alias)
|
|
103
|
+
|
|
104
|
+
# load the model
|
|
105
|
+
model_info = fl_manager.load_model(alias)
|
|
106
|
+
print(model_info)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Use the foundry local endpoint with an OpenAI compatible API client. For example, using the `openai` package:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
import openai
|
|
113
|
+
from foundry_local import FoundryLocalManager
|
|
114
|
+
|
|
115
|
+
# By using an alias, the most suitable model will be downloaded
|
|
116
|
+
# to your end-user's device.
|
|
117
|
+
alias = "phi-3.5-mini"
|
|
118
|
+
|
|
119
|
+
# Create a FoundryLocalManager instance. This will start the Foundry
|
|
120
|
+
# Local service if it is not already running and load the specified model.
|
|
121
|
+
manager = FoundryLocalManager(alias)
|
|
122
|
+
|
|
123
|
+
# The remaining code us es the OpenAI Python SDK to interact with the local model.
|
|
124
|
+
|
|
125
|
+
# Configure the client to use the local Foundry service
|
|
126
|
+
client = openai.OpenAI(
|
|
127
|
+
base_url=manager.endpoint,
|
|
128
|
+
api_key=manager.api_key # API key is not required for local usage
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Set the model to use and generate a streaming response
|
|
132
|
+
stream = client.chat.completions.create(
|
|
133
|
+
model=manager.get_model_info(alias).id,
|
|
134
|
+
messages=[{"role": "user", "content": "What is the golden ratio?"}],
|
|
135
|
+
stream=True
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Print the streaming response
|
|
139
|
+
for chunk in stream:
|
|
140
|
+
if chunk.choices[0].delta.content is not None:
|
|
141
|
+
print(chunk.choices[0].delta.content, end="", flush=True)
|
|
142
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
foundry_local/__init__.py,sha256=q83bJ8Aw6Yg1Pfy8KwXPglkJvEOkR66mJN8DPF1ulYQ,736
|
|
2
|
+
foundry_local/api.py,sha256=A1L63_duWGCRjFVzO76KRdpGCCyfvp4Lz9BLCBPACas,12699
|
|
3
|
+
foundry_local/client.py,sha256=vKKhshuTJOOyHLlZ8JdFRIm5pUl3fpEtMxamMu9kUwU,4065
|
|
4
|
+
foundry_local/logging.py,sha256=k4_qHXM0bitU_5DQZyGDTogWGVXX3FOd7z0zlC8TezM,2403
|
|
5
|
+
foundry_local/models.py,sha256=wAam90hp_PYFreYh_a0QDGCZ9GoMbKfeSnMBoFpfgwI,5487
|
|
6
|
+
foundry_local/service.py,sha256=vo7mYL1Nilj68rPGKaopdeazQQ9gk-wS1rDbZjqqc9Q,2104
|
|
7
|
+
foundry_local_sdk-0.3.0.dist-info/licenses/LICENSE.txt,sha256=wlDWJ48LR6ZDn7dZKwi1ilXrn1NapJodtjIRw_mCtnQ,1094
|
|
8
|
+
foundry_local_sdk-0.3.0.dist-info/METADATA,sha256=gdyyhnVtQEOxD6sIS-7ulVzbCbY9RN0TuQtBTx6e0AM,4960
|
|
9
|
+
foundry_local_sdk-0.3.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
|
10
|
+
foundry_local_sdk-0.3.0.dist-info/top_level.txt,sha256=TE2fqMfN2Txs1Wltmv8-tu-Y6C4E0-0fg0zwO8w69QQ,14
|
|
11
|
+
foundry_local_sdk-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Microsoft Corporation
|
|
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 @@
|
|
|
1
|
+
foundry_local
|