flask_session_azure_c 0.4.6__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Claas Diederichs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: flask_session_azure_c
3
+ Version: 0.4.6
4
+ Summary: Flask Session using Azure Table Storage or CosmosDB table API
5
+ Home-page: https://github.com/bassmandan/FlaskAzureTableSession
6
+ Author: Claas Diederichs, Daniel Harding
7
+ Author-email:
8
+ License: MIT
9
+ Classifier: Framework :: Flask
10
+ Classifier: Programming Language :: Python :: 3.6
11
+ Classifier: Programming Language :: Python :: 3.7
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.6
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: azure-data-tables
20
+ Requires-Dist: pycryptodomex>=3.9.7
21
+ Requires-Dist: flask
22
+ Dynamic: author
23
+ Dynamic: classifier
24
+ Dynamic: description
25
+ Dynamic: description-content-type
26
+ Dynamic: home-page
27
+ Dynamic: license
28
+ Dynamic: license-file
29
+ Dynamic: requires-dist
30
+ Dynamic: requires-python
31
+ Dynamic: summary
32
+
33
+ # Flask Session using Azure table storage (Using Azure-Data-Tables Library)
34
+
35
+
36
+ [![PyPI - License](https://img.shields.io/pypi/l/flask-session-azure)](https://pypi.org/project/flask-session-azure/)
37
+ [![PyPI](https://img.shields.io/pypi/v/flask-session-azure)](https://pypi.org/project/flask-session-azure/)
38
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flask-session-azure)
39
+
40
+ This module can be used as a Flask Session handler for Azure table storage or Azure CosmosDB.
41
+ All stored data is encrypted using AES encryption.
42
+
43
+ Example usage:
44
+
45
+ ```python
46
+ import flask
47
+ from flask_session_azure import storage_account_interface
48
+
49
+ app = flask.Flask(__name__)
50
+ app.secret_key = "MyVerySecretEnryptionKeyForEverything" # must be at least 16 characters, the longer the better
51
+ connection_string = "DefaultEndpointsProtocol=https;AccountName=someAccount;AccountKey=someKey;EndpointSuffix=core.windows.net"
52
+ app.session_interface = storage_account_interface(connection_string)
53
+ ```
54
+
55
+ This will store the session data in a table called `flasksession`, with a partition key called `default_session`. IF the table does not yet exists, it will be created the first time a session is stored.
56
+ You can overwrite these default when creating the session interface:
57
+ ```python
58
+ app.session_interface = storage_account_interface(connection_string, table_name="mytablename", partition_key="app1", create_table_if_not_exists=False)
59
+ ```
60
+
61
+ If you use this in Azure Function, or Azure Web-Service, you most certainly already have a storage account connection in your environment variable `AzureWebJobsStorage`:
62
+ ```python
63
+ import os
64
+ import flask
65
+ from flask_session_azure import storage_account_interface
66
+
67
+ app = flask.Flask(__name__)
68
+ app.secret_key = "MyVerySecretEnryptionKeyForEverything" # must be at least 16 characters, the longer the better
69
+ connection_string = os.environ.get("AzureWebjobsStorage")
70
+ app.session_interface = storage_account_interface(connection_string)
71
+ ```
72
+
73
+ ## Changelog
74
+
75
+ ### 0.4.5
76
+ - Edited functions to use updated Azure-Core library instead of deprecated Azure-Common. Updated flask elements to use newer app.config format.
77
+
78
+ ### 0.4.4
79
+ - Edited functions to use the Azure-Data-Tables library that is actively maintained by Microsoft
80
+
81
+ ### 0.4.3
82
+ - Fixed issue with secret key length and secret key containing non-asci characters
83
+
84
+ ### 0.4.2
85
+ - Fixed issue if "samesite" cookie value was not set (i.e. set to none). If it is not set, it is now set to Lax to work in an azure function (see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite)
86
+
87
+ ### 0.4.1
88
+ - First public release
@@ -0,0 +1,56 @@
1
+ # Flask Session using Azure table storage (Using Azure-Data-Tables Library)
2
+
3
+
4
+ [![PyPI - License](https://img.shields.io/pypi/l/flask-session-azure)](https://pypi.org/project/flask-session-azure/)
5
+ [![PyPI](https://img.shields.io/pypi/v/flask-session-azure)](https://pypi.org/project/flask-session-azure/)
6
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flask-session-azure)
7
+
8
+ This module can be used as a Flask Session handler for Azure table storage or Azure CosmosDB.
9
+ All stored data is encrypted using AES encryption.
10
+
11
+ Example usage:
12
+
13
+ ```python
14
+ import flask
15
+ from flask_session_azure import storage_account_interface
16
+
17
+ app = flask.Flask(__name__)
18
+ app.secret_key = "MyVerySecretEnryptionKeyForEverything" # must be at least 16 characters, the longer the better
19
+ connection_string = "DefaultEndpointsProtocol=https;AccountName=someAccount;AccountKey=someKey;EndpointSuffix=core.windows.net"
20
+ app.session_interface = storage_account_interface(connection_string)
21
+ ```
22
+
23
+ This will store the session data in a table called `flasksession`, with a partition key called `default_session`. IF the table does not yet exists, it will be created the first time a session is stored.
24
+ You can overwrite these default when creating the session interface:
25
+ ```python
26
+ app.session_interface = storage_account_interface(connection_string, table_name="mytablename", partition_key="app1", create_table_if_not_exists=False)
27
+ ```
28
+
29
+ If you use this in Azure Function, or Azure Web-Service, you most certainly already have a storage account connection in your environment variable `AzureWebJobsStorage`:
30
+ ```python
31
+ import os
32
+ import flask
33
+ from flask_session_azure import storage_account_interface
34
+
35
+ app = flask.Flask(__name__)
36
+ app.secret_key = "MyVerySecretEnryptionKeyForEverything" # must be at least 16 characters, the longer the better
37
+ connection_string = os.environ.get("AzureWebjobsStorage")
38
+ app.session_interface = storage_account_interface(connection_string)
39
+ ```
40
+
41
+ ## Changelog
42
+
43
+ ### 0.4.5
44
+ - Edited functions to use updated Azure-Core library instead of deprecated Azure-Common. Updated flask elements to use newer app.config format.
45
+
46
+ ### 0.4.4
47
+ - Edited functions to use the Azure-Data-Tables library that is actively maintained by Microsoft
48
+
49
+ ### 0.4.3
50
+ - Fixed issue with secret key length and secret key containing non-asci characters
51
+
52
+ ### 0.4.2
53
+ - Fixed issue if "samesite" cookie value was not set (i.e. set to none). If it is not set, it is now set to Lax to work in an azure function (see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite)
54
+
55
+ ### 0.4.1
56
+ - First public release
@@ -0,0 +1,15 @@
1
+ def storage_account_interface(connection_str: str, table_name: str = "flasksession",
2
+ partition_key: str = "default_session", create_table_if_not_exists: bool = True):
3
+ """
4
+ Creates a Session object as replacement for the default flask session
5
+ The Session uses Azure storage accounts or Azure cosmosDB with Table API as backend
6
+ :param connection_str: Connection string to storage account or CosmosDb with Table-API
7
+ :param table_name: the table name. Default is "flasksession". When overwriting this, follow the table name rules
8
+ https://docs.microsoft.com/en-us/rest/api/storageservices/Understanding-the-Table-Service-Data-Model#table-names
9
+ :param partition_key: the partition key within the table. Use a unique partition_key if applications share a table
10
+ :param create_table_if_not_exists: set to False if you do not want this application to create a table for you
11
+ :return: a Flask session manager
12
+ """
13
+ from flask_session_azure.storage_account_session import StorageAccountSessionInterface
14
+ return StorageAccountSessionInterface(connection_str, table_name=table_name, partition_key=partition_key,
15
+ create_table_if_not_exists=create_table_if_not_exists)
@@ -0,0 +1,95 @@
1
+ import base64
2
+ from typing import Dict, List, Union, Tuple
3
+
4
+ from Cryptodome.Cipher import AES
5
+ from azure.core.exceptions import ResourceNotFoundError
6
+ from flask.json.tag import TaggedJSONSerializer
7
+ from azure.data.tables import TableClient
8
+
9
+ class StorageAccount(object):
10
+ json_serializer = TaggedJSONSerializer()
11
+
12
+ def __init__(self, connection_str: str, table_name: str, partition_key: str, create_table_if_not_exists: bool):
13
+ self.table_name = table_name
14
+ self.partition_key = partition_key
15
+ self.create_table_if_not_exists = create_table_if_not_exists
16
+ self.table_service = TableClient.from_connection_string(conn_str=connection_str, table_name=self.table_name)
17
+
18
+ def write(self, key: str, data: dict, encryption_key: bytes) -> None:
19
+ """
20
+ serializes and encrypts the passed dict object object and writes it to the storage
21
+ """
22
+
23
+ data = self.json_serializer.dumps(data)
24
+ encoded_data, tag, nonce = self.encrypt(data, encryption_key)
25
+ entity = {
26
+ "PartitionKey": self.partition_key,
27
+ "RowKey": key,
28
+ "Data": encoded_data,
29
+ "Tag": tag,
30
+ "Nonce": nonce
31
+ }
32
+ try:
33
+ self.table_service.upsert_entity(entity=entity)
34
+ except ResourceNotFoundError:
35
+ if not self.create_table_if_not_exists:
36
+ raise
37
+ self.table_service.create_table()
38
+ self.table_service.upsert_entity(entity=entity)
39
+
40
+ def read(self, key: str, app_key: bytes) -> Union[List[Dict], None]:
41
+ """
42
+ reads encrypted data from storage and decrypts and deserializes it.
43
+ Returns None if no data was found or decryption failed.
44
+ """
45
+ try:
46
+ data = self.table_service.get_entity(self.partition_key, key)
47
+ decoded = self.decrypt(data["Data"], data["Tag"], data["Nonce"], app_key)
48
+ if decoded is not None:
49
+ return self.json_serializer.loads(decoded)
50
+ return None
51
+ except ResourceNotFoundError:
52
+ return None
53
+
54
+ def delete(self, key: str) -> None:
55
+ """
56
+ Removes an element from storage if it exists
57
+ """
58
+ try:
59
+ self.table_service.delete_entity(self.partition_key, key)
60
+ except ResourceNotFoundError:
61
+ pass
62
+
63
+ @staticmethod
64
+ def encrypt(data: str, secret_text: bytes) -> Tuple[str, str, str]:
65
+ """
66
+ encrypts the passed data with the secret text.
67
+ :return: a tuple of three elements: encrypted data, verification_tag and nonce element.
68
+ All elements are base64 encoded strings
69
+ """
70
+ cipher = AES.new(secret_text, AES.MODE_EAX)
71
+ ciphertext, tag = cipher.encrypt_and_digest((data.encode("utf-8")))
72
+ return (base64.b64encode(ciphertext).decode("ascii"),
73
+ base64.b64encode(tag).decode("ascii"),
74
+ base64.b64encode(cipher.nonce).decode("ascii"))
75
+
76
+ @staticmethod
77
+ def decrypt(encrypted_data: str, verification_tag: str, nonce: str, secret_text: bytes) -> Union[str, None]:
78
+ """
79
+ Decrypts encoded data using the passed secret_text
80
+ :param encrypted_data: as base64 encoded string or byte array
81
+ :param verification_tag: as base64 encoded string or byte array
82
+ :param nonce: as base64 encoded string or byte array
83
+ :param secret_text: the same secret text with wich the element was encoded
84
+ :return: the plaintext on success, None if the data could not be decoded or verified
85
+ """
86
+ nonce = base64.b64decode(nonce)
87
+ cipher = AES.new(secret_text, AES.MODE_EAX, nonce=nonce)
88
+ data = base64.b64decode(encrypted_data)
89
+ plaintext = cipher.decrypt(data)
90
+ tag = base64.b64decode(verification_tag)
91
+ try:
92
+ cipher.verify(tag)
93
+ return plaintext.decode("utf-8")
94
+ except ValueError:
95
+ return None
@@ -0,0 +1,102 @@
1
+ from uuid import uuid4
2
+
3
+ import flask
4
+ from flask.sessions import SessionMixin, SessionInterface
5
+ from werkzeug.datastructures import CallbackDict
6
+
7
+ from flask_session_azure.storage_account import StorageAccount
8
+
9
+
10
+ class StorageAccountSession(CallbackDict, SessionMixin):
11
+ def __init__(self, sid: str, initial=None):
12
+ def on_update(self):
13
+ self.modified = True
14
+
15
+ CallbackDict.__init__(self, initial, on_update)
16
+ self.sid = sid
17
+ self.modified = False
18
+
19
+
20
+ class StorageAccountSessionInterface(SessionInterface):
21
+ """
22
+ Replacement session interface for flask that uses Azure-Storage account tables as storage backend
23
+ Data is encrypted and tagged for manipulation using AES encryption
24
+ """
25
+ session_class = StorageAccountSession
26
+
27
+ def __init__(self, connection_str: str, table_name="flasksession", partition_key="default_session",
28
+ create_table_if_not_exists: bool = True):
29
+ """
30
+ :param connection_str: the connection string to the azure table storage (or cosmos db)
31
+ :param table_name: the table name. Default is "flasksession". When overwriting this, follow the table name rules
32
+ https://docs.microsoft.com/en-us/rest/api/storageservices/Understanding-the-Table-Service-Data-Model#table-names
33
+ :param partition_key: the partition key within the table. Use a unique partition_key if applications share a table
34
+ """
35
+ self.storage = StorageAccount(connection_str, table_name, partition_key, create_table_if_not_exists)
36
+
37
+ @staticmethod
38
+ def get_encryption_key_from_app_secret(app) -> bytes:
39
+ """
40
+ Checks if the app secret_key is set, and if it is long enough to use it as AES encryption key.
41
+ Returns the key truncated to a length of multiples of 8, to use it as AES encryption key
42
+ """
43
+ if app.secret_key is None:
44
+ raise RuntimeError(
45
+ "The session is unavailable because no secret key was set. "
46
+ "Set the secret_key on the application to something unique and secret.")
47
+ try:
48
+ # if secret_text is a string, make it bytes
49
+ secret_key = app.secret_key.encode("utf-8")
50
+ except AttributeError:
51
+ secret_key = app.secret_key
52
+
53
+ if len(secret_key) < 16:
54
+ raise RuntimeError(
55
+ "The session is unavailable because the secret is too short. "
56
+ f"The secret must be 16 characters or longer, but is only {len(app.secret_key)} character(s).")
57
+
58
+ # only use multiples of 8 from the secret_key tu use it as AES encryption_key
59
+ characters = len(app.secret_key)
60
+ characters = min(32, characters - characters % 8)
61
+ return secret_key[:characters]
62
+
63
+ def open_session(self, app: flask.Flask, request: flask.Request) -> StorageAccountSession:
64
+ """
65
+ Reads the session data from table storage, decrypts the data and returns the session object
66
+ """
67
+ sid = request.cookies.get(app.config["SESSION_COOKIE_NAME"])
68
+ if not sid:
69
+ sid = str(uuid4())
70
+ return self.session_class(sid=sid)
71
+ encryption_key = self.get_encryption_key_from_app_secret(app)
72
+ data = self.storage.read(sid, encryption_key)
73
+ if data is not None:
74
+ return self.session_class(sid=sid, initial=data)
75
+ return self.session_class(sid=sid)
76
+
77
+ def save_session(self, app: flask.Flask, session: StorageAccountSession, response: flask.Response) -> None:
78
+ """
79
+ Serializes the data to JSON, encrypts the data using AES
80
+ and stores the encrypted data along with a verification tag to azure storage.
81
+ Only stores the UUID of the table entry inside the session_cookie
82
+ """
83
+
84
+ domain = self.get_cookie_domain(app)
85
+ path = self.get_cookie_path(app)
86
+ if not session:
87
+ if session.modified:
88
+ self.storage.delete(session.sid)
89
+ response.delete_cookie(app.config["SESSION_COOKIE_NAME"], domain=domain, path=path)
90
+ return
91
+
92
+ if session.modified:
93
+ encryption_key = self.get_encryption_key_from_app_secret(app)
94
+ self.storage.write(session.sid, dict(session), encryption_key)
95
+ httponly = True
96
+ secure = self.get_cookie_secure(app)
97
+ samesite = self.get_cookie_samesite(app)
98
+ if samesite is None:
99
+ samesite = 'Lax'
100
+ expires = self.get_expiration_time(app, session)
101
+ response.set_cookie(app.config["SESSION_COOKIE_NAME"], session.sid, expires=expires, httponly=httponly, domain=domain,
102
+ path=path, secure=secure, samesite=samesite)
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: flask_session_azure_c
3
+ Version: 0.4.6
4
+ Summary: Flask Session using Azure Table Storage or CosmosDB table API
5
+ Home-page: https://github.com/bassmandan/FlaskAzureTableSession
6
+ Author: Claas Diederichs, Daniel Harding
7
+ Author-email:
8
+ License: MIT
9
+ Classifier: Framework :: Flask
10
+ Classifier: Programming Language :: Python :: 3.6
11
+ Classifier: Programming Language :: Python :: 3.7
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.6
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: azure-data-tables
20
+ Requires-Dist: pycryptodomex>=3.9.7
21
+ Requires-Dist: flask
22
+ Dynamic: author
23
+ Dynamic: classifier
24
+ Dynamic: description
25
+ Dynamic: description-content-type
26
+ Dynamic: home-page
27
+ Dynamic: license
28
+ Dynamic: license-file
29
+ Dynamic: requires-dist
30
+ Dynamic: requires-python
31
+ Dynamic: summary
32
+
33
+ # Flask Session using Azure table storage (Using Azure-Data-Tables Library)
34
+
35
+
36
+ [![PyPI - License](https://img.shields.io/pypi/l/flask-session-azure)](https://pypi.org/project/flask-session-azure/)
37
+ [![PyPI](https://img.shields.io/pypi/v/flask-session-azure)](https://pypi.org/project/flask-session-azure/)
38
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flask-session-azure)
39
+
40
+ This module can be used as a Flask Session handler for Azure table storage or Azure CosmosDB.
41
+ All stored data is encrypted using AES encryption.
42
+
43
+ Example usage:
44
+
45
+ ```python
46
+ import flask
47
+ from flask_session_azure import storage_account_interface
48
+
49
+ app = flask.Flask(__name__)
50
+ app.secret_key = "MyVerySecretEnryptionKeyForEverything" # must be at least 16 characters, the longer the better
51
+ connection_string = "DefaultEndpointsProtocol=https;AccountName=someAccount;AccountKey=someKey;EndpointSuffix=core.windows.net"
52
+ app.session_interface = storage_account_interface(connection_string)
53
+ ```
54
+
55
+ This will store the session data in a table called `flasksession`, with a partition key called `default_session`. IF the table does not yet exists, it will be created the first time a session is stored.
56
+ You can overwrite these default when creating the session interface:
57
+ ```python
58
+ app.session_interface = storage_account_interface(connection_string, table_name="mytablename", partition_key="app1", create_table_if_not_exists=False)
59
+ ```
60
+
61
+ If you use this in Azure Function, or Azure Web-Service, you most certainly already have a storage account connection in your environment variable `AzureWebJobsStorage`:
62
+ ```python
63
+ import os
64
+ import flask
65
+ from flask_session_azure import storage_account_interface
66
+
67
+ app = flask.Flask(__name__)
68
+ app.secret_key = "MyVerySecretEnryptionKeyForEverything" # must be at least 16 characters, the longer the better
69
+ connection_string = os.environ.get("AzureWebjobsStorage")
70
+ app.session_interface = storage_account_interface(connection_string)
71
+ ```
72
+
73
+ ## Changelog
74
+
75
+ ### 0.4.5
76
+ - Edited functions to use updated Azure-Core library instead of deprecated Azure-Common. Updated flask elements to use newer app.config format.
77
+
78
+ ### 0.4.4
79
+ - Edited functions to use the Azure-Data-Tables library that is actively maintained by Microsoft
80
+
81
+ ### 0.4.3
82
+ - Fixed issue with secret key length and secret key containing non-asci characters
83
+
84
+ ### 0.4.2
85
+ - Fixed issue if "samesite" cookie value was not set (i.e. set to none). If it is not set, it is now set to Lax to work in an azure function (see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite)
86
+
87
+ ### 0.4.1
88
+ - First public release
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ flask_session_azure/__init__.py
5
+ flask_session_azure/storage_account.py
6
+ flask_session_azure/storage_account_session.py
7
+ flask_session_azure_c.egg-info/PKG-INFO
8
+ flask_session_azure_c.egg-info/SOURCES.txt
9
+ flask_session_azure_c.egg-info/dependency_links.txt
10
+ flask_session_azure_c.egg-info/requires.txt
11
+ flask_session_azure_c.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ azure-data-tables
2
+ pycryptodomex>=3.9.7
3
+ flask
@@ -0,0 +1 @@
1
+ flask_session_azure
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,32 @@
1
+ from setuptools import setup
2
+
3
+ with open("README.md", "r") as fh:
4
+ long_description = fh.read()
5
+
6
+ setup(
7
+ name='flask_session_azure_c',
8
+ version='0.4.6',
9
+ packages=['flask_session_azure'],
10
+ url='https://github.com/bassmandan/FlaskAzureTableSession',
11
+ license='MIT',
12
+ author='Claas Diederichs, Daniel Harding',
13
+ author_email='',
14
+ description='Flask Session using Azure Table Storage or CosmosDB table API',
15
+ long_description=long_description,
16
+ long_description_content_type="text/markdown",
17
+ classifiers=[
18
+ "Framework :: Flask",
19
+ "Programming Language :: Python :: 3.6",
20
+ "Programming Language :: Python :: 3.7",
21
+ "Programming Language :: Python :: 3.8",
22
+ "Programming Language :: Python :: 3.9",
23
+ "License :: OSI Approved :: MIT License",
24
+ "Operating System :: OS Independent",
25
+ ],
26
+ python_requires='>=3.6',
27
+ install_requires=[
28
+ "azure-data-tables",
29
+ "pycryptodomex>=3.9.7",
30
+ "flask"
31
+ ]
32
+ )