arkindex-client 1.0.16__py3-none-any.whl → 1.1.1__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.
@@ -0,0 +1,132 @@
1
+ # -*- coding: utf-8 -*-
2
+ import http
3
+ from importlib.metadata import version
4
+
5
+ import requests
6
+
7
+ from arkindex import exceptions
8
+ from arkindex.client import decoders
9
+
10
+ REQUEST_TIMEOUT = (30, 60)
11
+
12
+
13
+ class BlockAllCookies(http.cookiejar.CookiePolicy):
14
+ """
15
+ A cookie policy that rejects all cookies.
16
+ Used to override the default `requests` behavior.
17
+ """
18
+
19
+ return_ok = set_ok = domain_return_ok = path_return_ok = (
20
+ lambda self, *args, **kwargs: False
21
+ )
22
+ netscape = True
23
+ rfc2965 = hide_cookie2 = False
24
+
25
+
26
+ class BaseTransport:
27
+ schemes = None
28
+
29
+ def send(self, method, url, query_params=None, content=None, encoding=None):
30
+ raise NotImplementedError()
31
+
32
+
33
+ class HTTPTransport(BaseTransport):
34
+ schemes = ["http", "https"]
35
+ default_decoders = [
36
+ decoders.JSONDecoder(),
37
+ decoders.TextDecoder(),
38
+ decoders.DownloadDecoder(),
39
+ ]
40
+
41
+ def __init__(
42
+ self,
43
+ auth=None,
44
+ decoders=None,
45
+ headers=None,
46
+ session=None,
47
+ allow_cookies=True,
48
+ verify=True,
49
+ ):
50
+ if session is None:
51
+ session = requests.Session()
52
+ if auth is not None:
53
+ session.auth = auth
54
+ if not allow_cookies:
55
+ session.cookies.set_policy(BlockAllCookies())
56
+
57
+ self.session = session
58
+ self.verify = verify
59
+ self.decoders = list(decoders) if decoders else list(self.default_decoders)
60
+
61
+ client_version = version("arkindex-client")
62
+ self.headers = {
63
+ "accept": ", ".join([decoder.media_type for decoder in self.decoders]),
64
+ "user-agent": f"arkindex-client/{client_version}",
65
+ }
66
+ if headers:
67
+ self.headers.update({key.lower(): value for key, value in headers.items()})
68
+
69
+ def send(self, method, url, query_params=None, content=None, encoding=None):
70
+ options = self.get_request_options(query_params, content, encoding)
71
+ response = self.session.request(method, url, **options)
72
+ result = self.decode_response_content(response)
73
+
74
+ if 400 <= response.status_code <= 599:
75
+ title = "%d %s" % (response.status_code, response.reason)
76
+ raise exceptions.ErrorResponse(
77
+ title=title, status_code=response.status_code, content=result
78
+ )
79
+
80
+ return result
81
+
82
+ def get_decoder(self, content_type=None):
83
+ """
84
+ Given the value of a 'Content-Type' header, return the appropriate
85
+ decoder for handling the response content.
86
+ """
87
+ if content_type is None:
88
+ return self.decoders[0]
89
+
90
+ content_type = content_type.split(";")[0].strip().lower()
91
+ main_type = content_type.split("/")[0] + "/*"
92
+ wildcard_type = "*/*"
93
+
94
+ for codec in self.decoders:
95
+ if codec.media_type in (content_type, main_type, wildcard_type):
96
+ return codec
97
+
98
+ text = (
99
+ "Unsupported encoding '%s' in response Content-Type header." % content_type
100
+ )
101
+ message = exceptions.ErrorMessage(text=text, code="cannot-decode-response")
102
+ raise exceptions.ClientError(messages=[message])
103
+
104
+ def get_request_options(self, query_params=None, content=None, encoding=None):
105
+ """
106
+ Return the 'options' for sending the outgoing request.
107
+ """
108
+ options = {
109
+ "headers": dict(self.headers),
110
+ "params": query_params,
111
+ "timeout": REQUEST_TIMEOUT,
112
+ "verify": self.verify,
113
+ }
114
+
115
+ if content is not None:
116
+ assert (
117
+ encoding == "application/json"
118
+ ), "Only JSON request bodies are supported"
119
+ options["json"] = content
120
+
121
+ return options
122
+
123
+ def decode_response_content(self, response):
124
+ """
125
+ Given an HTTP response, return the decoded data.
126
+ """
127
+ if not response.content:
128
+ return None
129
+
130
+ content_type = response.headers.get("content-type")
131
+ decoder = self.get_decoder(content_type)
132
+ return decoder.decode(response)
arkindex/compat.py ADDED
@@ -0,0 +1,24 @@
1
+ # -*- coding: utf-8 -*-
2
+ try:
3
+ # Ideally we subclass `_TemporaryFileWrapper` to present a clear __repr__
4
+ # for downloaded files.
5
+ from tempfile import _TemporaryFileWrapper
6
+
7
+ class DownloadedFile(_TemporaryFileWrapper):
8
+ basename = None
9
+
10
+ def __repr__(self):
11
+ state = "closed" if self.closed else "open"
12
+ mode = "" if self.closed else " '%s'" % self.file.mode
13
+ return "<DownloadedFile '%s', %s%s>" % (self.name, state, mode)
14
+
15
+ def __str__(self):
16
+ return self.__repr__()
17
+
18
+ except ImportError:
19
+ # On some platforms (eg GAE) the private _TemporaryFileWrapper may not be
20
+ # available, just use the standard `NamedTemporaryFile` function
21
+ # in this case.
22
+ import tempfile
23
+
24
+ DownloadedFile = tempfile.NamedTemporaryFile
arkindex/document.py ADDED
@@ -0,0 +1,212 @@
1
+ # -*- coding: utf-8 -*-
2
+ import collections
3
+ import re
4
+ import typing
5
+
6
+ LinkInfo = collections.namedtuple("LinkInfo", ["link", "name", "sections"])
7
+
8
+
9
+ class Document:
10
+ def __init__(
11
+ self,
12
+ content: typing.Sequence[typing.Union["Section", "Link"]] = None,
13
+ url: str = "",
14
+ title: str = "",
15
+ description: str = "",
16
+ version: str = "",
17
+ ):
18
+ content = [] if (content is None) else list(content)
19
+
20
+ # Ensure all names within a document are unique.
21
+ seen_fields = set()
22
+ seen_sections = set()
23
+ for item in content:
24
+ if isinstance(item, Link):
25
+ msg = 'Link "%s" in Document must have a unique name.'
26
+ assert item.name not in seen_fields, msg % item.name
27
+ seen_fields.add(item.name)
28
+ else:
29
+ msg = 'Section "%s" in Document must have a unique name.'
30
+ assert item.name not in seen_sections, msg % item.name
31
+ seen_sections.add(item.name)
32
+
33
+ self.content = content
34
+ self.url = url
35
+ self.title = title
36
+ self.description = description
37
+ self.version = version
38
+
39
+ def get_links(self):
40
+ return [item for item in self.content if isinstance(item, Link)]
41
+
42
+ def get_sections(self):
43
+ return [item for item in self.content if isinstance(item, Section)]
44
+
45
+ def walk_links(self):
46
+ link_info_list = []
47
+ for item in self.content:
48
+ if isinstance(item, Link):
49
+ link_info = LinkInfo(link=item, name=item.name, sections=())
50
+ link_info_list.append(link_info)
51
+ else:
52
+ link_info_list.extend(item.walk_links())
53
+ return link_info_list
54
+
55
+
56
+ class Section:
57
+ def __init__(
58
+ self,
59
+ name: str,
60
+ content: typing.Sequence[typing.Union["Section", "Link"]] = None,
61
+ title: str = "",
62
+ description: str = "",
63
+ ):
64
+ content = [] if (content is None) else list(content)
65
+
66
+ # Ensure all names within a section are unique.
67
+ seen_fields = set()
68
+ seen_sections = set()
69
+ for item in content:
70
+ if isinstance(item, Link):
71
+ msg = 'Link "%s" in Section "%s" must have a unique name.'
72
+ assert item.name not in seen_fields, msg % (item.name, name)
73
+ seen_fields.add(item.name)
74
+ else:
75
+ msg = 'Section "%s" in Section "%s" must have a unique name.'
76
+ assert item.name not in seen_sections, msg % (item.name, name)
77
+ seen_sections.add(item.name)
78
+
79
+ self.content = content
80
+ self.name = name
81
+ self.title = title
82
+ self.description = description
83
+
84
+ def get_links(self):
85
+ return [item for item in self.content if isinstance(item, Link)]
86
+
87
+ def get_sections(self):
88
+ return [item for item in self.content if isinstance(item, Section)]
89
+
90
+ def walk_links(self, previous_sections=()):
91
+ link_info_list = []
92
+ sections = previous_sections + (self,)
93
+ for item in self.content:
94
+ if isinstance(item, Link):
95
+ name = ":".join([section.name for section in sections] + [item.name])
96
+ link_info = LinkInfo(link=item, name=name, sections=sections)
97
+ link_info_list.append(link_info)
98
+ else:
99
+ link_info_list.extend(item.walk_links(previous_sections=sections))
100
+ return link_info_list
101
+
102
+
103
+ class Link:
104
+ """
105
+ Links represent the actions that a client may perform.
106
+ """
107
+
108
+ def __init__(
109
+ self,
110
+ url: str,
111
+ method: str,
112
+ handler: typing.Callable = None,
113
+ name: str = "",
114
+ encoding: str = "",
115
+ response: "Response" = None,
116
+ title: str = "",
117
+ description: str = "",
118
+ fields: typing.Sequence["Field"] = None,
119
+ ):
120
+ method = method.upper()
121
+ fields = [] if (fields is None) else list(fields)
122
+
123
+ url_path_names = set(
124
+ [item.strip("{}").lstrip("+") for item in re.findall("{[^}]*}", url)]
125
+ )
126
+ path_fields = [field for field in fields if field.location == "path"]
127
+ body_fields = [field for field in fields if field.location == "body"]
128
+
129
+ assert method in (
130
+ "GET",
131
+ "POST",
132
+ "PUT",
133
+ "PATCH",
134
+ "DELETE",
135
+ "OPTIONS",
136
+ "HEAD",
137
+ "TRACE",
138
+ )
139
+ assert len(body_fields) < 2
140
+ if body_fields:
141
+ assert encoding
142
+ for field in path_fields:
143
+ assert field.name in url_path_names
144
+
145
+ # Add in path fields for any "{param}" items that don't already have
146
+ # a corresponding path field.
147
+ for path_name in url_path_names:
148
+ if path_name not in [field.name for field in path_fields]:
149
+ fields += [Field(name=path_name, location="path", required=True)]
150
+
151
+ self.url = url
152
+ self.method = method
153
+ self.handler = handler
154
+ self.name = name if name else handler.__name__
155
+ self.encoding = encoding
156
+ self.response = response
157
+ self.title = title
158
+ self.description = description
159
+ self.fields = fields
160
+
161
+ def get_path_fields(self):
162
+ return [field for field in self.fields if field.location == "path"]
163
+
164
+ def get_query_fields(self):
165
+ return [field for field in self.fields if field.location == "query"]
166
+
167
+ def get_body_field(self):
168
+ for field in self.fields:
169
+ if field.location == "body":
170
+ return field
171
+ return None
172
+
173
+ def get_expanded_body(self):
174
+ field = self.get_body_field()
175
+ if field is None or not hasattr(field.schema, "properties"):
176
+ return None
177
+ return field.schema.properties
178
+
179
+
180
+ class Field:
181
+ def __init__(
182
+ self,
183
+ name: str,
184
+ location: str,
185
+ title: str = "",
186
+ description: str = "",
187
+ required: bool = None,
188
+ schema: typing.Any = None,
189
+ example: typing.Any = None,
190
+ ):
191
+ assert location in ("path", "query", "body", "cookie", "header", "formData")
192
+ if required is None:
193
+ required = True if location in ("path", "body") else False
194
+ if location == "path":
195
+ assert required, "May not set 'required=False' on path fields."
196
+
197
+ self.name = name
198
+ self.title = title
199
+ self.description = description
200
+ self.location = location
201
+ self.required = required
202
+ self.schema = schema
203
+ self.example = example
204
+
205
+
206
+ class Response:
207
+ def __init__(
208
+ self, encoding: str, status_code: int = 200, schema: typing.Any = None
209
+ ):
210
+ self.encoding = encoding
211
+ self.status_code = status_code
212
+ self.schema = schema
arkindex/exceptions.py CHANGED
@@ -1,4 +1,77 @@
1
1
  # -*- coding: utf-8 -*-
