| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent c892c78 commit afe0092
13 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -23,9 +23,15 @@ jobs: | |||
| 23 | 23 | python-version: ${{ matrix.python-version }} | |
| 24 | 24 | cache: "pip" | |
| 25 | 25 | - name: Install dependencies | |
| 26 | - run: python -m pip install -r requirements.txt -r requirements-dev.txt . | ||
| 26 | + run: | | ||
| 27 | + python -m pip install -r requirements.txt -r requirements-dev.txt . | ||
| 28 | + yes | python -m mypy --install-types replicate || true | ||
| 29 | + | ||
| 27 | 30 | - name: Lint | |
| 28 | - run: python -m ruff . | ||
| 31 | + run: | | ||
| 32 | + python -m mypy replicate | ||
| 33 | + python -m ruff . | ||
| 34 | + python -m black --check . | ||
| 29 | 35 | - name: Test | |
| 30 | 36 | run: python -m pytest | |
| 31 | 37 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -11,7 +11,13 @@ license = { file = "LICENSE" } | |||
| 11 | 11 | authors = [{ name = "Replicate, Inc." }] | |
| 12 | 12 | requires-python = ">=3.8" | |
| 13 | 13 | dependencies = ["packaging", "pydantic>1", "requests>2"] | |
| 14 | - optional-dependencies = { dev = ["black", "pytest", "responses", "ruff"] } | ||
| 14 | + optional-dependencies = { dev = [ | ||
| 15 | + "black", | ||
| 16 | + "mypy", | ||
| 17 | + "pytest", | ||
| 18 | + "responses", | ||
| 19 | + "ruff", | ||
| 20 | + ] } | ||
| 15 | 21 | ||
| 16 | 22 | [project.urls] | |
| 17 | 23 | homepage = "https://replicate.com" | |
@@ -31,11 +37,21 @@ select = [ | |||
| 31 | 37 | "BLE", # flake8-blind-except | |
| 32 | 38 | "FBT", # flake8-boolean-trap | |
| 33 | 39 | "B", # flake8-bugbear | |
| 40 | + "ANN", # flake8-annotations | ||
| 34 | 41 | ] | |
| 35 | 42 | ignore = [ | |
| 36 | - "E501", # Line too long | ||
| 37 | - "S113", # Probable use of requests call without timeout | ||
| 43 | + "E501", # Line too long | ||
| 44 | + "S113", # Probable use of requests call without timeout | ||
| 45 | + "ANN001", # Missing type annotation for function argument | ||
| 46 | + "ANN002", # Missing type annotation for `*args` | ||
| 47 | + "ANN003", # Missing type annotation for `**kwargs` | ||
| 48 | + "ANN101", # Missing type annotation for self in method | ||
| 49 | + "ANN102", # Missing type annotation for cls in classmethod | ||
| 38 | 50 | ] | |
| 39 | 51 | ||
| 40 | 52 | [tool.ruff.per-file-ignores] | |
| 41 | - "tests/*" = ["S101", "S106"] | ||
| 53 | + "tests/*" = [ | ||
| 54 | + "S101", # Use of assert | ||
| 55 | + "S106", # Possible use of hard-coded password function arguments | ||
| 56 | + "ANN201", # Missing return type annotation for public function | ||
| 57 | + ] | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,20 +1,23 @@ | |||
| 1 | - from typing import ForwardRef | ||
| 1 | + from typing import TYPE_CHECKING | ||
| 2 | 2 | ||
| 3 | - import pydantic | ||
| 3 | + if TYPE_CHECKING: | ||
| 4 | + from replicate.client import Client | ||
| 5 | + from replicate.collection import Collection | ||
| 4 | 6 | ||
| 5 | - Client = ForwardRef("Client") | ||
| 6 | - Collection = ForwardRef("Collection") | ||
| 7 | + import pydantic | ||
| 7 | 8 | ||
| 8 | 9 | ||
| 9 | 10 | class BaseModel(pydantic.BaseModel): | |
| 10 | 11 | """ | |
| 11 | 12 | A base class for representing a single object on the server. | |
| 12 | 13 | """ | |
| 13 | 14 | ||
| 14 | - _client: Client = pydantic.PrivateAttr() | ||
| 15 | - _collection: Collection = pydantic.PrivateAttr() | ||
| 15 | + id: str | ||
| 16 | + | ||
| 17 | + _client: "Client" = pydantic.PrivateAttr() | ||
| 18 | + _collection: "Collection" = pydantic.PrivateAttr() | ||
| 16 | 19 | ||
| 17 | - def reload(self): | ||
| 20 | + def reload(self) -> None: | ||
| 18 | 21 | """ | |
| 19 | 22 | Load this object from the server again. | |
| 20 | 23 | """ | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,7 +1,7 @@ | |||
| 1 | 1 | import os | |
| 2 | 2 | import re | |
| 3 | 3 | from json import JSONDecodeError | |
| 4 | - from typing import Any, Iterator, Union | ||
| 4 | + from typing import Any, Dict, Iterator, Optional, Union | ||
| 5 | 5 | ||
| 6 | 6 | import requests | |
| 7 | 7 | from requests.adapters import HTTPAdapter, Retry | |
@@ -14,7 +14,7 @@ | |||
| 14 | 14 | ||
| 15 | 15 | ||
| 16 | 16 | class Client: | |
| 17 | - def __init__(self, api_token=None) -> None: | ||
| 17 | + def __init__(self, api_token: Optional[str] = None) -> None: | ||
| 18 | 18 | super().__init__() | |
| 19 | 19 | # Client is instantiated at import time, so do as little as possible. | |
| 20 | 20 | # This includes resolving environment variables -- they might be set programmatically. | |
@@ -30,7 +30,7 @@ def __init__(self, api_token=None) -> None: | |||
| 30 | 30 | total=5, | |
| 31 | 31 | backoff_factor=2, | |
| 32 | 32 | # Only retry 500s on GET so we don't unintionally mutute data | |
| 33 | - method_whitelist=["GET"], | ||
| 33 | + allowed_methods=["GET"], | ||
| 34 | 34 | # https://support.cloudflare.com/hc/en-us/articles/115003011431-Troubleshooting-Cloudflare-5XX-errors | |
| 35 | 35 | status_forcelist=[ | |
| 36 | 36 | 429, | |
@@ -54,14 +54,14 @@ def __init__(self, api_token=None) -> None: | |||
| 54 | 54 | write_retries = Retry( | |
| 55 | 55 | total=5, | |
| 56 | 56 | backoff_factor=2, | |
| 57 | - method_whitelist=["POST", "PUT"], | ||
| 57 | + allowed_methods=["POST", "PUT"], | ||
| 58 | 58 | # Only retry POST/PUT requests on rate limits, so we don't unintionally mutute data | |
| 59 | 59 | status_forcelist=[429], | |
| 60 | 60 | ) | |
| 61 | 61 | self.write_session.mount("http://", HTTPAdapter(max_retries=write_retries)) | |
| 62 | 62 | self.write_session.mount("https://", HTTPAdapter(max_retries=write_retries)) | |
| 63 | 63 | ||
| 64 | - def _request(self, method: str, path: str, **kwargs): | ||
| 64 | + def _request(self, method: str, path: str, **kwargs) -> requests.Response: | ||
| 65 | 65 | # from requests.Session | |
| 66 | 66 | if method in ["GET", "OPTIONS"]: | |
| 67 | 67 | kwargs.setdefault("allow_redirects", True) | |
@@ -81,13 +81,13 @@ def _request(self, method: str, path: str, **kwargs): | |||
| 81 | 81 | raise ReplicateError(f"HTTP error: {resp.status_code, resp.reason}") | |
| 82 | 82 | return resp | |
| 83 | 83 | ||
| 84 | - def _headers(self): | ||
| 84 | + def _headers(self) -> Dict[str, str]: | ||
| 85 | 85 | return { | |
| 86 | 86 | "Authorization": f"Token {self._api_token()}", | |
| 87 | 87 | "User-Agent": f"replicate-python@{__version__}", | |
| 88 | 88 | } | |
| 89 | 89 | ||
| 90 | - def _api_token(self): | ||
| 90 | + def _api_token(self) -> str: | ||
| 91 | 91 | token = self.api_token | |
| 92 | 92 | # Evaluate lazily in case environment variable is set with dotenv, or something | |
| 93 | 93 | if token is None: | |
@@ -112,7 +112,7 @@ def predictions(self) -> PredictionCollection: | |||
| 112 | 112 | def trainings(self) -> TrainingCollection: | |
| 113 | 113 | return TrainingCollection(client=self) | |
| 114 | 114 | ||
| 115 | - def run(self, model_version, **kwargs) -> Union[Any, Iterator[Any]]: | ||
| 115 | + def run(self, model_version: str, **kwargs) -> Union[Any, Iterator[Any]]: | ||
| 116 | 116 | """ | |
| 117 | 117 | Run a model in the format owner/name:version. | |
| 118 | 118 | """ | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,38 +1,54 @@ | |||
| 1 | + import abc | ||
| 2 | + from typing import TYPE_CHECKING, Dict, Generic, List, TypeVar, Union, cast | ||
| 3 | + | ||
| 4 | + if TYPE_CHECKING: | ||
| 5 | + from replicate.client import Client | ||
| 6 | + | ||
| 1 | 7 | from replicate.base_model import BaseModel | |
| 2 | 8 | ||
| 9 | + Model = TypeVar("Model", bound=BaseModel) | ||
| 10 | + | ||
| 3 | 11 | ||
| 4 | - class Collection: | ||
| 12 | + class Collection(abc.ABC, Generic[Model]): | ||
| 5 | 13 | """ | |
| 6 | 14 | A base class for representing all objects of a particular type on the | |
| 7 | 15 | server. | |
| 8 | 16 | """ | |
| 9 | 17 | ||
| 10 | - model: BaseModel = None | ||
| 11 | - | ||
| 12 | - def __init__(self, client=None): | ||
| 18 | + def __init__(self, client: "Client") -> None: | ||
| 13 | 19 | self._client = client | |
| 14 | 20 | ||
| 15 | - def list(self): | ||
| 16 | - raise NotImplementedError | ||
| 21 | + @abc.abstractproperty | ||
| 22 | + def model(self) -> Model: | ||
| 23 | + pass | ||
| 24 | + | ||
| 25 | + @abc.abstractmethod | ||
| 26 | + def list(self) -> List[Model]: | ||
| 27 | + pass | ||
| 17 | 28 | ||
| 18 | - def get(self, key): | ||
| 19 | - raise NotImplementedError | ||
| 29 | + @abc.abstractmethod | ||
| 30 | + def get(self, key: str) -> Model: | ||
| 31 | + pass | ||
| 20 | 32 | ||
| 21 | - def create(self, attrs=None): | ||
| 22 | - raise NotImplementedError | ||
| 33 | + @abc.abstractmethod | ||
| 34 | + def create(self, **kwargs) -> Model: | ||
| 35 | + pass | ||
| 23 | 36 | ||
| 24 | - def prepare_model(self, attrs): | ||
| 37 | + def prepare_model(self, attrs: Union[Model, Dict]) -> Model: | ||
| 25 | 38 | """ | |
| 26 | 39 | Create a model from a set of attributes. | |
| 27 | 40 | """ | |
| 28 | 41 | if isinstance(attrs, BaseModel): | |
| 29 | 42 | attrs._client = self._client | |
| 30 | 43 | attrs._collection = self | |
| 31 | - return attrs | ||
| 32 | - elif isinstance(attrs, dict): | ||
| 44 | + return cast(Model, attrs) | ||
| 45 | + elif ( | ||
| 46 | + isinstance(attrs, dict) and self.model is not None and callable(self.model) | ||
| 47 | + ): | ||
| 33 | 48 | model = self.model(**attrs) | |
| 34 | 49 | model._client = self._client | |
| 35 | 50 | model._collection = self | |
| 36 | 51 | return model | |
| 37 | 52 | else: | |
| 38 | - raise Exception(f"Can't create {self.model.__name__} from {attrs}") | ||
| 53 | + name = self.model.__name__ if hasattr(self.model, "__name__") else "model" | ||
| 54 | + raise Exception(f"Can't create {name} from {attrs}") | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,11 +2,12 @@ | |||
| 2 | 2 | import io | |
| 3 | 3 | import mimetypes | |
| 4 | 4 | import os | |
| 5 | + from typing import Optional | ||
| 5 | 6 | ||
| 6 | 7 | import requests | |
| 7 | 8 | ||
| 8 | 9 | ||
| 9 | - def upload_file(fh: io.IOBase, output_file_prefix: str = None) -> str: | ||
| 10 | + def upload_file(fh: io.IOBase, output_file_prefix: Optional[str] = None) -> str: | ||
| 10 | 11 | """ | |
| 11 | 12 | Lifted straight from cog.files | |
| 12 | 13 | """ | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -11,7 +11,9 @@ | |||
| 11 | 11 | has_numpy = False | |
| 12 | 12 | ||
| 13 | 13 | ||
| 14 | - def encode_json(obj: Any, upload_file: Callable[[io.IOBase], str]) -> Any: | ||
| 14 | + def encode_json( | ||
| 15 | + obj: Any, upload_file: Callable[[io.IOBase], str] # noqa: ANN401 | ||
| 16 | + ) -> Any: # noqa: ANN401 | ||
| 15 | 17 | """ | |
| 16 | 18 | Returns a JSON-compatible version of the object. Effectively the same thing as cog.json.encode_json. | |
| 17 | 19 | """ | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,3 +1,5 @@ | |||
| 1 | + from typing import Dict, List, Union | ||
| 2 | + | ||
| 1 | 3 | from replicate.base_model import BaseModel | |
| 2 | 4 | from replicate.collection import Collection | |
| 3 | 5 | from replicate.exceptions import ReplicateException | |
@@ -8,21 +10,34 @@ class Model(BaseModel): | |||
| 8 | 10 | username: str | |
| 9 | 11 | name: str | |
| 10 | 12 | ||
| 11 | - def predict(self, *args, **kwargs): | ||
| 13 | + def predict(self, *args, **kwargs) -> None: | ||
| 12 | 14 | raise ReplicateException( | |
| 13 | 15 | "The `model.predict()` method has been removed, because it's unstable: if a new version of the model you're using is pushed and its API has changed, your code may break. Use `version.predict()` instead. See https://github.com/replicate/replicate-python#readme" | |
| 14 | 16 | ) | |
| 15 | 17 | ||
| 16 | 18 | @property | |
| 17 | - def versions(self): | ||
| 19 | + def versions(self) -> VersionCollection: | ||
| 18 | 20 | return VersionCollection(client=self._client, model=self) | |
| 19 | 21 | ||
| 20 | 22 | ||
| 21 | 23 | class ModelCollection(Collection): | |
| 22 | 24 | model = Model | |
| 23 | 25 | ||
| 26 | + def list(self) -> List[Model]: | ||
| 27 | + raise NotImplementedError() | ||
| 28 | + | ||
| 24 | 29 | def get(self, name: str) -> Model: | |
| 25 | 30 | # TODO: fetch model from server | |
| 26 | 31 | # TODO: support permanent IDs | |
| 27 | 32 | username, name = name.split("/") | |
| 28 | 33 | return self.prepare_model({"username": username, "name": name}) | |
| 34 | + | ||
| 35 | + def create(self, **kwargs) -> Model: | ||
| 36 | + raise NotImplementedError() | ||
| 37 | + | ||
| 38 | + def prepare_model(self, attrs: Union[Model, Dict]) -> Model: | ||
| 39 | + if isinstance(attrs, BaseModel): | ||
| 40 | + attrs.id = f"{attrs.username}/{attrs.name}" | ||
| 41 | + elif isinstance(attrs, dict): | ||
| 42 | + attrs["id"] = f"{attrs['username']}/{attrs['name']}" | ||
| 43 | + return super().prepare_model(attrs) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -21,7 +21,7 @@ class Prediction(BaseModel): | |||
| 21 | 21 | created_at: Optional[str] | |
| 22 | 22 | completed_at: Optional[str] | |
| 23 | 23 | ||
| 24 | - def wait(self): | ||
| 24 | + def wait(self) -> None: | ||
| 25 | 25 | """Wait for prediction to finish.""" | |
| 26 | 26 | while self.status not in ["succeeded", "failed", "canceled"]: | |
| 27 | 27 | time.sleep(self._client.poll_interval) | |
@@ -47,21 +47,38 @@ def output_iterator(self) -> Iterator[Any]: | |||
| 47 | 47 | for output in new_output: | |
| 48 | 48 | yield output | |
| 49 | 49 | ||
| 50 | - def cancel(self): | ||
| 50 | + def cancel(self) -> None: | ||
| 51 | 51 | """Cancel a currently running prediction""" | |
| 52 | 52 | self._client._request("POST", f"/v1/predictions/{self.id}/cancel") | |
| 53 | 53 | ||
| 54 | 54 | ||
| 55 | 55 | class PredictionCollection(Collection): | |
| 56 | 56 | model = Prediction | |
| 57 | 57 | ||
| 58 | - def create( | ||
| 58 | + def list(self) -> List[Prediction]: | ||
| 59 | + resp = self._client._request("GET", "/v1/predictions") | ||
| 60 | + # TODO: paginate | ||
| 61 | + predictions = resp.json()["results"] | ||
| 62 | + for prediction in predictions: | ||
| 63 | + # HACK: resolve this? make it lazy somehow? | ||
| 64 | + del prediction["version"] | ||
| 65 | + return [self.prepare_model(obj) for obj in predictions] | ||
| 66 | + | ||
| 67 | + def get(self, id: str) -> Prediction: | ||
| 68 | + resp = self._client._request("GET", f"/v1/predictions/{id}") | ||
| 69 | + obj = resp.json() | ||
| 70 | + # HACK: resolve this? make it lazy somehow? | ||
| 71 | + del obj["version"] | ||
| 72 | + return self.prepare_model(obj) | ||
| 73 | + | ||
| 74 | + def create( # type: ignore | ||
| 59 | 75 | self, | |
| 60 | 76 | version: Version, | |
| 61 | 77 | input: Dict[str, Any], | |
| 62 | 78 | webhook: Optional[str] = None, | |
| 63 | 79 | webhook_completed: Optional[str] = None, | |
| 64 | 80 | webhook_events_filter: Optional[List[str]] = None, | |
| 81 | + **kwargs, | ||
| 65 | 82 | ) -> Prediction: | |
| 66 | 83 | input = encode_json(input, upload_file=upload_file) | |
| 67 | 84 | body = { | |
@@ -83,19 +100,3 @@ def create( | |||
| 83 | 100 | obj = resp.json() | |
| 84 | 101 | obj["version"] = version | |
| 85 | 102 | return self.prepare_model(obj) | |
| 86 | - | ||
| 87 | - def get(self, id: str) -> Prediction: | ||
| 88 | - resp = self._client._request("GET", f"/v1/predictions/{id}") | ||
| 89 | - obj = resp.json() | ||
| 90 | - # HACK: resolve this? make it lazy somehow? | ||
| 91 | - del obj["version"] | ||
| 92 | - return self.prepare_model(obj) | ||
| 93 | - | ||
| 94 | - def list(self) -> List[Prediction]: | ||
| 95 | - resp = self._client._request("GET", "/v1/predictions") | ||
| 96 | - # TODO: paginate | ||
| 97 | - predictions = resp.json()["results"] | ||
| 98 | - for prediction in predictions: | ||
| 99 | - # HACK: resolve this? make it lazy somehow? | ||
| 100 | - del prediction["version"] | ||
| 101 | - return [self.prepare_model(obj) for obj in predictions] | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -3,12 +3,15 @@ | |||
| 3 | 3 | # TODO: this code is shared with replicate's backend. Maybe we should put it in the Cog Python package as the source of truth? | |
| 4 | 4 | ||
| 5 | 5 | ||
| 6 | - def version_has_no_array_type(cog_version): | ||
| 6 | + def version_has_no_array_type(cog_version: str) -> bool: | ||
| 7 | 7 | """Iterators have x-cog-array-type=iterator in the schema from 0.3.9 onward""" | |
| 8 | 8 | return version.parse(cog_version) < version.parse("0.3.9") | |
| 9 | 9 | ||
| 10 | 10 | ||
| 11 | - def make_schema_backwards_compatible(schema, version): | ||
| 11 | + def make_schema_backwards_compatible( | ||
| 12 | + schema: dict, | ||
| 13 | + version: str, | ||
| 14 | + ) -> dict: | ||
| 12 | 15 | """A place to add backwards compatibility logic for our openapi schema""" | |
| 13 | 16 | # If the top-level output is an array, assume it is an iterator in old versions which didn't have an array type | |
| 14 | 17 | if version_has_no_array_type(version): | |
| Back | FazBrowse Home | New Git URL |
0 commit comments