Skip to content

Commit

Permalink
Remove support for py <3.6, mypy, lots of cleanup (#19)
Browse files Browse the repository at this point in the history
* python 3.6 + typing (#25)

- Formally require Python 3.6 or later. This never worked under Python 2.7.
- Drop now-unnecessary `__future__` and `six` references.
- Use `py.typed` to declare that we support type hints.
- Fix a variety of mypy errors.
- Don't use star imports, so that other packages can tell if they're making an invalid reference.
- Drop support for non-`requests` HTTP clients.
- Drop `EngineAPIResource.update`; it could never have worked.

Tested against primaryapi and engineapi in staging, and it doesn't break them. After this, `mypy ./openai` runs clean.

* delete unused code (#27)

- Delete `ListObject`. It has some typing errors that suggest it never worked, and it is unused as far as I can tell.
- Delete `VerifyMixin`. It is unused.
- Delete `CardError`. It is unused and smells of rotten pasta.
- Delete `OpenAIErrorWithParamCode`, which only has one subclass, `InvalidRequestError`, and make `InvalidRequestError` a direct subclass of `OpenAIError`. Currrently, `OpenAIErrorWithParamCode` depends on the internal structure of `InvalidRequestError` so they're not independent.

* boring formatting and typing fixes (#26)

These are another step towards being able to enforce black, flake8, and mypy on CI.

* fix more typing issues in prep for CI (#28)

- Simplify `platform.XXX` calls. As far as I know these can't raise an exception in Python 3.
- Simplify `EngineAPIResource` constructor and remove its unused `retrieve` method.

* Update readme, bump version

* typo fix

Co-authored-by: Madeleine Thompson <madeleineth@gmail.com>
Co-authored-by: Madeleine Thompson <madeleine@openai.com>
  • Loading branch information
3 people authored Jun 11, 2021
1 parent d53d9ef commit 40c32f9
Show file tree
Hide file tree
Showing 33 changed files with 153 additions and 1,734 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ openai api completions.create -e ada -p "Hello world"

## Requirements

- Python 3.4+
- Python 3.6+

In general we want to support the versions of Python that our
customers are using, so if you run into issues with any version
Expand Down
15 changes: 11 additions & 4 deletions openai/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from __future__ import absolute_import, division, print_function

import os

# OpenAI Python bindings.
Expand Down Expand Up @@ -27,6 +25,15 @@
log = None

# API resources
from openai.api_resources import * # noqa
from openai.api_resources import ( # noqa: E402,F401
Answer,
Classification,
Completion,
Engine,
ErrorObject,
File,
FineTune,
Snapshot,
)

from openai.error import OpenAIError, APIError, InvalidRequestError
from openai.error import OpenAIError, APIError, InvalidRequestError # noqa: E402,F401
40 changes: 15 additions & 25 deletions openai/api_requestor.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
from __future__ import absolute_import, division, print_function

import calendar
import datetime
import json
import platform
import time
import uuid
import warnings
import gzip
from io import BytesIO
from collections import OrderedDict
from urllib.parse import urlencode, urlsplit, urlunsplit

import openai
from openai import error, http_client, version, util, six
from openai import error, http_client, version, util
from openai.multipart_data_generator import MultipartDataGenerator
from openai.six.moves.urllib.parse import urlencode, urlsplit, urlunsplit
from openai.openai_response import OpenAIResponse
from openai.upload_progress import BufferReader


def _encode_datetime(dttime):
def _encode_datetime(dttime) -> int:
utc_timestamp: float
if dttime.tzinfo and dttime.tzinfo.utcoffset(dttime) is not None:
utc_timestamp = calendar.timegm(dttime.utctimetuple())
else:
Expand All @@ -30,14 +28,13 @@ def _encode_datetime(dttime):

def _encode_nested_dict(key, data, fmt="%s[%s]"):
d = OrderedDict()
for subkey, subvalue in six.iteritems(data):
for subkey, subvalue in data.items():
d[fmt % (key, subkey)] = subvalue
return d


def _api_encode(data):
for key, value in six.iteritems(data):
key = util.utf8(key)
for key, value in data.items():
if value is None:
continue
elif hasattr(value, "openai_id"):
Expand All @@ -49,15 +46,15 @@ def _api_encode(data):
for k, v in _api_encode(subdict):
yield (k, v)
else:
yield ("%s[%d]" % (key, i), util.utf8(sv))
yield ("%s[%d]" % (key, i), sv)
elif isinstance(value, dict):
subdict = _encode_nested_dict(key, value)
for subkey, subvalue in _api_encode(subdict):
yield (subkey, subvalue)
elif isinstance(value, datetime.datetime):
yield (key, _encode_datetime(value))
else:
yield (key, util.utf8(value))
yield (key, value)


def _build_api_url(url, query):
Expand All @@ -81,7 +78,7 @@ def parse_stream(rbody):
yield line


class APIRequestor(object):
class APIRequestor:
def __init__(
self, key=None, client=None, api_base=None, api_version=None, organization=None
):
Expand Down Expand Up @@ -205,20 +202,13 @@ def request_headers(self, api_key, method, extra):

ua = {
"bindings_version": version.VERSION,
"httplib": self._client.name,
"lang": "python",
"lang_version": platform.python_version(),
"platform": platform.platform(),
"publisher": "openai",
"httplib": self._client.name,
"uname": " ".join(platform.uname()),
}
for attr, func in [
["lang_version", platform.python_version],
["platform", platform.platform],
["uname", lambda: " ".join(platform.uname())],
]:
try:
val = func()
except Exception as e:
val = "!! %s" % (e,)
ua[attr] = val
if openai.app_info:
ua["application"] = openai.app_info

Expand Down Expand Up @@ -257,7 +247,7 @@ def request_raw(

if my_api_key is None:
raise error.AuthenticationError(
"No API key provided. (HINT: set your API key using in code using "
"No API key provided. (HINT: set your API key in code using "
'"openai.api_key = <API-KEY>", or you can set the environment variable OPENAI_API_KEY=<API-KEY>). You can generate API keys '
"in the OpenAI web interface. See https://onboard.openai.com "
"for details, or email support@openai.com if you have any "
Expand Down Expand Up @@ -320,7 +310,7 @@ def request_raw(

headers = self.request_headers(my_api_key, method, headers)
if supplied_headers is not None:
for key, value in six.iteritems(supplied_headers):
for key, value in supplied_headers.items():
headers[key] = value

util.log_info("Request to OpenAI API", method=method, path=abs_url)
Expand Down
16 changes: 8 additions & 8 deletions openai/api_resources/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from openai.api_resources.completion import Completion
from openai.api_resources.engine import Engine
from openai.api_resources.error_object import ErrorObject
from openai.api_resources.file import File
from openai.api_resources.answer import Answer
from openai.api_resources.classification import Classification
from openai.api_resources.snapshot import Snapshot
from openai.api_resources.fine_tune import FineTune
from openai.api_resources.completion import Completion # noqa: F401
from openai.api_resources.engine import Engine # noqa: F401
from openai.api_resources.error_object import ErrorObject # noqa: F401
from openai.api_resources.file import File # noqa: F401
from openai.api_resources.answer import Answer # noqa: F401
from openai.api_resources.classification import Classification # noqa: F401
from openai.api_resources.snapshot import Snapshot # noqa: F401
from openai.api_resources.fine_tune import FineTune # noqa: F401
26 changes: 5 additions & 21 deletions openai/api_resources/abstract/__init__.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,12 @@
from __future__ import absolute_import, division, print_function

# flake8: noqa

from openai.api_resources.abstract.api_resource import APIResource
from openai.api_resources.abstract.singleton_api_resource import (
SingletonAPIResource,
)

from openai.api_resources.abstract.createable_api_resource import (
CreateableAPIResource,
)
from openai.api_resources.abstract.updateable_api_resource import (
UpdateableAPIResource,
)
from openai.api_resources.abstract.deletable_api_resource import (
DeletableAPIResource,
)
from openai.api_resources.abstract.listable_api_resource import (
ListableAPIResource,
)
from openai.api_resources.abstract.verify_mixin import VerifyMixin

from openai.api_resources.abstract.singleton_api_resource import SingletonAPIResource
from openai.api_resources.abstract.createable_api_resource import CreateableAPIResource
from openai.api_resources.abstract.updateable_api_resource import UpdateableAPIResource
from openai.api_resources.abstract.deletable_api_resource import DeletableAPIResource
from openai.api_resources.abstract.listable_api_resource import ListableAPIResource
from openai.api_resources.abstract.custom_method import custom_method

from openai.api_resources.abstract.nested_resource_class_methods import (
nested_resource_class_methods,
)
12 changes: 5 additions & 7 deletions openai/api_resources/abstract/api_resource.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from __future__ import absolute_import, division, print_function
from urllib.parse import quote_plus

from openai import api_requestor, error, six, util
from openai import api_requestor, error, util
from openai.openai_object import OpenAIObject
from openai.six.moves.urllib.parse import quote_plus


class APIResource(OpenAIObject):
Expand All @@ -28,21 +27,20 @@ def class_url(cls):
)
# Namespaces are separated in object names with periods (.) and in URLs
# with forward slashes (/), so replace the former with the latter.
base = cls.OBJECT_NAME.replace(".", "/")
base = cls.OBJECT_NAME.replace(".", "/") # type: ignore
return "/%s/%ss" % (cls.api_prefix, base)

def instance_url(self):
id = self.get("id")

if not isinstance(id, six.string_types):
if not isinstance(id, str):
raise error.InvalidRequestError(
"Could not determine which URL to request: %s instance "
"has invalid ID: %r, %s. ID should be of type `str` (or"
" `unicode`)" % (type(self).__name__, id, type(id)),
"id",
)

id = util.utf8(id)
base = self.class_url()
extn = quote_plus(id)
return "%s/%s" % (base, extn)
Expand All @@ -60,7 +58,7 @@ def _static_request(
request_id=None,
api_version=None,
organization=None,
**params
**params,
):
requestor = api_requestor.APIRequestor(
api_key,
Expand Down
2 changes: 1 addition & 1 deletion openai/api_resources/abstract/createable_api_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def create(
request_id=None,
api_version=None,
organization=None,
**params
**params,
):
requestor = api_requestor.APIRequestor(
api_key,
Expand Down
9 changes: 3 additions & 6 deletions openai/api_resources/abstract/custom_method.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import absolute_import, division, print_function
from urllib.parse import quote_plus

from openai import util
from openai.six.moves.urllib.parse import quote_plus


def custom_method(name, http_verb, http_path=None):
Expand All @@ -17,7 +16,7 @@ def wrapper(cls):
def custom_method_request(cls, sid, **params):
url = "%s/%s/%s" % (
cls.class_url(),
quote_plus(util.utf8(sid)),
quote_plus(sid),
http_path,
)
return cls._static_request(http_verb, url, **params)
Expand All @@ -33,9 +32,7 @@ def custom_method_request(cls, sid, **params):
# that the new class method is called when the original method is
# called as a class method.
setattr(cls, "_cls_" + name, classmethod(custom_method_request))
instance_method = util.class_method_variant("_cls_" + name)(
existing_method
)
instance_method = util.class_method_variant("_cls_" + name)(existing_method)
setattr(cls, name, instance_method)

return cls
Expand Down
5 changes: 2 additions & 3 deletions openai/api_resources/abstract/deletable_api_resource.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
from __future__ import absolute_import, division, print_function
from urllib.parse import quote_plus

from openai import util
from openai.api_resources.abstract.api_resource import APIResource
from openai.six.moves.urllib.parse import quote_plus


class DeletableAPIResource(APIResource):
@classmethod
def _cls_delete(cls, sid, **params):
url = "%s/%s" % (cls.class_url(), quote_plus(util.utf8(sid)))
url = "%s/%s" % (cls.class_url(), quote_plus(sid))
return cls._static_request("delete", url, **params)

@util.class_method_variant("_cls_delete")
Expand Down
53 changes: 8 additions & 45 deletions openai/api_resources/abstract/engine_api_resource.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import time
from typing import Optional
from urllib.parse import quote_plus

from openai import api_requestor, error, six, util
from openai import api_requestor, error, util
from openai.api_resources.abstract.api_resource import APIResource
from openai.six.moves.urllib.parse import quote_plus

MAX_TIMEOUT = 20

Expand All @@ -11,56 +12,20 @@ class EngineAPIResource(APIResource):
engine_required = True
plain_old_data = False

def __init__(self, *args, **kwargs):
engine = kwargs.pop("engine", None)
super().__init__(*args, engine=engine, **kwargs)
def __init__(self, engine: Optional[str] = None, **kwargs):
super().__init__(engine=engine, **kwargs)

@classmethod
def class_url(cls, engine=None):
def class_url(cls, engine: Optional[str] = None):
# Namespaces are separated in object names with periods (.) and in URLs
# with forward slashes (/), so replace the former with the latter.
base = cls.OBJECT_NAME.replace(".", "/")
base = cls.OBJECT_NAME.replace(".", "/") # type: ignore
if engine is None:
return "/%s/%ss" % (cls.api_prefix, base)

engine = util.utf8(engine)
extn = quote_plus(engine)
return "/%s/engines/%s/%ss" % (cls.api_prefix, extn, base)

@classmethod
def retrieve(cls, id, api_key=None, request_id=None, **params):
engine = params.pop("engine", None)
instance = cls(id, api_key, engine=engine, **params)
instance.refresh(request_id=request_id)
return instance

@classmethod
def update(
cls,
api_key=None,
api_base=None,
idempotency_key=None,
request_id=None,
api_version=None,
organization=None,
**params,
):
# TODO max
engine_id = params.get("id")
replicas = params.get("replicas")

engine = EngineAPIResource(id=id)

requestor = api_requestor.APIRequestor(
api_key,
api_base=api_base,
api_version=api_version,
organization=organization,
)
url = cls.class_url(engine)
headers = util.populate_headers(idempotency_key, request_id)
response, _, api_key = requestor.request("post", url, params, headers)

@classmethod
def create(
cls,
Expand Down Expand Up @@ -138,15 +103,14 @@ def create(
def instance_url(self):
id = self.get("id")

if not isinstance(id, six.string_types):
if not isinstance(id, str):
raise error.InvalidRequestError(
"Could not determine which URL to request: %s instance "
"has invalid ID: %r, %s. ID should be of type `str` (or"
" `unicode`)" % (type(self).__name__, id, type(id)),
"id",
)

id = util.utf8(id)
base = self.class_url(self.engine)
extn = quote_plus(id)
url = "%s/%s" % (base, extn)
Expand All @@ -158,7 +122,6 @@ def instance_url(self):
return url

def wait(self, timeout=None):
engine = self.engine
start = time.time()
while self.status != "complete":
self.timeout = (
Expand Down
Loading

0 comments on commit 40c32f9

Please sign in to comment.