2
+ from collections import namedtuple
3
+
4
+ Position = namedtuple("Position", ["line_no", "column_no", "index"])
5
+
6
+
7
+ class ErrorMessage:
8
+ def __init__(self, text, code, index=None, position=None):
9
+ self.text = text
10
+ self.code = code
11
+ self.index = index
12
+ self.position = position
13
+
14
+ def __eq__(self, other):
15
+ return (
16
+ self.text == other.text
17
+ and self.code == other.code
18
+ and self.index == other.index
19
+ and self.position == other.position
20
+ )
21
+
22
+ def __repr__(self):
23
+ return "%s(%s, code=%s, index=%s, position=%s)" % (
24
+ self.__class__.__name__,
25
+ repr(self.text),
26
+ repr(self.code),
27
+ repr(self.index),
28
+ repr(self.position),
29
+ )
30
+
31
+
32
+ class ErrorResponse(Exception):
33
+ """
34
+ Raised when a client request results in an error response being returned.
35
+ """
36
+
37
+ def __init__(self, title, status_code, content):
38
+ self.title = title
39
+ self.status_code = status_code
40
+ self.content = content
41
+
42
+ def __repr__(self):
43
+ return "%s(%s, status_code=%s, content=%s)" % (
44
+ self.__class__.__name__,
45
+ repr(self.title),
46
+ repr(self.status_code),
47
+ repr(self.content),
48
+ )
49
+
50
+ def __str__(self):
51
+ return repr(self.content)
52
+
53
+
54
+ class ClientError(Exception):
55
+ """
56
+ Raised when a client is unable to fulfil an API request.
57
+ """
58
+
59
+ def __init__(self, messages):
60
+ self.messages = messages
61
+ super().__init__(messages)
62
+
63
+ def __repr__(self):
64
+ return "%s(messages=%s)" % (
65
+ self.__class__.__name__,
66
+ repr(self.messages),
67
+ )
68
+
69
+ def __str__(self):
70
+ if len(self.messages) == 1 and not self.messages[0].index:
71
+ return self.messages[0].text
72
+ return str(self.messages)
73
+
74
+
2
75
  class SchemaError(Exception):
