lswapi 0.4.2__tar.gz → 0.5.0__tar.gz

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.
Files changed (34) hide show
  1. lswapi-0.5.0/.version +1 -0
  2. lswapi-0.5.0/PKG-INFO +114 -0
  3. lswapi-0.5.0/README.md +78 -0
  4. lswapi-0.5.0/lswapi.egg-info/PKG-INFO +114 -0
  5. lswapi-0.5.0/lswapi.egg-info/SOURCES.txt +22 -0
  6. {lswapi-0.4.2 → lswapi-0.5.0}/lswapi.egg-info/entry_points.txt +0 -1
  7. lswapi-0.5.0/lswapi.egg-info/requires.txt +17 -0
  8. lswapi-0.5.0/pyproject.toml +67 -0
  9. lswapi-0.5.0/setup.cfg +4 -0
  10. lswapi-0.5.0/src/lswapi/__init__.py +9 -0
  11. lswapi-0.5.0/src/lswapi/aio/__init__.py +27 -0
  12. lswapi-0.5.0/src/lswapi/aio/client.py +46 -0
  13. lswapi-0.5.0/src/lswapi/aio/middleware.py +20 -0
  14. lswapi-0.5.0/src/lswapi/aio/oauth.py +85 -0
  15. lswapi-0.4.2/lswapi/httpie/__init__.py → lswapi-0.5.0/src/lswapi/httpie.py +15 -11
  16. lswapi-0.5.0/src/lswapi/requests/__init__.py +21 -0
  17. lswapi-0.5.0/src/lswapi/requests/oauth.py +12 -0
  18. lswapi-0.5.0/src/lswapi/requests/session.py +49 -0
  19. lswapi-0.5.0/tests/test_aio.py +23 -0
  20. lswapi-0.5.0/tests/test_lswapi.py +7 -0
  21. lswapi-0.5.0/tests/test_requests.py +16 -0
  22. lswapi-0.4.2/MANIFEST.in +0 -6
  23. lswapi-0.4.2/PKG-INFO +0 -74
  24. lswapi-0.4.2/README.md +0 -51
  25. lswapi-0.4.2/lswapi/__init__.py +0 -76
  26. lswapi-0.4.2/lswapi.egg-info/PKG-INFO +0 -74
  27. lswapi-0.4.2/lswapi.egg-info/SOURCES.txt +0 -14
  28. lswapi-0.4.2/lswapi.egg-info/requires.txt +0 -1
  29. lswapi-0.4.2/requirements.txt +0 -1
  30. lswapi-0.4.2/setup.cfg +0 -24
  31. lswapi-0.4.2/setup.py +0 -50
  32. {lswapi-0.4.2 → lswapi-0.5.0}/LICENSE.txt +0 -0
  33. {lswapi-0.4.2 → lswapi-0.5.0}/lswapi.egg-info/dependency_links.txt +0 -0
  34. {lswapi-0.4.2 → lswapi-0.5.0}/lswapi.egg-info/top_level.txt +0 -0
