Skip to content

Commit 656d1d0

Browse files
authored
Versioned api (#51)
* add swagger spec generator * fix tests * fix tests * enhance tests for lock apis * remove specs of version below 3.2.0
1 parent f3f3c1b commit 656d1d0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+6391
-192
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ language: python
55
cache: pip
66

77
python:
8+
# - 3.7
89
- 3.6
910
- 3.5
1011
- 2.7
@@ -20,7 +21,7 @@ services:
2021
env:
2122
matrix:
2223
- ETCD_VER=v3.2.20
23-
- ETCD_VER=v3.3.0
24+
# - ETCD_VER=v3.3.0
2425
# - ETCD_VER=v3.3.4
2526
# - ETCD_VER=v3.3.7
2627
# - ETCD_VER=v3.3.9

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,8 @@ dist: clean ## builds source and wheel package
8484
python setup.py bdist_wheel
8585
ls -l dist
8686

87+
upload: dist
88+
twine upload dist/*
89+
8790
install: clean ## install the package to the active Python's site-packages
8891
python setup.py install

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Notice: The authentication header through gRPC-JSON-Gateway only supported in [e
2929
* [x] Watch
3030
* [x] Cluster
3131
* [x] Lease
32+
* [x] Lock
3233
* [x] Maintenance
3334
* [x] Extra APIs
3435
* [x] stateful utilities
@@ -159,6 +160,9 @@ docker run -d \
159160

160161
## TODO
161162

163+
- [ ] human friendly middle level apis
164+
- [ ] able to expose json or raw response to user
165+
- [ ] add election api
162166
- [ ] benchmark
163167
- [ ] python-etcd(etcd v2) compatible client
164168
- [ ] etcd browser

docs/etcd3.apis.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ etcd3\.apis\.lease
3838
:undoc-members:
3939
:show-inheritance:
4040

41+
etcd3\.apis\.lock
42+
-------------------------
43+
44+
.. automodule:: etcd3.apis.lock
45+
:members:
46+
:undoc-members:
47+
:show-inheritance:
48+
4149
etcd3\.apis\.maintenance
4250
-------------------------------
4351

etcd3/aio_client.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,36 @@
22
asynchronous client
33
"""
44

5-
import aiohttp
65
import json
7-
import six
86
import ssl
97
import warnings
8+
9+
import aiohttp
10+
import six
1011
from aiohttp.client import _RequestContextManager
1112

1213
from .baseclient import BaseClient
1314
from .baseclient import BaseModelizedStreamResponse
15+
from .baseclient import DEFAULT_VERSION
1416
from .errors import Etcd3Exception
1517
from .errors import Etcd3StreamError
1618
from .errors import get_client_error
1719
from .utils import iter_json_string, Etcd3Warning
1820

1921

2022
class ModelizedResponse(object):
21-
def __init__(self, method, resp, decode=True):
23+
def __init__(self, client, method, resp, decode=True):
24+
self.client = client
2225
self._coro = resp
2326
self._method = method
2427
self._resp = None
2528
self._decode = decode
2629

2730
async def __modelize(self):
2831
self._resp = await self._coro
29-
await AioClient._raise_for_status(self._resp)
32+
await self.client._raise_for_status(self._resp)
3033
data = await self._resp.json()
31-
return AioClient._modelizeResponseData(self._method, data, self._decode)
34+
return self.client._modelizeResponseData(self._method, data, self._decode)
3235

3336
def __await__(self):
3437
return self.__modelize().__await__()
@@ -39,10 +42,11 @@ class ModelizedStreamResponse(BaseModelizedStreamResponse):
3942
Model of a stream response
4043
"""
4144

42-
def __init__(self, method, resp, decode=True):
45+
def __init__(self, client, method, resp, decode=True):
4346
"""
4447
:param resp: aiohttp.ClientResponse
4548
"""
49+
self.client = client
4650
self.resp = resp
4751
self.decode = decode
4852
self.method = method
@@ -75,17 +79,20 @@ async def __aiter__(self):
7579
async def __anext__(self):
7680
if isinstance(self.resp, _RequestContextManager):
7781
self.resp = await self.resp
78-
await AioClient._raise_for_status(self.resp)
82+
await self.client._raise_for_status(self.resp)
7983
data = await self.resp_iter.next()
8084
data = json.loads(str(data, encoding='utf-8'))
8185
if data.get('error'): # pragma: no cover
8286
# {"error":{"grpc_code":14,"http_code":503,"message":"rpc error: code = Unavailable desc = transport is closing","http_status":"Service Unavailable"}}
8387
err = data.get('error')
8488
raise get_client_error(err.get('message'), code=err.get('code'), status=err.get('http_code'))
85-
return AioClient._modelizeResponseData(self.method, data, decode=self.decode)
89+
r = self.client._modelizeResponseData(self.method, data, decode=self.decode)
90+
if r.result:
91+
r = r.result
92+
return r
8693

8794

88-
class ResponseIter():
95+
class ResponseIter(object):
8996
"""
9097
yield response content by every json object
9198
we don't yield by line, because the content of etcd's gRPC-JSON-Gateway stream response
@@ -126,14 +133,16 @@ async def next(self):
126133

127134

128135
class AioClient(BaseClient):
129-
def __init__(self, host='localhost', port=2379, protocol='http',
136+
def __init__(self, host='127.0.0.1', port=2379, protocol='http',
130137
cert=(), verify=None,
131138
timeout=None, headers=None, user_agent=None, pool_size=30,
132-
username=None, password=None, token=None):
139+
username=None, password=None, token=None,
140+
server_version=DEFAULT_VERSION, cluster_version=DEFAULT_VERSION):
133141
super(AioClient, self).__init__(host=host, port=port, protocol=protocol,
134142
cert=cert, verify=verify,
135143
timeout=timeout, headers=headers, user_agent=user_agent, pool_size=pool_size,
136-
username=username, password=password, token=token)
144+
username=username, password=password, token=token,
145+
server_version=server_version, cluster_version=cluster_version)
137146
self.ssl_context = None
138147
if self.cert:
139148
if verify is False:
@@ -151,8 +160,7 @@ def __init__(self, host='localhost', port=2379, protocol='http',
151160
# the ssl problem is a pain in the ass, seems i can never get it right
152161
# https://github.com/requests/requests/issues/1847
153162
# https://stackoverflow.com/questions/44316292/ssl-sslerror-tlsv1-alert-protocol-version
154-
self.ssl_context = ssl_context = ssl.SSLContext()
155-
ssl_context.protocol = ssl.PROTOCOL_TLS
163+
self.ssl_context = ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS)
156164
if not hasattr(ssl, 'PROTOCOL_TLSv1_1'): # should support TLSv1.2 to pass the test
157165
warnings.warn(Etcd3Warning("the openssl version of your python is too old to support TLSv1.1+,"
158166
"please upgrade you python"))
@@ -174,13 +182,11 @@ async def __aenter__(self):
174182
async def __aexit__(self, exc_type, exc_val, exc_tb):
175183
await self.close()
176184

177-
@classmethod
178-
def _modelizeResponse(cls, method, resp, decode=True):
179-
return ModelizedResponse(method, resp, decode)
185+
def _modelizeResponse(self, method, resp, decode=True):
186+
return ModelizedResponse(self, method, resp, decode)
180187

181-
@classmethod
182-
def _modelizeStreamResponse(cls, method, resp, decode=True):
183-
return ModelizedStreamResponse(method, resp, decode)
188+
def _modelizeStreamResponse(self, method, resp, decode=True):
189+
return ModelizedStreamResponse(self, method, resp, decode)
184190

185191
def _get(self, url, **kwargs):
186192
r"""
@@ -212,7 +218,7 @@ async def _raise_for_status(resp):
212218
try:
213219
data = await resp.json()
214220
except Exception:
215-
error = resp.content
221+
error = resp._content or resp.reason
216222
code = 2
217223
else:
218224
error = data.get('error')

etcd3/apis/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@
1717
'KVAPI',
1818
'MaintenanceAPI',
1919
'LeaseAPI',
20-
'BaseAPI'
20+
'BaseAPI',
2121
'LockAPI'
2222
]

etcd3/apis/auth.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def authenticate(self, name, password):
1313
:type password: str
1414
:param password: password of the user
1515
"""
16-
method = '/v3alpha/auth/authenticate'
16+
method = '/auth/authenticate'
1717
data = {
1818
"name": name,
1919
"password": password
@@ -25,7 +25,7 @@ def auth_disable(self):
2525
AuthDisable disables authentication.
2626
2727
"""
28-
method = '/v3alpha/auth/disable'
28+
method = '/auth/disable'
2929
data = {}
3030
r = self.call_rpc(method, data=data)
3131
self.token = None # clear local token
@@ -36,7 +36,7 @@ def auth_enable(self):
3636
AuthEnable enables authentication.
3737
3838
"""
39-
method = '/v3alpha/auth/enable'
39+
method = '/auth/enable'
4040
data = {}
4141
return self.call_rpc(method, data=data)
4242

@@ -47,7 +47,7 @@ def role_add(self, name):
4747
:type name: str
4848
:param name: name is the name of the role to add to the authentication system.
4949
"""
50-
method = '/v3alpha/auth/role/add'
50+
method = '/auth/role/add'
5151
data = {
5252
"name": name
5353
}
@@ -60,7 +60,7 @@ def role_delete(self, role):
6060
:type role: str
6161
:param role: None
6262
"""
63-
method = '/v3alpha/auth/role/delete'
63+
method = '/auth/role/delete'
6464
data = {
6565
"role": role
6666
}
@@ -73,7 +73,7 @@ def role_get(self, role):
7373
:type role: str
7474
:param role: None
7575
"""
76-
method = '/v3alpha/auth/role/get'
76+
method = '/auth/role/get'
7777
data = {
7878
"role": role
7979
}
@@ -102,7 +102,7 @@ def role_grant_permission(self, name, key=None, permType=authpbPermissionType.RE
102102
:type all: bool
103103
:param all: all the keys [default: False]
104104
"""
105-
method = '/v3alpha/auth/role/grant'
105+
method = '/auth/role/grant'
106106
if all:
107107
key = range_end = '\0'
108108
if prefix:
@@ -122,7 +122,7 @@ def role_list(self):
122122
"""
123123
RoleList gets lists of all roles.
124124
"""
125-
method = '/v3alpha/auth/role/list'
125+
method = '/auth/role/list'
126126
data = {}
127127
return self.call_rpc(method, data=data)
128128

@@ -146,7 +146,7 @@ def role_revoke_permission(self, role, key=None, range_end=None, prefix=False, a
146146
:type all: bool
147147
:param all: all the keys [default: False]
148148
"""
149-
method = '/v3alpha/auth/role/revoke'
149+
method = '/auth/role/revoke'
150150
if all:
151151
key = range_end = '\0'
152152
if prefix:
@@ -168,7 +168,7 @@ def user_add(self, name, password):
168168
:type password: str
169169
:param password: password of the user
170170
"""
171-
method = '/v3alpha/auth/user/add'
171+
method = '/auth/user/add'
172172
data = {
173173
"name": name,
174174
"password": password
@@ -184,7 +184,7 @@ def user_change_password(self, name, password):
184184
:type password: str
185185
:param password: password is the new password for the user.
186186
"""
187-
method = '/v3alpha/auth/user/changepw'
187+
method = '/auth/user/changepw'
188188
data = {
189189
"name": name,
190190
"password": password
@@ -198,7 +198,7 @@ def user_delete(self, name):
198198
:type name: str
199199
:param name: name is the name of the user to delete.
200200
"""
201-
method = '/v3alpha/auth/user/delete'
201+
method = '/auth/user/delete'
202202
data = {
203203
"name": name
204204
}
@@ -211,7 +211,7 @@ def user_get(self, name):
211211
:type name: str
212212
:param name: name is the name of the user to get.
213213
"""
214-
method = '/v3alpha/auth/user/get'
214+
method = '/auth/user/get'
215215
data = {
216216
"name": name
217217
}
@@ -226,7 +226,7 @@ def user_grant_role(self, user, role):
226226
:type role: str
227227
:param role: role is the name of the role to grant to the user.
228228
"""
229-
method = '/v3alpha/auth/user/grant'
229+
method = '/auth/user/grant'
230230
data = {
231231
"user": user,
232232
"role": role
@@ -237,7 +237,7 @@ def user_list(self):
237237
"""
238238
UserList gets a list of all users.
239239
"""
240-
method = '/v3alpha/auth/user/list'
240+
method = '/auth/user/list'
241241
data = {}
242242
return self.call_rpc(method, data=data)
243243

@@ -250,7 +250,7 @@ def user_revoke_role(self, name, role):
250250
:type role: str
251251
:param role: role name
252252
"""
253-
method = '/v3alpha/auth/user/revoke'
253+
method = '/auth/user/revoke'
254254
data = {
255255
"name": name,
256256
"role": role

etcd3/apis/base.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
class BaseAPI(object): # pragma: no cover
2+
def __init__(self):
3+
self.headers = {}
4+
25
@staticmethod
36
def _raise_for_status(resp):
47
raise NotImplementedError
58

6-
def _url(self, method):
9+
def _url(self, method, prefix=True):
710
raise NotImplementedError
811

912
def _get(self, url, **kwargs):

etcd3/apis/cluster.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def member_add(self, peerURLs):
99
:type peerURLs: list of str
1010
:param peerURLs: peerURLs is the list of URLs the added member will use to communicate with the cluster.
1111
"""
12-
method = '/v3alpha/cluster/member/add'
12+
method = '/cluster/member/add'
1313
data = {
1414
"peerURLs": peerURLs
1515
}
@@ -20,7 +20,7 @@ def member_list(self):
2020
MemberList lists all the members in the cluster.
2121
2222
"""
23-
method = '/v3alpha/cluster/member/list'
23+
method = '/cluster/member/list'
2424
data = {}
2525
return self.call_rpc(method, data=data)
2626

@@ -31,7 +31,7 @@ def member_remove(self, ID):
3131
:type ID: int
3232
:param ID: ID is the member ID of the member to remove.
3333
"""
34-
method = '/v3alpha/cluster/member/remove'
34+
method = '/cluster/member/remove'
3535
data = {
3636
"ID": ID
3737
}
@@ -46,7 +46,7 @@ def member_update(self, ID, peerURLs):
4646
:type peerURLs: list of str
4747
:param peerURLs: peerURLs is the new list of URLs the member will use to communicate with the cluster.
4848
"""
49-
method = '/v3alpha/cluster/member/update'
49+
method = '/cluster/member/update'
5050
data = {
5151
"ID": ID,
5252
"peerURLs": peerURLs

0 commit comments

Comments
 (0)