3
76
  """
4
77
  Any error occurring during the acquisition and processing of the OpenAPI schema.
arkindex/mock.py CHANGED
@@ -2,7 +2,7 @@
2
2
  import collections
3
3
  import logging
4
4
 
5
- import apistar
5
+ from arkindex.exceptions import ErrorResponse
6
6
 
7
7
  logger = logging.getLogger(__name__)
8
8
 
@@ -36,7 +36,7 @@ class MockApiClient(object):
36
36
  ):
37
37
  """Store a new mock error response for a request on an endpoint"""
38
38
  request = MockRequest(operation_id, body, args, kwargs)
39
- error = apistar.exceptions.ErrorResponse(
39
+ error = ErrorResponse(
40
40
  title=title,
41
41
  status_code=status_code,
42
42
  content=content,
@@ -74,7 +74,7 @@ class MockApiClient(object):
74
74
  logger.error(
75
75
  f"No mock response found for {operation_id} with body={body} args={args} kwargs={kwargs}"
76
76
  )
77
- raise apistar.exceptions.ErrorResponse(
77
+ raise ErrorResponse(
78
78
  title="No mock response found",
79
79
  status_code=400,
80
80
  content="No mock response found",
arkindex/pagination.py CHANGED
@@ -7,9 +7,10 @@ from collections.abc import Iterator, Sized
7
7
  from enum import Enum
8
8
  from urllib.parse import parse_qs, urlsplit
9
9
 
10
- import apistar
11
10
  import requests
12
11
 
12
+ from arkindex.exceptions import ErrorResponse
13
+
13
14
  logger = logging.getLogger(__name__)
14
15
 
15
16
 
@@ -28,12 +29,12 @@ class ResponsePaginator(Sized, Iterator):
28
29
 
29
30
  def __init__(self, client, operation_id, *request_args, **request_kwargs):
30
31
  r"""
