awskit 0.0.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.
- awskit-0.0.0/PKG-INFO +14 -0
- awskit-0.0.0/README.md +7 -0
- awskit-0.0.0/awesome/interfaces/__init__.py +5 -0
- awskit-0.0.0/awesome/interfaces/dynamodb.py +5 -0
- awskit-0.0.0/awesome/interfaces/s3.py +14 -0
- awskit-0.0.0/awesome/interfaces/sg.py +31 -0
- awskit-0.0.0/awesome/interfaces/sqs.py +59 -0
- awskit-0.0.0/awesome/interfaces/subnet.py +31 -0
- awskit-0.0.0/awesome/interfaces/vpc.py +31 -0
- awskit-0.0.0/awesome/lib/auth.py +123 -0
- awskit-0.0.0/awesome/lib/interface.py +114 -0
- awskit-0.0.0/awskit.egg-info/PKG-INFO +14 -0
- awskit-0.0.0/awskit.egg-info/SOURCES.txt +16 -0
- awskit-0.0.0/awskit.egg-info/dependency_links.txt +1 -0
- awskit-0.0.0/awskit.egg-info/requires.txt +11 -0
- awskit-0.0.0/awskit.egg-info/top_level.txt +1 -0
- awskit-0.0.0/setup.cfg +4 -0
- awskit-0.0.0/setup.py +30 -0
awskit-0.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: awskit
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Author: André Bienemann
|
|
5
|
+
Author-email: andre.bienemann@gmail.com
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Provides-Extra: docs
|
awskit-0.0.0/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from lib.interface import AwsInterface
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class S3(AwsInterface):
|
|
5
|
+
|
|
6
|
+
def __init__(self):
|
|
7
|
+
super().__init__("s3", "2006-03-01")
|
|
8
|
+
|
|
9
|
+
def CreateBucket(self, region: str, **kwargs) -> dict:
|
|
10
|
+
"""
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
resp = self.post(region, "CreateBucket", **kwargs)
|
|
14
|
+
return resp["CreateSecurityGroupResponse"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from lib.interface import AwsInterface
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SecurityGroup(AwsInterface):
|
|
5
|
+
|
|
6
|
+
def __init__(self):
|
|
7
|
+
super().__init__("ec2", "2016-11-15")
|
|
8
|
+
|
|
9
|
+
def CreateSecurityGroup(self, region: str, **kwargs) -> dict:
|
|
10
|
+
"""
|
|
11
|
+
Given a region and request arguments, creates a security group
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
resp = self.post(region, "CreateSecurityGroup", **kwargs)
|
|
15
|
+
return resp["CreateSecurityGroupResponse"]
|
|
16
|
+
|
|
17
|
+
def DescribeSecurityGroups(self, region: str) -> list:
|
|
18
|
+
"""
|
|
19
|
+
Given a region, returns a list of all security groups available in that region
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
resp = self.get(region, "DescribeSecurityGroups")
|
|
23
|
+
return resp["DescribeSecurityGroupsResponse"]
|
|
24
|
+
|
|
25
|
+
def DeleteSecurityGroup(self, region: str, **kwargs) -> dict:
|
|
26
|
+
"""
|
|
27
|
+
Given a region and request arguments, deletes a security group
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
resp = self.post(region, "DeleteSecurityGroup", **kwargs)
|
|
31
|
+
return resp["DeleteSecurityGroupResponse"]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from lib.interface import AwsInterface
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Sqs(AwsInterface):
|
|
5
|
+
|
|
6
|
+
def __init__(self):
|
|
7
|
+
super().__init__("sqs", "2012-11-05")
|
|
8
|
+
|
|
9
|
+
def CreateQueue(self, region: str, **kwargs) -> dict:
|
|
10
|
+
"""
|
|
11
|
+
Given a region and request arguments, creates a queue
|
|
12
|
+
|
|
13
|
+
QueueName
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
resp = self.post(region, "CreateQueue", **kwargs)
|
|
17
|
+
return resp["CreateQueueResponse"]
|
|
18
|
+
|
|
19
|
+
def ListQueues(self, region: str) -> list:
|
|
20
|
+
"""
|
|
21
|
+
Given a region, returns a list of all queues available in that region
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
resp = self.get(region, "ListQueues")
|
|
25
|
+
return resp["ListQueuesResponse"]
|
|
26
|
+
|
|
27
|
+
def DeleteQueue(self, region: str, **kwargs) -> dict:
|
|
28
|
+
"""
|
|
29
|
+
Given a region and request arguments, deletes a queue
|
|
30
|
+
|
|
31
|
+
QueueUrl
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
resp = self.post(region, "DeleteQueue", **kwargs)
|
|
35
|
+
return resp["DeleteQueueResponse"]
|
|
36
|
+
|
|
37
|
+
def SendMessage(self, region: str, **kwargs) -> dict:
|
|
38
|
+
"""
|
|
39
|
+
Given a region and request arguments, sends a message
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
resp = self.post(region, "SendMessage", **kwargs)
|
|
43
|
+
return resp["SendMessageResponse"]
|
|
44
|
+
|
|
45
|
+
def ReceiveMessage(self, region: str, **kwargs) -> dict:
|
|
46
|
+
"""
|
|
47
|
+
Given a region and request arguments, receives a message
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
resp = self.post(region, "ReceiveMessage", **kwargs)
|
|
51
|
+
return resp["ReceiveMessageResponse"]
|
|
52
|
+
|
|
53
|
+
def DeleteMessage(self, region: str, **kwargs) -> dict:
|
|
54
|
+
"""
|
|
55
|
+
Given a region and request arguments, deletes a message
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
resp = self.post(region, "DeleteMessage", **kwargs)
|
|
59
|
+
return resp["DeleteMessageResponse"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from lib.interface import AwsInterface
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Subnet(AwsInterface):
|
|
5
|
+
|
|
6
|
+
def __init__(self):
|
|
7
|
+
super().__init__("ec2", "2016-11-15")
|
|
8
|
+
|
|
9
|
+
def CreateSubnet(self, region: str, **kwargs) -> dict:
|
|
10
|
+
"""
|
|
11
|
+
Given a region and request arguments, creates a subnet
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
resp = self.post(region, "CreateSubnet", **kwargs)
|
|
15
|
+
return resp["CreateSubnetResponse"]
|
|
16
|
+
|
|
17
|
+
def DescribeSubnets(self, region: str) -> list:
|
|
18
|
+
"""
|
|
19
|
+
Given a region, returns a list of all subnets available in that region
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
resp = self.get(region, "DescribeSubnets")
|
|
23
|
+
return resp["DescribeSubnetsResponse"]
|
|
24
|
+
|
|
25
|
+
def DeleteSubnet(self, region: str,**kwargs) -> dict:
|
|
26
|
+
"""
|
|
27
|
+
Given a region and request arguments, deletes a subnet
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
resp = self.post(region, "DeleteSubnet", **kwargs)
|
|
31
|
+
return resp["DeleteSubnetResponse"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from lib.interface import AwsInterface
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Vpc(AwsInterface):
|
|
5
|
+
|
|
6
|
+
def __init__(self):
|
|
7
|
+
super().__init__("ec2", "2016-11-15")
|
|
8
|
+
|
|
9
|
+
def CreateVpc(self, region: str, **kwargs) -> dict:
|
|
10
|
+
"""
|
|
11
|
+
Given a region and request arguments, creates a VPC
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
resp = self.post(region, "CreateVpc", **kwargs)
|
|
15
|
+
return resp["CreateVpcResponse"]
|
|
16
|
+
|
|
17
|
+
def DescribeVpcs(self, region: str) -> list:
|
|
18
|
+
"""
|
|
19
|
+
Given a region, returns a list of all VPCs available in that region
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
resp = self.get(region, "DescribeVpcs")
|
|
23
|
+
return resp["DescribeVpcsResponse"]
|
|
24
|
+
|
|
25
|
+
def DeleteVpc(self, region: str,**kwargs) -> dict:
|
|
26
|
+
"""
|
|
27
|
+
Given a region and request arguments, deletes a VPC
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
resp = self.post(region, "DeleteVpc", **kwargs)
|
|
31
|
+
return resp["DeleteVpcResponse"]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import datetime
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
|
|
6
|
+
from requests.auth import AuthBase
|
|
7
|
+
from requests.compat import urlparse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AwsAuth(AuthBase):
|
|
11
|
+
"""
|
|
12
|
+
AWS Authentification using AWS Signature Version 4
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, service: str, region: str):
|
|
16
|
+
"""
|
|
17
|
+
Parameters
|
|
18
|
+
service: AWS service name
|
|
19
|
+
region: AWS region name
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
self.service = service
|
|
23
|
+
self.region = region
|
|
24
|
+
|
|
25
|
+
def __call__(self, request):
|
|
26
|
+
"""
|
|
27
|
+
Parameters
|
|
28
|
+
request: request to be signed
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
now = datetime.datetime.utcnow()
|
|
32
|
+
|
|
33
|
+
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
|
34
|
+
datestamp = now.strftime("%Y%m%d")
|
|
35
|
+
|
|
36
|
+
url = urlparse(request.url)
|
|
37
|
+
|
|
38
|
+
if "Host" not in request.headers:
|
|
39
|
+
request.headers["Host"] = url.hostname
|
|
40
|
+
|
|
41
|
+
if "Content-Type" not in request.headers:
|
|
42
|
+
request.headers["Content-Type"] = (
|
|
43
|
+
"application/x-www-form-urlencoded; charset=utf-8"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if request.method == "GET":
|
|
47
|
+
payload_hash = hashlib.sha256("".encode("utf-8")).hexdigest()
|
|
48
|
+
else:
|
|
49
|
+
if request.body:
|
|
50
|
+
if isinstance(request.body, bytes):
|
|
51
|
+
payload_hash = hashlib.sha256(request.body).hexdigest()
|
|
52
|
+
else:
|
|
53
|
+
payload_hash = hashlib.sha256(
|
|
54
|
+
request.body.encode("utf-8")
|
|
55
|
+
).hexdigest()
|
|
56
|
+
else:
|
|
57
|
+
payload_hash = hashlib.sha256(b"").hexdigest()
|
|
58
|
+
|
|
59
|
+
request.headers["x-amz-content-sha256"] = payload_hash
|
|
60
|
+
request.headers["X-AMZ-Date"] = amz_date
|
|
61
|
+
|
|
62
|
+
headers_to_sign = sorted(
|
|
63
|
+
(
|
|
64
|
+
header
|
|
65
|
+
for header in (header.lower() for header in request.headers.keys())
|
|
66
|
+
if header.startswith("x-amz-") or header == "host"
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
canonical_headers = "".join(
|
|
71
|
+
(f"{header}:{request.headers[header]}\n" for header in headers_to_sign)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
canonical_query_string = "&".join(
|
|
75
|
+
("=".join(t) for t in sorted((s.split("=") for s in url.query.split("&"))))
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
signed_headers = ";".join(headers_to_sign)
|
|
79
|
+
|
|
80
|
+
canonical_request = (
|
|
81
|
+
f"{request.method}\n"
|
|
82
|
+
f"{url.path}\n"
|
|
83
|
+
f"{canonical_query_string}\n"
|
|
84
|
+
f"{canonical_headers}\n"
|
|
85
|
+
f"{signed_headers}\n"
|
|
86
|
+
f"{payload_hash}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
credential_scope = f"{datestamp}/{self.region}/{self.service}/aws4_request"
|
|
90
|
+
|
|
91
|
+
string_to_sign = (
|
|
92
|
+
"AWS4-HMAC-SHA256\n"
|
|
93
|
+
f"{amz_date}\n"
|
|
94
|
+
f"{credential_scope}\n"
|
|
95
|
+
f"{hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()}"
|
|
96
|
+
).encode("utf-8")
|
|
97
|
+
|
|
98
|
+
k_key = f"AWS4{os.getenv('AWS_SECRET_ACCESS_KEY')}".encode("utf-8")
|
|
99
|
+
k_date = self.sign_msg(k_key, datestamp)
|
|
100
|
+
k_region = self.sign_msg(k_date, self.region)
|
|
101
|
+
k_service = self.sign_msg(k_region, self.service)
|
|
102
|
+
k_signing = self.sign_msg(k_service, "aws4_request")
|
|
103
|
+
signature = hmac.new(k_signing, string_to_sign, hashlib.sha256).hexdigest()
|
|
104
|
+
|
|
105
|
+
request.headers["Authorization"] = (
|
|
106
|
+
"AWS4-HMAC-SHA256 Credential="
|
|
107
|
+
f"{os.getenv('AWS_ACCESS_KEY_ID')}/{credential_scope}, "
|
|
108
|
+
f"SignedHeaders={signed_headers}, "
|
|
109
|
+
f"Signature={signature}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return request
|
|
113
|
+
|
|
114
|
+
def sign_msg(self, key: str, msg: str) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Given a key and a message returns a hash-based message authentication code
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
key: key to be used for encryption
|
|
120
|
+
msg: message to be signed by the key
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import xmltodict
|
|
3
|
+
|
|
4
|
+
from lib.auth import AwsAuth
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AwsInterface:
|
|
8
|
+
|
|
9
|
+
def __init__(self, service, version):
|
|
10
|
+
self.service = service
|
|
11
|
+
self.version = version
|
|
12
|
+
|
|
13
|
+
def post(self, region: str, action: str, **kwargs) -> dict:
|
|
14
|
+
"""
|
|
15
|
+
Creates a POST request to AWS API
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
region: geographic location
|
|
19
|
+
action: request action
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
return self._request("POST", region, action, **kwargs)
|
|
23
|
+
|
|
24
|
+
def get(self, region: str, action: str, **kwargs) -> dict:
|
|
25
|
+
"""
|
|
26
|
+
Creates a GET request to AWS API
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
region: geographic location
|
|
30
|
+
action: request action
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
return self._request("GET", region, action, **kwargs)
|
|
34
|
+
|
|
35
|
+
def _request(self, method: str, region: str, action: str, **kwargs) -> dict:
|
|
36
|
+
"""
|
|
37
|
+
Creates a request to AWS API
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
method: request method
|
|
41
|
+
region: geographic location
|
|
42
|
+
action: request action
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
auth = AwsAuth(self.service, region)
|
|
46
|
+
url = f"https://{self.service}.{region}.amazonaws.com"
|
|
47
|
+
|
|
48
|
+
if method == "GET":
|
|
49
|
+
params = self._params(action, **kwargs)
|
|
50
|
+
response = requests.request(method=method, auth=auth, url=url, params=params)
|
|
51
|
+
return self._parse(response)
|
|
52
|
+
|
|
53
|
+
elif method == "POST":
|
|
54
|
+
data = self._data(action, **kwargs)
|
|
55
|
+
response = requests.request(method=method, auth=auth, url=url, data=data)
|
|
56
|
+
return self._parse(response)
|
|
57
|
+
|
|
58
|
+
def _parse(self, response: requests.Response) -> dict:
|
|
59
|
+
"""
|
|
60
|
+
Returns a parsed dictionary
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
data = xmltodict.parse(response.text)
|
|
64
|
+
|
|
65
|
+
if response.status_code != 200:
|
|
66
|
+
|
|
67
|
+
print(data)
|
|
68
|
+
|
|
69
|
+
if "Response" in data:
|
|
70
|
+
raise Exception(
|
|
71
|
+
data["Response"]
|
|
72
|
+
.get("Errors", {})
|
|
73
|
+
.get("Error", {})
|
|
74
|
+
.get("Message", "")
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
elif "ErrorResponse" in data:
|
|
78
|
+
raise Exception(
|
|
79
|
+
data["ErrorResponse"]
|
|
80
|
+
.get("Error", {})
|
|
81
|
+
.get("Message", "")
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
elif "Error" in data:
|
|
85
|
+
raise Exception(
|
|
86
|
+
data["Error"].
|
|
87
|
+
get("Message", "")
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
raise Exception("Failed to parse error message")
|
|
91
|
+
|
|
92
|
+
return data
|
|
93
|
+
|
|
94
|
+
def _data(self, action: str, **kwargs) -> dict:
|
|
95
|
+
"""
|
|
96
|
+
Returns data dictionary
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
data = {"Version": self.version, "Action": action}
|
|
100
|
+
|
|
101
|
+
data.update(kwargs)
|
|
102
|
+
|
|
103
|
+
return data
|
|
104
|
+
|
|
105
|
+
def _params(self, action: str, **kwargs) -> str:
|
|
106
|
+
"""
|
|
107
|
+
Returns URL query parameters
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
params = [f"Version={self.version}", f"Action={action}"]
|
|
111
|
+
|
|
112
|
+
params.extend([f"{key}={value}" for key, value in kwargs.items()])
|
|
113
|
+
|
|
114
|
+
return "&".join(params)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: awskit
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Author: André Bienemann
|
|
5
|
+
Author-email: andre.bienemann@gmail.com
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Provides-Extra: docs
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
awesome/interfaces/__init__.py
|
|
4
|
+
awesome/interfaces/dynamodb.py
|
|
5
|
+
awesome/interfaces/s3.py
|
|
6
|
+
awesome/interfaces/sg.py
|
|
7
|
+
awesome/interfaces/sqs.py
|
|
8
|
+
awesome/interfaces/subnet.py
|
|
9
|
+
awesome/interfaces/vpc.py
|
|
10
|
+
awesome/lib/auth.py
|
|
11
|
+
awesome/lib/interface.py
|
|
12
|
+
awskit.egg-info/PKG-INFO
|
|
13
|
+
awskit.egg-info/SOURCES.txt
|
|
14
|
+
awskit.egg-info/dependency_links.txt
|
|
15
|
+
awskit.egg-info/requires.txt
|
|
16
|
+
awskit.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
awesome
|
awskit-0.0.0/setup.cfg
ADDED
awskit-0.0.0/setup.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from setuptools import setup
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="awskit",
|
|
5
|
+
version="0.0.0",
|
|
6
|
+
author="André Bienemann",
|
|
7
|
+
author_email="andre.bienemann@gmail.com",
|
|
8
|
+
extras_require={
|
|
9
|
+
"dev": [
|
|
10
|
+
"black",
|
|
11
|
+
"coverage",
|
|
12
|
+
"isort",
|
|
13
|
+
"twine",
|
|
14
|
+
"wheel",
|
|
15
|
+
],
|
|
16
|
+
"docs": [
|
|
17
|
+
"mkdocs",
|
|
18
|
+
"mkdocs-material",
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
classifiers=[
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3.8",
|
|
25
|
+
"Programming Language :: Python :: 3.9",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
],
|
|
30
|
+
)
|