Skip to content

Commit 426e145

Browse files
release: 0.1.0-alpha.9 (#145)
* chore(tests): improve ci test names * Move the files upload into files resource. * Format * Fix circular import * Add async upload support * Remove top level exports of UploadManager, DownloadManager * feat(api): move upload to be a method of existing files resource * feat(api): move upload to be a method of existing files resource * fix(api): correct file reroute handling, error message * feat(api): get file upload working * feat(api): get file upload working and add tests * chore(api): format * chore(api): address lint complaints, remove unused redirect:false option from upload manager * chore(api): file.upload -> file.upload_file to make room for files/upload path if added to openapi minimizing conflicts * chore(cli): fix missed renaming * release: 0.1.0-alpha.9 --------- Co-authored-by: Justin Driemeyer <[email protected]> Co-authored-by: jdreamerz <[email protected]> Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
1 parent 5844dd0 commit 426e145

File tree

13 files changed

+243
-103
lines changed

13 files changed

+243
-103
lines changed

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "0.1.0-alpha.8"
2+
".": "0.1.0-alpha.9"
33
}

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## 0.1.0-alpha.9 (2025-05-31)
4+
5+
Full Changelog: [v0.1.0-alpha.8...v0.1.0-alpha.9](https://github.com/togethercomputer/together-py/compare/v0.1.0-alpha.8...v0.1.0-alpha.9)
6+
7+
### Features
8+
9+
* **api:** get file upload working ([cb8b8b8](https://github.com/togethercomputer/together-py/commit/cb8b8b86974721c2b2366e8481b88b3cb4851f0c))
10+
* **api:** move upload to be a method of existing files resource ([b7c43be](https://github.com/togethercomputer/together-py/commit/b7c43be446e48390528994ee5a070699c490cec4))
11+
12+
13+
### Bug Fixes
14+
15+
* **api:** correct file reroute handling, error message ([b8bc101](https://github.com/togethercomputer/together-py/commit/b8bc1010e047ba0b1bd75a311cb1220f13366f04))
16+
317
## 0.1.0-alpha.8 (2025-05-29)
418

519
Full Changelog: [v0.1.0-alpha.7...v0.1.0-alpha.8](https://github.com/togethercomputer/together-py/compare/v0.1.0-alpha.7...v0.1.0-alpha.8)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "together"
3-
version = "0.1.0-alpha.8"
3+
version = "0.1.0-alpha.9"
44
description = "The official Python library for the together API"
55
dynamic = ["readme"]
66
license = "Apache-2.0"

src/together/_base_client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,9 @@ def request(
960960
if self.custom_auth is not None:
961961
kwargs["auth"] = self.custom_auth
962962

963+
if options.follow_redirects is not None:
964+
kwargs["follow_redirects"] = options.follow_redirects
965+
963966
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
964967

965968
response = None
@@ -1460,6 +1463,9 @@ async def request(
14601463
if self.custom_auth is not None:
14611464
kwargs["auth"] = self.custom_auth
14621465

1466+
if options.follow_redirects is not None:
1467+
kwargs["follow_redirects"] = options.follow_redirects
1468+
14631469
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
14641470

14651471
response = None

src/together/_models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
737737
idempotency_key: str
738738
json_data: Body
739739
extra_json: AnyMapping
740+
follow_redirects: bool
740741

741742

742743
@final
@@ -750,6 +751,7 @@ class FinalRequestOptions(pydantic.BaseModel):
750751
files: Union[HttpxRequestFiles, None] = None
751752
idempotency_key: Union[str, None] = None
752753
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
754+
follow_redirects: Union[bool, None] = None
753755

754756
# It should be noted that we cannot use `json` here as that would override
755757
# a BaseModel method in an incompatible fashion.

src/together/_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class RequestOptions(TypedDict, total=False):
100100
params: Query
101101
extra_json: AnyMapping
102102
idempotency_key: str
103+
follow_redirects: bool
103104

104105

105106
# Sentinel class used until PEP 0661 is accepted
@@ -215,3 +216,4 @@ class _GenericAlias(Protocol):
215216

216217
class HttpxSendArgs(TypedDict, total=False):
217218
auth: httpx.Auth
219+
follow_redirects: bool

src/together/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

33
__title__ = "together"
4-
__version__ = "0.1.0-alpha.8" # x-release-please-version
4+
__version__ = "0.1.0-alpha.9" # x-release-please-version

src/together/lib/cli/api/files.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ def files(_ctx: click.Context) -> None:
2929
@click.option(
3030
"--purpose",
3131
type=str,
32-
default="fine-tunes",
33-
help="Purpose of file upload. Acceptable values in enum `together.types.FilePurpose`. Defaults to `fine-tunes`.",
32+
default="fine-tune",
33+
help="Purpose of file upload. Acceptable values in enum `together.types.FilePurpose`. Defaults to `fine-tune`.",
3434
)
3535
@click.option(
3636
"--check/--no-check",
@@ -42,7 +42,7 @@ def upload(ctx: click.Context, file: pathlib.Path, purpose: str, check: bool) ->
4242

4343
client: Together = ctx.obj
4444

45-
response = client.files.upload(file=file, purpose=purpose, check=check)
45+
response = client.files.upload_file(file=file, purpose=purpose, check=check)
4646

4747
click.echo(json.dumps(response.model_dump(exclude_none=True), indent=4))
4848

src/together/lib/resources/files.py

Lines changed: 74 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
import stat
55
import uuid
66
import shutil
7+
import logging
78
import tempfile
8-
from typing import Tuple
9+
from typing import IO, Tuple, cast
910
from pathlib import Path
1011
from functools import partial
1112

@@ -21,6 +22,8 @@
2122
from ..types.error import DownloadError, FileTypeError
2223
from ..._exceptions import APIStatusError, AuthenticationError
2324

25+
log: logging.Logger = logging.getLogger(__name__)
26+
2427

2528
def chmod_and_replace(src: Path, dst: Path) -> None:
2629
"""Set correct permission before moving a blob from tmp directory to cache dir.
@@ -225,6 +228,7 @@ def get_upload_url(
225228
path=url,
226229
cast_to=httpx.Response,
227230
body=data,
231+
options={"headers": {"Content-Type": "multipart/form-data"}, "follow_redirects": False},
228232
)
229233
except APIStatusError as e:
230234
if e.response.status_code == 401:
@@ -235,15 +239,14 @@ def get_upload_url(
235239
response=e.response,
236240
body=e.body,
237241
) from e
238-
raise
239242

240-
# Raise error for non 302 status codes
241-
if response.status_code != 302:
242-
raise APIStatusError(
243-
f"Unexpected error raised by endpoint: {response.content.decode()}, headers: {response.headers}",
244-
response=response,
245-
body=response.content.decode(),
246-
)
243+
if e.response.status_code != 302:
244+
raise APIStatusError(
245+
f"Unexpected error raised by endpoint: {e.response.content.decode()}, headers: {e.response.headers}",
246+
response=e.response,
247+
body=e.response.content.decode(),
248+
) from e
249+
response = e.response
247250

248251
redirect_url = response.headers["Location"]
249252
file_id = response.headers["X-Together-File-Id"]
@@ -263,21 +266,19 @@ def upload(
263266
url: str,
264267
file: Path,
265268
purpose: FilePurpose,
266-
redirect: bool = False,
267269
) -> FileRetrieveResponse:
268270
file_id = None
269271

270272
redirect_url = None
271-
if redirect:
272-
if file.suffix == ".jsonl":
273-
filetype = "jsonl"
274-
elif file.suffix == ".parquet":
275-
filetype = "parquet"
276-
else:
277-
raise FileTypeError(
278-
f"Unknown extension of file {file}. Only files with extensions .jsonl and .parquet are supported."
279-
)
280-
redirect_url, file_id = self.get_upload_url(url, file, purpose, filetype) # type: ignore
273+
if file.suffix == ".jsonl":
274+
filetype = "jsonl"
275+
elif file.suffix == ".parquet":
276+
filetype = "parquet"
277+
else:
278+
raise FileTypeError(
279+
f"Unknown extension of file {file}. Only files with extensions .jsonl and .parquet are supported."
280+
)
281+
redirect_url, file_id = self.get_upload_url(url, file, purpose, filetype) # type: ignore
281282

282283
file_size = os.stat(file.as_posix()).st_size
283284

@@ -289,33 +290,32 @@ def upload(
289290
disable=bool(DISABLE_TQDM),
290291
) as pbar:
291292
with file.open("rb") as f:
292-
wrapped_file = CallbackIOWrapper(pbar.update, f, "read")
293+
wrapped_file = cast(IO[bytes], CallbackIOWrapper(pbar.update, f, "read"))
293294

294-
if redirect:
295-
assert redirect_url is not None
296-
callback_response = self._client.put(
297-
cast_to=httpx.Response,
298-
path=redirect_url,
299-
body=wrapped_file,
300-
)
301-
else:
302-
response = self._client.put(
303-
cast_to=FileRetrieveResponse,
304-
path=url,
305-
body=wrapped_file,
306-
)
295+
assert redirect_url is not None
296+
callback_response = self._client._client.put(
297+
url=redirect_url,
298+
content=wrapped_file.read(),
299+
)
300+
log.debug(
301+
'HTTP Response: %s %s "%i %s" %s',
302+
"put",
303+
redirect_url,
304+
callback_response.status_code,
305+
callback_response.reason_phrase,
306+
callback_response.headers,
307+
)
307308

308-
if redirect:
309-
assert isinstance(callback_response, httpx.Response) # type: ignore
309+
assert isinstance(callback_response, httpx.Response) # type: ignore
310310

311-
if not callback_response.status_code == 200:
312-
raise APIStatusError(
313-
f"Error during file upload: {callback_response.content.decode()}, headers: {callback_response.headers}",
314-
response=callback_response,
315-
body=callback_response.content.decode(),
316-
)
311+
if not callback_response.status_code == 200:
312+
raise APIStatusError(
313+
f"Error during file upload: {callback_response.content.decode()}, headers: {callback_response.headers}",
314+
response=callback_response,
315+
body=callback_response.content.decode(),
316+
)
317317

318-
response = self.callback(f"{url}/{file_id}/preprocess")
318+
response = self.callback(f"{url}/{file_id}/preprocess")
319319

320320
assert isinstance(response, FileRetrieveResponse) # type: ignore
321321

@@ -379,21 +379,19 @@ async def upload(
379379
url: str,
380380
file: Path,
381381
purpose: FilePurpose,
382-
redirect: bool = False,
383382
) -> FileRetrieveResponse:
384383
file_id = None
385384

386385
redirect_url = None
387-
if redirect:
388-
if file.suffix == ".jsonl":
389-
filetype = "jsonl"
390-
elif file.suffix == ".parquet":
391-
filetype = "parquet"
392-
else:
393-
raise FileTypeError(
394-
f"Unknown extension of file {file}. Only files with extensions .jsonl and .parquet are supported."
395-
)
396-
redirect_url, file_id = await self.get_upload_url(url, file, purpose, filetype) # type: ignore
386+
if file.suffix == ".jsonl":
387+
filetype = "jsonl"
388+
elif file.suffix == ".parquet":
389+
filetype = "parquet"
390+
else:
391+
raise FileTypeError(
392+
f"Unknown extension of file {file}. Only files with extensions .jsonl and .parquet are supported."
393+
)
394+
redirect_url, file_id = await self.get_upload_url(url, file, purpose, filetype) # type: ignore
397395

398396
file_size = os.stat(file.as_posix()).st_size
399397

@@ -405,33 +403,32 @@ async def upload(
405403
disable=bool(DISABLE_TQDM),
406404
) as pbar:
407405
with file.open("rb") as f:
408-
wrapped_file = CallbackIOWrapper(pbar.update, f, "read")
406+
wrapped_file = cast(IO[bytes], CallbackIOWrapper(pbar.update, f, "read"))
409407

410-
if redirect:
411-
assert redirect_url is not None
412-
callback_response = self._client.put(
413-
cast_to=httpx.Response,
414-
path=redirect_url,
415-
body=wrapped_file,
416-
)
417-
else:
418-
response = self._client.put(
419-
cast_to=FileRetrieveResponse,
420-
path=url,
421-
body=wrapped_file,
422-
)
408+
assert redirect_url is not None
409+
callback_response = await self._client._client.put(
410+
url=redirect_url,
411+
content=wrapped_file.read(),
412+
)
413+
log.debug(
414+
'HTTP Response: %s %s "%i %s" %s',
415+
"put",
416+
redirect_url,
417+
callback_response.status_code,
418+
callback_response.reason_phrase,
419+
callback_response.headers,
420+
)
423421

424-
if redirect:
425-
assert isinstance(callback_response, httpx.Response) # type: ignore
422+
assert isinstance(callback_response, httpx.Response) # type: ignore
426423

427-
if not callback_response.status_code == 200:
428-
raise APIStatusError(
429-
f"Error during file upload: {callback_response.content.decode()}, headers: {callback_response.headers}",
430-
response=callback_response,
431-
body=callback_response.content.decode(),
432-
)
424+
if not callback_response.status_code == 200:
425+
raise APIStatusError(
426+
f"Error during file upload: {callback_response.content.decode()}, headers: {callback_response.headers}",
427+
response=callback_response,
428+
body=callback_response.content.decode(),
429+
)
433430

434-
response = self.callback(f"{url}/{file_id}/preprocess")
431+
response = self.callback(f"{url}/{file_id}/preprocess")
435432

436433
assert isinstance(response, FileRetrieveResponse) # type: ignore
437434

src/together/lib/types/error.py

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,9 @@
1-
from typing import Any
2-
31
from ..._exceptions import TogetherError
42

53

64
class DownloadError(TogetherError):
7-
def __init__(
8-
self,
9-
message: str,
10-
**kwargs: Any,
11-
) -> None:
12-
self.message = message
13-
super().__init__(**kwargs)
5+
pass
146

157

168
class FileTypeError(TogetherError):
17-
def __init__(
18-
self,
19-
message: str,
20-
**kwargs: Any,
21-
) -> None:
22-
self.message = message
23-
super().__init__(**kwargs)
9+
pass

0 commit comments

Comments
 (0)