31
- :param client apistar.Client: An API client to use to perform requests for each page.
32
- :param \*request_args: Arguments to send to :meth:`apistar.Client.request`.
33
- :param \**request_kwargs: Keyword arguments to send to :meth:`apistar.Client.request`.
32
+ :param client arkindex.ArkindexClient: An API client to use to perform requests for each page.
33
+ :param \*request_args: Arguments to send to :meth:`arkindex.ArkindexClient.request`.
34
+ :param \**request_kwargs: Keyword arguments to send to :meth:`arkindex.ArkindexClient.request`.
34
35
  """
35
36
  self.client = client
36
- """The APIStar client used to perform requests on each page."""
37
+ """The API client used to perform requests on each page."""
37
38
 
38
39
  self.data = {}
39
40
  """Stored data from the last performed request."""
@@ -45,10 +46,10 @@ class ResponsePaginator(Sized, Iterator):
45
46
  """Client operation"""
46
47
 
47
48
  self.request_args = request_args
48
- """Arguments to send to :meth:`apistar.Client.request` with each request."""
49
+ """Arguments to send to :meth:`arkindex.ArkindexClient.request` with each request."""
49
50
 
50
51
  self.request_kwargs = request_kwargs
51
- """Keyword arguments to send to :meth:`apistar.Client.request` with each request."""
52
+ """Keyword arguments to send to :meth:`arkindex.ArkindexClient.request` with each request."""
52
53
 
53
54
  self.mode = None
54
55
  """`page` for PageNumberPagination endpoints or `cursor` for CursorPagination endpoints."""
@@ -172,7 +173,7 @@ class ResponsePaginator(Sized, Iterator):
172
173
  self.pages_loaded += 1
173
174
  return True
174
175
 
175
- except apistar.exceptions.ErrorResponse as e:
176
+ except ErrorResponse as e:
176
177
  logger.warning(f"API Error {e.status_code} on pagination: {e.content}")
177
178
 
178
179
  # Decrement pages counter
File without changes
@@ -0,0 +1,66 @@
1
+ # -*- coding: utf-8 -*-
2
+ import typesystem
3
+
4
+ definitions = typesystem.SchemaDefinitions()
5
+
6
+ JSON_SCHEMA = (
7
+ typesystem.Object(
8
+ properties={
9
+ "$ref": typesystem.String(),
10
+ "type": typesystem.String() | typesystem.Array(items=typesystem.String()),
11
+ "enum": typesystem.Array(unique_items=True, min_items=1),
12
+ "definitions": typesystem.Object(
13
+ additional_properties=typesystem.Reference(
14
+ "JSONSchema", definitions=definitions
15
+ )
16
+ ),
17
+ # String
18
+ "minLength": typesystem.Integer(minimum=0),
19
+ "maxLength": typesystem.Integer(minimum=0),
20
+ "pattern": typesystem.String(format="regex"),
21
+ "format": typesystem.String(),
22
+ # Numeric
23
+ "minimum": typesystem.Number(),
24
+ "maximum": typesystem.Number(),
25
+ "exclusiveMinimum": typesystem.Number(),
26
+ "exclusiveMaximum": typesystem.Number(),
27
+ "multipleOf": typesystem.Number(exclusive_minimum=0),
28
+ # Object
29
+ "properties": typesystem.Object(
30
+ additional_properties=typesystem.Reference(
31
+ "JSONSchema", definitions=definitions
32
+ )
33
+ ),
34
+ "minProperties": typesystem.Integer(minimum=0),
35
+ "maxProperties": typesystem.Integer(minimum=0),
36
+ "patternProperties": typesystem.Object(
37
+ additional_properties=typesystem.Reference(
38
+ "JSONSchema", definitions=definitions
39
+ )
40
+ ),
41
+ "additionalProperties": (
42
+ typesystem.Reference("JSONSchema", definitions=definitions)
43
+ | typesystem.Boolean()
44
+ ),
45
+ "required": typesystem.Array(items=typesystem.String(), unique_items=True),
46
+ # Array
47
+ "items": (
48
+ typesystem.Reference("JSONSchema", definitions=definitions)
49
+ | typesystem.Array(
50
+ items=typesystem.Reference("JSONSchema", definitions=definitions),
51
+ min_items=1,
52
+ )
53
+ ),
54
+ "additionalItems": (
55
+ typesystem.Reference("JSONSchema", definitions=definitions)
56
+ | typesystem.Boolean()
57
+ ),
58
+ "minItems": typesystem.Integer(minimum=0),
59
+ "maxItems": typesystem.Integer(minimum=0),
60
+ "uniqueItems": typesystem.Boolean(),
61
+ }
62
+ )
63
+ | typesystem.Boolean()
64
+ )
65
+
66
+ definitions["JSONSchema"] = JSON_SCHEMA