lswapi-0.5.0/.version ADDED
@@ -0,0 +1 @@
1
+ 0.5.0
lswapi-0.5.0/PKG-INFO ADDED
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.2
2
+ Name: lswapi
3
+ Version: 0.5.0
4
+ Summary: a python client library for easy leaseweb api access
5
+ Author-email: Nico Di Rocco <n.dirocco@global.leaseweb.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, http://developer.leaseweb.com
8
+ Project-URL: Issues, https://github.com/nrocco/lswapi
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
11
+ Classifier: Operating System :: Unix
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE.txt
23
+ Provides-Extra: develop
24
+ Requires-Dist: flake8; extra == "develop"
25
+ Requires-Dist: pytest; extra == "develop"
26
+ Requires-Dist: pytest-cov; extra == "develop"
27
+ Requires-Dist: requests-mock; extra == "develop"
28
+ Requires-Dist: twine; extra == "develop"
29
+ Provides-Extra: httpie
30
+ Requires-Dist: httpie; extra == "httpie"
31
+ Provides-Extra: requests
32
+ Requires-Dist: requests; extra == "requests"
33
+ Provides-Extra: aio
34
+ Requires-Dist: aiodns; extra == "aio"
35
+ Requires-Dist: aiohttp; extra == "aio"
36
+
37
+ # lswapi (beta)
38
+
39
+ [![Actions Status](https://github.com/nrocco/lswapi/workflows/Python%20package/badge.svg)](https://github.com/nrocco/lswapi/actions)
40
+
41
+ Python module to talk to Leaseweb's API.
42
+
43
+ For more information refer to the documentation available at
44
+ [http://developer.leaseweb.com]
45
+
46
+ This module contains two api clients:
47
+
48
+ 1. One is based on the `requests` module.
49
+ 2. The second one is based on the `aiohttp` module for asyncio support.
50
+
51
+
52
+ ## Installation
53
+
54
+ Install the module using pip. This will not install `requests` or `aiohttp`.
55
+
56
+ pip install lswapi
57
+
58
+
59
+ You can install optional depencies, such as `aiohttp`:
60
+
61
+ pip install 'lswapi[aio]'
62
+
63
+ Or if you prefer to use `requests`:
64
+
65
+ pip install 'lswapi[requests]'
66
+
67
+
68
+ ## Usage
69
+
70
+
71
+ ### Using requests
72
+
73
+ The `lswapi.requests.get_leaseweb_api` function creates an instance of the Leaseweb Api
74
+ object with the `X-Lsw-Auth` key. You can provide the api key as an argument
75
+ to `get_leaseweb_api`
76
+
77
+ $ python
78
+ >>> from lswapi.requests import get_leaseweb_api
79
+ >>> client = get_leaseweb_api(api_key="xxxx-xxx-xxxxxx")
80
+ >>> response = client.get("/bareMetals/v2/servers")
81
+ >>> servers = response.json()
82
+
83
+
84
+ or get `LSW_API_KEY` from an environment variable:
85
+
86
+ $ LSW_API_KEY=xxxx-xxxx-xxxxx python
87
+ >>> from lswapi.requests import get_leaseweb_api
88
+ >>> client = get_leaseweb_api()
89
+ >>> response = client.get("/bareMetals/v2/servers")
90
+ >>> servers = response.json()
91
+
92
+
93
+ ### Using aiohttp
94
+
95
+ The `lswapi.aio.get_leaseweb_api` function creates an instance of the Leaseweb Api
96
+ object with the `X-Lsw-Auth` key. You can provide the api key as an argument
97
+ to `get_leaseweb_api` or via the environment variable `LSW_API_KEY`:
98
+
99
+ $ python -m asyncio
100
+ >>> from lswapi.aio import get_leaseweb_api
101
+ >>> client = get_leaseweb_api(api_key="xxxx-xxx-xxxxxx")
102
+ >>> response = await client.get("/bareMetals/v2/servers")
103
+ >>> servers = await response.json()
104
+
105
+
106
+ Contribute
107
+ ----------
108
+
109
+ 1. Fork it
110
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
111
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
112
+ 4. Make sure that tests pass (`make test`)
113
+ 5. Push to the branch (`git push origin my-new-feature`)
114
+ 6. Create new Pull Request
lswapi-0.5.0/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # lswapi (beta)
2
+
3
+ [![Actions Status](https://github.com/nrocco/lswapi/workflows/Python%20package/badge.svg)](https://github.com/nrocco/lswapi/actions)
4
+
5
+ Python module to talk to Leaseweb's API.
6
+
7
+ For more information refer to the documentation available at
8
+ [http://developer.leaseweb.com]
9
+
10
+ This module contains two api clients:
11
+
12
+ 1. One is based on the `requests` module.
13
+ 2. The second one is based on the `aiohttp` module for asyncio support.
14
+
15
+
16
+ ## Installation
17
+
18
+ Install the module using pip. This will not install `requests` or `aiohttp`.
19
+
20
+ pip install lswapi
21
+
22
+
23
+ You can install optional depencies, such as `aiohttp`:
24
+
25
+ pip install 'lswapi[aio]'
26
+
27
+ Or if you prefer to use `requests`:
28
+
29
+ pip install 'lswapi[requests]'
30
+
31
+
32
+ ## Usage
33
+
34
+
35
+ ### Using requests
36
+
37
+ The `lswapi.requests.get_leaseweb_api` function creates an instance of the Leaseweb Api
38
+ object with the `X-Lsw-Auth` key. You can provide the api key as an argument
39
+ to `get_leaseweb_api`
40
+
41
+ $ python
42
+ >>> from lswapi.requests import get_leaseweb_api
43
+ >>> client = get_leaseweb_api(api_key="xxxx-xxx-xxxxxx")
44
+ >>> response = client.get("/bareMetals/v2/servers")
45
+ >>> servers = response.json()
46
+
47
+
48
+ or get `LSW_API_KEY` from an environment variable:
49
+
50
+ $ LSW_API_KEY=xxxx-xxxx-xxxxx python
51
+ >>> from lswapi.requests import get_leaseweb_api
52
+ >>> client = get_leaseweb_api()
53
+ >>> response = client.get("/bareMetals/v2/servers")
54
+ >>> servers = response.json()
55
+
56
+
57
+ ### Using aiohttp
58
+
59
+ The `lswapi.aio.get_leaseweb_api` function creates an instance of the Leaseweb Api
60
+ object with the `X-Lsw-Auth` key. You can provide the api key as an argument
61
+ to `get_leaseweb_api` or via the environment variable `LSW_API_KEY`:
62
+
63
+ $ python -m asyncio
64
+ >>> from lswapi.aio import get_leaseweb_api
65
+ >>> client = get_leaseweb_api(api_key="xxxx-xxx-xxxxxx")
66
+ >>> response = await client.get("/bareMetals/v2/servers")
67
+ >>> servers = await response.json()
68
+
69
+
70
+ Contribute
71
+ ----------
72
+
73
+ 1. Fork it
74
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
75
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
76
+ 4. Make sure that tests pass (`make test`)
77
+ 5. Push to the branch (`git push origin my-new-feature`)
78
+ 6. Create new Pull Request
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.2
2
+ Name: lswapi
3
+ Version: 0.5.0
4
+ Summary: a python client library for easy leaseweb api access
5
+ Author-email: Nico Di Rocco <n.dirocco@global.leaseweb.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, http://developer.leaseweb.com
8
+ Project-URL: Issues, https://github.com/nrocco/lswapi
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
11
+ Classifier: Operating System :: Unix
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE.txt
23
+ Provides-Extra: develop
24
+ Requires-Dist: flake8; extra == "develop"
25
+ Requires-Dist: pytest; extra == "develop"
26
+ Requires-Dist: pytest-cov; extra == "develop"
27
+ Requires-Dist: requests-mock; extra == "develop"
28
+ Requires-Dist: twine; extra == "develop"
29
+ Provides-Extra: httpie
30
+ Requires-Dist: httpie; extra == "httpie"
31
+ Provides-Extra: requests
32
+ Requires-Dist: requests; extra == "requests"
33
+ Provides-Extra: aio
34
+ Requires-Dist: aiodns; extra == "aio"
35
+ Requires-Dist: aiohttp; extra == "aio"
36
+
37
+ # lswapi (beta)
38
+
39
+ [![Actions Status](https://github.com/nrocco/lswapi/workflows/Python%20package/badge.svg)](https://github.com/nrocco/lswapi/actions)
40
+
41
+ Python module to talk to Leaseweb's API.
42
+
43
+ For more information refer to the documentation available at
44
+ [http://developer.leaseweb.com]
45
+
46
+ This module contains two api clients:
47
+
48
+ 1. One is based on the `requests` module.
49
+ 2. The second one is based on the `aiohttp` module for asyncio support.
50
+
51
+
52
+ ## Installation
53
+
54
+ Install the module using pip. This will not install `requests` or `aiohttp`.
55
+
56
+ pip install lswapi
57
+
58
+
59
+ You can install optional depencies, such as `aiohttp`:
60
+
61
+ pip install 'lswapi[aio]'
62
+
63
+ Or if you prefer to use `requests`:
64
+
65
+ pip install 'lswapi[requests]'
66
+
67
+
68
+ ## Usage
69
+
70
+
71
+ ### Using requests
72
+
73
+ The `lswapi.requests.get_leaseweb_api` function creates an instance of the Leaseweb Api
74
+ object with the `X-Lsw-Auth` key. You can provide the api key as an argument
75
+ to `get_leaseweb_api`
76
+
77
+ $ python
78
+ >>> from lswapi.requests import get_leaseweb_api
79
+ >>> client = get_leaseweb_api(api_key="xxxx-xxx-xxxxxx")
80
+ >>> response = client.get("/bareMetals/v2/servers")
81
+ >>> servers = response.json()
82
+
83
+
84
+ or get `LSW_API_KEY` from an environment variable:
85
+
86
+ $ LSW_API_KEY=xxxx-xxxx-xxxxx python
87
+ >>> from lswapi.requests import get_leaseweb_api
88
+ >>> client = get_leaseweb_api()
89
+ >>> response = client.get("/bareMetals/v2/servers")
90
+ >>> servers = response.json()
91
+
92
+
93
+ ### Using aiohttp
94
+
95
+ The `lswapi.aio.get_leaseweb_api` function creates an instance of the Leaseweb Api
96
+ object with the `X-Lsw-Auth` key. You can provide the api key as an argument
97
+ to `get_leaseweb_api` or via the environment variable `LSW_API_KEY`:
98
+
99
+ $ python -m asyncio
100
+ >>> from lswapi.aio import get_leaseweb_api
101
+ >>> client = get_leaseweb_api(api_key="xxxx-xxx-xxxxxx")
102
+ >>> response = await client.get("/bareMetals/v2/servers")
103
+ >>> servers = await response.json()
104
+
105
+
106
+ Contribute
107
+ ----------
108
+
109
+ 1. Fork it
110
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
111
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
112
+ 4. Make sure that tests pass (`make test`)
113
+ 5. Push to the branch (`git push origin my-new-feature`)
114
+ 6. Create new Pull Request
@@ -0,0 +1,22 @@
1
+ .version
2
+ LICENSE.txt
3
+ README.md
4
+ pyproject.toml
5
+ lswapi.egg-info/PKG-INFO
6
+ lswapi.egg-info/SOURCES.txt
7
+ lswapi.egg-info/dependency_links.txt
8
+ lswapi.egg-info/entry_points.txt
9
+ lswapi.egg-info/requires.txt
10
+ lswapi.egg-info/top_level.txt
11
+ src/lswapi/__init__.py
12
+ src/lswapi/httpie.py
13
+ src/lswapi/aio/__init__.py
14
+ src/lswapi/aio/client.py
15
+ src/lswapi/aio/middleware.py
16
+ src/lswapi/aio/oauth.py
17
+ src/lswapi/requests/__init__.py
18
+ src/lswapi/requests/oauth.py
19
+ src/lswapi/requests/session.py
20
+ tests/test_aio.py
21
+ tests/test_lswapi.py
22
+ tests/test_requests.py
@@ -1,3 +1,2 @@
1
1
  [httpie.plugins.auth.v1]
2
2
  httpie_lswapi = lswapi.httpie:ApiAuthPlugin
3
-
@@ -0,0 +1,17 @@
1
+
2
+ [aio]
3
+ aiodns
4
+ aiohttp
5
+
6
+ [develop]
7
+ flake8
8
+ pytest
9
+ pytest-cov
10
+ requests-mock
11
+ twine
12
+
13
+ [httpie]
14
+ httpie
15
+
16
+ [requests]
17
+ requests
@@ -0,0 +1,67 @@
1
+ [project]
2
+ name = "lswapi"
3
+ dynamic = ["version"]
4
+ description = "a python client library for easy leaseweb api access"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name="Nico Di Rocco", email="n.dirocco@global.leaseweb.com" },
8
+ ]
9
+ license = {text="Apache-2.0"}
10
+ requires-python = ">= 3.9"
11
+ classifiers = [
12
+ "Development Status :: 5 - Production/Stable",
13
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
14
+ "Operating System :: Unix",
15
+ "Programming Language :: Python",
16
+ "Programming Language :: Python :: 3.9",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ "Topic :: Utilities"
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "http://developer.leaseweb.com"
27
+ Issues = "https://github.com/nrocco/lswapi"
28
+
29
+ [project.optional-dependencies]
30
+ develop = [
31
+ "flake8",
32
+ "pytest",
33
+ "pytest-cov",
34
+ "requests-mock",
35
+ "twine",
36
+ ]
37
+ httpie = [
38
+ "httpie",
39
+ ]
40
+ requests = [
41
+ "requests",
42
+ ]
43
+ aio = [
44
+ "aiodns",
45
+ "aiohttp",
46
+ ]
47
+
48
+ [project.entry-points."httpie.plugins.auth.v1"]
49
+ httpie_lswapi = "lswapi.httpie:ApiAuthPlugin"
50
+
51
+ [build-system]
52
+ requires = [
53
+ "setuptools>=64",
54
+ ]
55
+ build-backend = "setuptools.build_meta"
56
+
57
+ [tool.setuptools]
58
+ include-package-data = true
59
+
60
+ [tool.setuptools.dynamic]
61
+ version = {file = ".version"}
62
+
63
+ [tool.pytest.ini_options]
64
+ testpaths = ["tests"]
65
+
66
+ [tool.distutils.egg_info]
67
+ egg_base = "."
lswapi-0.5.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,9 @@
1
+ try:
2
+ from importlib.metadata import version
3
+ __version__ = version(__name__)
4
+ del version
5
+ except Exception:
6
+ __version__ = "unknown"
7
+
8
+ __auth_token_url__ = "https://auth.leaseweb.com/token"
9
+ __api_base_url__ = "https://api.leaseweb.com"
@@ -0,0 +1,27 @@
1
+ from lswapi import __api_base_url__
2
+ from lswapi import __auth_token_url__
3
+ from lswapi.aio.client import LeasewebHttpClient
4
+ from lswapi.aio.middleware import LoggingMiddleware
5
+ from lswapi.aio.oauth import AccessTokenFileStore
6
+ from lswapi.aio.oauth import LeasewebOAuthMiddleware
7
+ from os import environ
8
+
9
+
10
+ def get_leaseweb_api(api_key=None, client_id=None, client_secret=None, base_url=None, token_url=None, token_store=None):
11
+ base_url = base_url or environ.get("LSW_BASE_URL", __api_base_url__)
12
+ token_url = token_url or environ.get("LSW_AUTH_URL", __auth_token_url__)
13
+ token_store = token_store or environ.get("LSW_AUTH_TOKEN_STORE")
14
+ api_key = api_key or environ.get("LSW_API_KEY")
15
+ if api_key:
16
+ return LeasewebHttpClient(base_url, middlewares=[LoggingMiddleware(debug=False)], headers={"X-Lsw-Auth": api_key})
17
+ if not client_id or not client_secret:
18
+ raise Exception("No authentication method specified")
19
+ return LeasewebHttpClient(base_url, middlewares=[
20
+ LeasewebOAuthMiddleware(
21
+ client_id,
22
+ client_secret,
23
+ token_url,
24
+ store=AccessTokenFileStore(token_store) if token_store else None,
25
+ ),
26
+ LoggingMiddleware(debug=False),
27
+ ])
@@ -0,0 +1,46 @@
1
+ from aiohttp import ClientSession
2
+
3
+
4
+ class LeasewebHttpClient:
5
+ def __init__(self, *args, middlewares: list[callable] = [], **kwargs):
6
+ self.middlewares = middlewares
7
+ self.session = ClientSession(*args, **kwargs)
8
+
9
+ async def _run_middlewares(self, method, url, next_func, **kwargs):
10
+ async def middleware_chain(index, method, url, **kwargs):
11
+ if index < len(self.middlewares):
12
+ return await self.middlewares[index](
13
+ method, url, lambda m, u, **k: middleware_chain(index + 1, m, u, **k), **kwargs
14
+ )
15
+ return await next_func(method, url, **kwargs)
16
+ return await middleware_chain(0, method, url, **kwargs)
17
+
18
+ async def _request(self, method, url, **kwargs):
19
+ return await self.session.request(method, url, **kwargs)
20
+
21
+ async def request(self, method, url, **kwargs):
22
+ return await self._run_middlewares(method, url, self._request, **kwargs)
23
+
24
+ async def get(self, url, **kwargs):
25
+ return await self.request("GET", url, **kwargs)
26
+
27
+ async def post(self, url, **kwargs):
28
+ return await self.request("POST", url, **kwargs)
29
+
30
+ async def put(self, url, **kwargs):
31
+ return await self.request("PUT", url, **kwargs)
32
+
33
+ async def patch(self, url, **kwargs):
34
+ return await self.request("PATCH", url, **kwargs)
35
+
36
+ async def delete(self, url, **kwargs):
37
+ return await self.request("DELETE", url, **kwargs)
38
+
39
+ async def close(self):
40
+ await self.session.close()
41
+
42
+ async def __aenter__(self):
43
+ return self
44
+
45
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
46
+ await self.close()
@@ -0,0 +1,20 @@
1
+ import time
2
+ import logging
3
+
4
+
5
+ class LoggingMiddleware:
6
+ def __init__(self, debug=False) -> None:
7
+ self.debug = debug
8
+
9
+ async def __call__(self, method, url, next_func, **kwargs):
10
+ start = time.monotonic()
11
+ response = await next_func(method, url, **kwargs)
12
+ end = time.monotonic()
13
+ logging.info(f"Request to {response.method} {response.url} took {(end - start):.3f} seconds and resulted in a {response.status} {response.reason}")
14
+ if self.debug:
15
+ for header, value in response.request_info.headers.items():
16
+ logging.debug(f">>> {header}: {value}")
17
+ logging.debug(f"<<< HTTP/{response.version.major}.{response.version.minor} {response.status} {response.reason}")
18
+ for header, value in response.headers.items():
19
+ logging.debug(f"<<< {header}: {value}")
20
+ return response
@@ -0,0 +1,85 @@
1
+ import aiohttp
2
+ import asyncio
3
+ import json
4
+ import os
5
+ import time
6
+
7
+ from lswapi.aio.client import LeasewebHttpClient
8
+ from lswapi.aio.middleware import LoggingMiddleware
9
+
10
+
11
+ class AccessTokenInMemoryStore:
12
+ def __init__(self):
13
+ self.token = None
14
+
15
+ async def load(self):
16
+ return self.token
17
+
18
+ async def save(self, token):
19
+ self.token = token
20
+
21
+ async def remove(self):
22
+ self.token = None
23
+
24
+
25
+ class AccessTokenFileStore:
26
+ def __init__(self, filepath):
27
+ self.token = None
28
+ self.filepath = filepath
29
+ self.lock = asyncio.Lock()
30
+
31
+ async def load(self):
32
+ async with self.lock:
33
+ if self.token:
34
+ return self.token
35
+ if not os.path.exists(self.filepath):
36
+ return None
37
+ with open(self.filepath, "r") as file:
38
+ try:
39
+ self.token = json.load(file)
40
+ except json.decoder.JSONDecodeError:
41
+ os.remove(self.filepath)
42
+ return self.token
43
+
44
+ async def save(self, token):
45
+ async with self.lock:
46
+ with open(self.filepath, "w") as file:
47
+ json.dump(token, file)
48
+ self.token = token
49
+
50
+ async def remove(self):
51
+ async with self.lock:
52
+ if os.path.exists(self.filepath):
53
+ os.remove(self.filepath)
54
+ self.token = None
55
+
56
+
57
+ class LeasewebOAuthMiddleware:
58
+ def __init__(self, client_id, client_secret, token_url, store=None):
59
+ self.client_id = client_id
60
+ self.client_secret = client_secret
61
+ self.token_url = token_url
62
+ self.store = store or AccessTokenInMemoryStore()
63
+ self.lock = asyncio.Lock()
64
+
65
+ async def __call__(self, method, url, next_func, **kwargs):
66
+ async with self.lock:
67
+ token = await self.store.load()
68
+ if not token or token.get("expires_at") < time.time():
69
+ token = await self.__fetch_access_token()
70
+ await self.store.save(token)
71
+ if "headers" not in kwargs:
72
+ kwargs["headers"] = {}
73
+ kwargs["headers"]["Authorization"] = "{token_type} {access_token}".format(**token)
74
+ return await next_func(method, url, **kwargs)
75
+
76
+ async def __fetch_access_token(self):
77
+ auth = aiohttp.BasicAuth(login=self.client_id, password=self.client_secret)
78
+ async with LeasewebHttpClient(auth=auth, middlewares=[LoggingMiddleware(debug=False)]) as client:
79
+ payload = {"grant_type": "client_credentials"}
80
+ response = await client.post(self.token_url, data=payload)
81
+ assert response.status == 200
82
+ token = await response.json()
83
+ token["issued_at"] = int(time.time())
84
+ token["expires_at"] = token["issued_at"] + token["expires_in"] - 10
85
+ return token
@@ -1,11 +1,15 @@
1
1
  """
2
2
  LswApi auth plugin for HTTPie.
3
3
  """
4
+ from httpie.plugins import AuthPlugin
4
5
  from json import loads, dumps
5
- from time import time
6
+ from lswapi import __auth_token_url__
7
+ from lswapi.requests import fetch_access_token
6
8
  from os import path
7
- from lswapi import __auth_token_url__, __token_store__, fetch_access_token
8
- from httpie.plugins import AuthPlugin
9
+ from time import time
10
+
11
+
12
+ __token_store__ = path.expanduser("~/.lswapi.token")
9
13
 
10
14
 
11
15
  class LswApiAuth(object):
@@ -15,25 +19,25 @@ class LswApiAuth(object):
15
19
 
16
20
  def __call__(self, r):
17
21
  if path.exists(__token_store__):
18
- with open(__token_store__, 'r') as file:
22
+ with open(__token_store__, "r") as file:
19
23
  token = loads(file.read())
20
- if 'expires_at' in token and token['expires_at'] > time():
21
- r.headers['Authorization'] = '{token_type} {access_token}'.format(**token)
24
+ if "expires_at" in token and token["expires_at"] > time():
25
+ r.headers["Authorization"] = "{token_type} {access_token}".format(**token)
22
26
  return r
23
27
 
24
28
  token = fetch_access_token(__auth_token_url__, self.client_id, self.client_secret)
25
29
 
26
- with open(__token_store__, 'w') as file:
30
+ with open(__token_store__, "w") as file:
27
31
  file.write(dumps(token))
28
32
 
29
- r.headers['Authorization'] = '{token_type} {access_token}'.format(**token)
33
+ r.headers["Authorization"] = "{token_type} {access_token}".format(**token)
30
34
  return r
31
35
 
32
36
 
33
37
  class ApiAuthPlugin(AuthPlugin):
34
- name = 'LswApi Oauth'
35
- auth_type = 'lswapi'
36
- description = 'LeaseWeb Api Oauth Authentication'
38
+ name = "LswApi Oauth"
39
+ auth_type = "lswapi"
40
+ description = "Leaseweb Api Oauth Authentication"
37
41
 
38
42
  def get_auth(self, username, password):
39
43
  return LswApiAuth(username, password)
@@ -0,0 +1,21 @@
1
+ from lswapi import __api_base_url__
2
+ from lswapi import __auth_token_url__
3
+ from lswapi.requests.session import LeasewebSession
4
+ from lswapi.requests.session import LeasewebApiKeyAuth
5
+ from os import environ
6
+
7
+
8
+ def get_leaseweb_api(api_key=None, client_id=None, client_secret=None, base_url=None, token_url=None, token_store=None):
9
+ base_url = base_url or environ.get("LSW_BASE_URL", __api_base_url__)
10
+ token_url = token_url or environ.get("LSW_AUTH_URL", __auth_token_url__)
11
+ token_store = token_store or environ.get("LSW_AUTH_TOKEN_STORE", None)
12
+ api_key = api_key or environ.get("LSW_API_KEY")
13
+ if api_key:
14
+ api = LeasewebSession(base_url=base_url)
15
+ api.auth = (LeasewebApiKeyAuth(api_key))
16
+ return api
17
+ client_id = client_id or environ.get("LSW_CLIENT_ID")
18
+ client_secret = client_secret or environ.get("LSW_CLIENT_SECRET")
19
+ if client_id and client_secret:
20
+ return LeasewebSession(base_url=base_url, client_id=client_id, client_secret=client_secret, token_url=token_url, token_store=token_store)
21
+ raise Exception("No authentication method specified")
@@ -0,0 +1,12 @@
1
+ from requests import post
2
+ from time import time
3
+
4
+
5
+ def fetch_access_token(token_url, client_id, client_secret):
6
+ response = post(token_url, auth=(client_id, client_secret), data={"grant_type": "client_credentials"})
7
+ if response.status_code != 200:
8
+ raise Exception("Could not obtain an access token")
9
+ token = response.json()
10
+ token["created_at"] = int(time())
11
+ token["expires_at"] = token["created_at"] + token["expires_in"] - 10
12
+ return token
@@ -0,0 +1,49 @@
1
+ from json import load, dump
2
+ from lswapi import __api_base_url__
3
+ from lswapi import __auth_token_url__
4
+ from lswapi.requests.oauth import fetch_access_token
5
+ from os import path
6
+ from requests import Session
7
+ from requests.auth import AuthBase
8
+ from time import time
9
+
10
+
11
+ class LeasewebApiKeyAuth(AuthBase):
12
+ def __init__(self, lsw_auth_key):
13
+ self.lsw_auth_key = lsw_auth_key
14
+
15
+ def __call__(self, r):
16
+ r.headers["X-Lsw-Auth"] = self.lsw_auth_key
17
+ return r
18
+
19
+
20
+ class LeasewebSession(Session):
21
+ access_token = None
22
+
23
+ def __init__(self, client_id=None, client_secret=None, base_url=None, token_url=None, token_store=None):
24
+ self.base_url = base_url or __api_base_url__
25
+ self.token_url = token_url or __auth_token_url__
26
+ self.token_store = token_store
27
+ self.client_id = client_id
28
+ self.client_secret = client_secret
29
+ super().__init__()
30
+
31
+ def _fetch_access_token(self):
32
+ if not self.access_token and self.token_store:
33
+ if path.exists(self.token_store):
34
+ with open(self.token_store, "r") as file:
35
+ self.access_token = load(file)
36
+ if self.access_token and self.access_token.get("expires_at") > time():
37
+ return self.access_token
38
+ self.access_token = fetch_access_token(self.token_url, self.client_id, self.client_secret)
39
+ if self.token_store:
40
+ with open(self.token_store, "w") as file:
41
+ dump(self.access_token, file)
42
+ return self.access_token
43
+
44
+ def request(self, method, url, data=None, headers={}, **kwargs):
45
+ if self.client_id and self.client_secret:
46
+ headers["Authorization"] = "{token_type} {access_token}".format(**self._fetch_access_token())
47
+ if not url.startswith("http"):
48
+ url = "{}/{}".format(self.base_url, url.lstrip("/"))
49
+ return super().request(method, url, headers=headers, data=data, **kwargs)
@@ -0,0 +1,23 @@
1
+ import asyncio
2
+ import pytest
3
+
4
+ from lswapi.aio import get_leaseweb_api
5
+
6
+
7
+ def test_get_with_nothing():
8
+ async def fuubar():
9
+ with pytest.raises(Exception):
10
+ get_leaseweb_api()
11
+ asyncio.run(fuubar())
12
+
13
+
14
+ def test_get_with_api_key():
15
+ async def fuubar():
16
+ assert get_leaseweb_api(api_key="xxx")
17
+ asyncio.run(fuubar())
18
+
19
+
20
+ def test_get_with_oauth():
21
+ async def fuubar():
22
+ assert get_leaseweb_api(client_id="xxx", client_secret="yyy")
23
+ asyncio.run(fuubar())
@@ -0,0 +1,7 @@
1
+ import lswapi
2
+
3
+
4
+ def test_version():
5
+ assert lswapi.__version__
6
+ assert lswapi.__auth_token_url__ == "https://auth.leaseweb.com/token"
7
+ assert lswapi.__api_base_url__ == "https://api.leaseweb.com"
@@ -0,0 +1,16 @@
1
+ import pytest
2
+
3
+ from lswapi.requests import get_leaseweb_api
4
+
5
+
6
+ def test_get_with_nothing():
7
+ with pytest.raises(Exception):
8
+ get_leaseweb_api()
9
+
10
+
11
+ def test_get_with_api_key():
12
+ assert get_leaseweb_api(api_key="xxx")
13
+
14
+
15
+ def test_get_with_oauth():
16
+ assert get_leaseweb_api(client_id="xxx", client_secret="yyy")
lswapi-0.4.2/MANIFEST.in DELETED
@@ -1,6 +0,0 @@
1
- include LICENSE.txt
2
- include README.md
3
- include requirements.txt
4
- recursive-exclude * __pycache__
5
- recursive-exclude * *.py[co]
6
- recursive-exclude tests *
lswapi-0.4.2/PKG-INFO DELETED
@@ -1,74 +0,0 @@
1
- Metadata-Version: 1.1
2
- Name: lswapi
3
- Version: 0.4.2
4
- Summary: a python client library for easy leaseweb api access
5
- Home-page: http://developer.leaseweb.com
6
- Author: Nico Di Rocco
7
- Author-email: n.dirocco@global.leaseweb.com
8
- License: GPLv3
9
- Download-URL: https://github.com/nrocco/lswapi/tags
10
- Description: lswapi (beta)
11
- =============
12
-
13
- [![Actions Status](https://github.com/nrocco/lswapi/workflows/Python%20package/badge.svg)](https://github.com/nrocco/lswapi/actions)
14
-
15
- Python module to talk to LeaseWeb's API.
16
-
17
- For more information refer to the documentation available at
18
- [http://developer.leaseweb.com]
19
-
20
-
21
- Installation
22
- ------------
23
-
24
- Install the module using pip
25
-
26
- pip install lswapi
27
-
28
-
29
- Usage
30
- -----
31
-
32
- The `lswapi.get_leaseweb_api` function creates an instance of the LeaseWeb Api
33
- object with the `X-Lsw-Auth` key. You can provide the api key as an argument
34
- to `get_leaseweb_api`
35
-
36
- $ python
37
- >>> import lswapi
38
- >>> client = lswapi.get_leaseweb_api(api_key="xxxx-xxx-xxxxxx")
39
- >>> response = client.get("/bareMetals/v2/servers")
40
- >>> servers = response.json()
41
-
42
-
43
- or as an environment variable `LSW_API_KEY`
44
-
45
- $ LSW_API_KEY=xxxx-xxxx-xxxxx python
46
- >>> import lswapi
47
- >>> client = lswapi.get_leaseweb_api()
48
- >>> response = client.get("/bareMetals/v2/servers")
49
- >>> servers = response.json()
50
-
51
-
52
- Contribute
53
- ----------
54
-
55
- 1. Fork it
56
- 2. Create your feature branch (`git checkout -b my-new-feature`)
57
- 3. Commit your changes (`git commit -am 'Add some feature'`)
58
- 4. Make sure that tests pass (`make test`)
59
- 5. Push to the branch (`git push origin my-new-feature`)
60
- 6. Create new Pull Request
61
-
62
- Platform: UNKNOWN
63
- Classifier: Development Status :: 5 - Production/Stable
64
- Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
65
- Classifier: Operating System :: Unix
66
- Classifier: Programming Language :: Python
67
- Classifier: Programming Language :: Python :: 2
68
- Classifier: Programming Language :: Python :: 2.7
69
- Classifier: Programming Language :: Python :: 3
70
- Classifier: Programming Language :: Python :: 3.5
71
- Classifier: Programming Language :: Python :: 3.6
72
- Classifier: Programming Language :: Python :: 3.7
73
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
74
- Classifier: Topic :: Utilities
lswapi-0.4.2/README.md DELETED
@@ -1,51 +0,0 @@
1
- lswapi (beta)
2
- =============
3
-
4
- [![Actions Status](https://github.com/nrocco/lswapi/workflows/Python%20package/badge.svg)](https://github.com/nrocco/lswapi/actions)
5
-
6
- Python module to talk to LeaseWeb's API.
7
-
8
- For more information refer to the documentation available at
9
- [http://developer.leaseweb.com]
10
-
11
-
12
- Installation
13
- ------------
14
-
15
- Install the module using pip
16
-
17
- pip install lswapi
18
-
19
-
20
- Usage
21
- -----
22
-
23
- The `lswapi.get_leaseweb_api` function creates an instance of the LeaseWeb Api
24
- object with the `X-Lsw-Auth` key. You can provide the api key as an argument
25
- to `get_leaseweb_api`
26
-
27
- $ python
28
- >>> import lswapi
29
- >>> client = lswapi.get_leaseweb_api(api_key="xxxx-xxx-xxxxxx")
30
- >>> response = client.get("/bareMetals/v2/servers")
31
- >>> servers = response.json()
32
-
33
-
34
- or as an environment variable `LSW_API_KEY`
35
-
36
- $ LSW_API_KEY=xxxx-xxxx-xxxxx python
37
- >>> import lswapi
38
- >>> client = lswapi.get_leaseweb_api()
39
- >>> response = client.get("/bareMetals/v2/servers")
40
- >>> servers = response.json()
41
-
42
-
43
- Contribute
44
- ----------
45
-
46
- 1. Fork it
47
- 2. Create your feature branch (`git checkout -b my-new-feature`)
48
- 3. Commit your changes (`git commit -am 'Add some feature'`)
49
- 4. Make sure that tests pass (`make test`)
50
- 5. Push to the branch (`git push origin my-new-feature`)
51
- 6. Create new Pull Request
@@ -1,76 +0,0 @@
1
- from os import environ, path
2
- from requests.auth import AuthBase
3
- from requests import Session
4
- from requests import post
5
- from time import time
6
-
7
- __version__ = '0.4.2'
8
- __auth_token_url__ = 'https://auth.leaseweb.com/token'
9
- __api_base_url__ = 'https://api.leaseweb.com'
10
- __token_store__ = path.expanduser('~/.lswapi.token')
11
-
12
-
13
- def get_leaseweb_api(api_key=None, client_id=None, client_secret=None, base_url=None, token_url=None):
14
- base_url = base_url or environ.get('LSW_BASE_URL', __api_base_url__)
15
- token_url = token_url or environ.get('LSW_AUTH_URL', __auth_token_url__)
16
-
17
- api_key = api_key or environ.get('LSW_API_KEY')
18
- if api_key:
19
- api = LeaseWebSession(base_url=base_url)
20
- api.auth = (LeasewebApiKeyAuth(api_key))
21
- return api
22
-
23
- client_id = client_id or environ.get('LSW_CLIENT_ID')
24
- client_secret = client_secret or environ.get('LSW_CLIENT_SECRET')
25
- if client_id and client_secret:
26
- return LeaseWebSession(base_url=base_url, client_id=client_id, client_secret=client_secret, token_url=token_url)
27
-
28
- raise Exception("No authentication method specified")
29
-
30
-
31
- def fetch_access_token(token_url, client_id, client_secret):
32
- response = post(token_url, auth=(client_id, client_secret), data={'grant_type': 'client_credentials'})
33
- if response.status_code != 200:
34
- raise Exception('Could not obtain an access token')
35
- token = response.json()
36
- token['created_at'] = int(time())
37
- token['expires_at'] = token['created_at'] + token['expires_in'] - 10
38
- return token
39
-
40
-
41
- class LeasewebApiKeyAuth(AuthBase):
42
- def __init__(self, lsw_auth_key):
43
- self.lsw_auth_key = lsw_auth_key
44
-
45
- def __call__(self, r):
46
- r.headers['X-Lsw-Auth'] = self.lsw_auth_key
47
- return r
48
-
49
-
50
- class LeaseWebSession(Session):
51
- access_token = None
52
-
53
- def __init__(self, client_id=None, client_secret=None, base_url=None, token_url=None):
54
- self.base_url = base_url or __api_base_url__
55
- self.token_url = token_url or __auth_token_url__
56
- self.client_id = client_id
57
- self.client_secret = client_secret
58
- super(LeaseWebSession, self).__init__()
59
-
60
- def _fetch_access_token(self):
61
- return fetch_access_token(self.token_url, self.client_id, self.client_secret)
62
-
63
- def request(self, method, url, data=None, headers={}, **kwargs):
64
- if self.client_id and self.client_secret:
65
- if not self.access_token:
66
- self.access_token = self._fetch_access_token()
67
- if int(time()) > self.access_token['expires_at']:
68
- self.access_token = self._fetch_access_token() # TODO: use refresh tokens instead
69
- headers['Authorization'] = '{token_type} {access_token}'.format(**self.access_token)
70
- if not url.startswith('http'):
71
- url = '{}/{}'.format(self.base_url, url.lstrip('/'))
72
-
73
- # if response.status_code == 401 and 'WWW-Authenticate' in response.headers:
74
- # pass # TODO analyse the WWW-Authenticate response header
75
-
76
- return super(LeaseWebSession, self).request(method, url, headers=headers, data=data, **kwargs)
@@ -1,74 +0,0 @@
1
- Metadata-Version: 1.1
2
- Name: lswapi
3
- Version: 0.4.2
4
- Summary: a python client library for easy leaseweb api access
5
- Home-page: http://developer.leaseweb.com
6
- Author: Nico Di Rocco
7
- Author-email: n.dirocco@global.leaseweb.com
8
- License: GPLv3
9
- Download-URL: https://github.com/nrocco/lswapi/tags
10
- Description: lswapi (beta)
11
- =============
12
-
13
- [![Actions Status](https://github.com/nrocco/lswapi/workflows/Python%20package/badge.svg)](https://github.com/nrocco/lswapi/actions)
14
-
15
- Python module to talk to LeaseWeb's API.
16
-
17
- For more information refer to the documentation available at
18
- [http://developer.leaseweb.com]
19
-
20
-
21
- Installation
22
- ------------
23
-
24
- Install the module using pip
25
-
26
- pip install lswapi
27
-
28
-
29
- Usage
30
- -----
31
-
32
- The `lswapi.get_leaseweb_api` function creates an instance of the LeaseWeb Api
33
- object with the `X-Lsw-Auth` key. You can provide the api key as an argument
34
- to `get_leaseweb_api`
35
-
36
- $ python
37
- >>> import lswapi
38
- >>> client = lswapi.get_leaseweb_api(api_key="xxxx-xxx-xxxxxx")
39
- >>> response = client.get("/bareMetals/v2/servers")
40
- >>> servers = response.json()
41
-
42
-
43
- or as an environment variable `LSW_API_KEY`
44
-
45
- $ LSW_API_KEY=xxxx-xxxx-xxxxx python
46
- >>> import lswapi
47
- >>> client = lswapi.get_leaseweb_api()
48
- >>> response = client.get("/bareMetals/v2/servers")
49
- >>> servers = response.json()
50
-
51
-
52
- Contribute
53
- ----------
54
-
55
- 1. Fork it
56
- 2. Create your feature branch (`git checkout -b my-new-feature`)
57
- 3. Commit your changes (`git commit -am 'Add some feature'`)
58
- 4. Make sure that tests pass (`make test`)
59
- 5. Push to the branch (`git push origin my-new-feature`)
60
- 6. Create new Pull Request
61
-
62
- Platform: UNKNOWN
63
- Classifier: Development Status :: 5 - Production/Stable
64
- Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
65
- Classifier: Operating System :: Unix
66
- Classifier: Programming Language :: Python
67
- Classifier: Programming Language :: Python :: 2
68
- Classifier: Programming Language :: Python :: 2.7
69
- Classifier: Programming Language :: Python :: 3
70
- Classifier: Programming Language :: Python :: 3.5
71
- Classifier: Programming Language :: Python :: 3.6
72
- Classifier: Programming Language :: Python :: 3.7
73
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
74
- Classifier: Topic :: Utilities
@@ -1,14 +0,0 @@
1
- LICENSE.txt
2
- MANIFEST.in
3
- README.md
4
- requirements.txt
5
- setup.cfg
6
- setup.py
7
- lswapi/__init__.py
8
- lswapi.egg-info/PKG-INFO
9
- lswapi.egg-info/SOURCES.txt
10
- lswapi.egg-info/dependency_links.txt
11
- lswapi.egg-info/entry_points.txt
12
- lswapi.egg-info/requires.txt
13
- lswapi.egg-info/top_level.txt
14
- lswapi/httpie/__init__.py
@@ -1 +0,0 @@
1
- requests
@@ -1 +0,0 @@
1
- requests
lswapi-0.4.2/setup.cfg DELETED
@@ -1,24 +0,0 @@
1
- [bumpversion]
2
- current_version = 0.4.2
3
- commit = True
4
- tag = True
5
-
6
- [wheel]
7
- universal = 1
8
-
9
- [flake8]
10
- exclude = .git,__pycache__,build,dist,env
11
- max-complexity = 10
12
- max-line-length = 1000
13
-
14
- [pycodestyle]
15
- ignore = E501
16
-
17
- [bumpversion:file:setup.py]
18
-
19
- [bumpversion:file:lswapi/__init__.py]
20
-
21
- [egg_info]
22
- tag_build =
23
- tag_date = 0
24
-
lswapi-0.4.2/setup.py DELETED
@@ -1,50 +0,0 @@
1
- #!/usr/bin/env python
2
- import io
3
- import codecs
4
-
5
- from setuptools import setup, find_packages
6
-
7
-
8
- def load_requirements(filename):
9
- with io.open(filename, encoding='utf-8') as reqfile:
10
- return [line.strip() for line in reqfile if not line.startswith('#')]
11
-
12
-
13
- setup(
14
- name='lswapi',
15
- description='a python client library for easy leaseweb api access',
16
- version='0.4.2',
17
- author='Nico Di Rocco',
18
- author_email='n.dirocco@global.leaseweb.com',
19
- url='http://developer.leaseweb.com',
20
- license='GPLv3',
21
- long_description=codecs.open('README.md', 'rb', 'utf-8').read(),
22
- download_url='https://github.com/nrocco/lswapi/tags',
23
- include_package_data=True,
24
- install_requires=load_requirements('requirements.txt'),
25
- tests_require=[
26
- 'httpie',
27
- 'coverage',
28
- ],
29
- entry_points={
30
- 'httpie.plugins.auth.v1': [
31
- 'httpie_lswapi = lswapi.httpie:ApiAuthPlugin'
32
- ]
33
- },
34
- packages=find_packages(exclude=['tests']),
35
- test_suite='tests',
36
- classifiers=[
37
- 'Development Status :: 5 - Production/Stable',
38
- 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
39
- 'Operating System :: Unix',
40
- 'Programming Language :: Python',
41
- 'Programming Language :: Python :: 2',
42
- 'Programming Language :: Python :: 2.7',
43
- 'Programming Language :: Python :: 3',
44
- 'Programming Language :: Python :: 3.5',
45
- 'Programming Language :: Python :: 3.6',
46
- 'Programming Language :: Python :: 3.7',
47
- 'Topic :: Software Development :: Libraries :: Python Modules',
48
- 'Topic :: Utilities'
49
- ],
50
- )
File without changes