fastapi-startkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_startkit/__init__.py +3 -0
- fastapi_startkit/application.py +40 -0
- fastapi_startkit/configuration/Configuration.py +80 -0
- fastapi_startkit/configuration/__init__.py +2 -0
- fastapi_startkit/configuration/helpers.py +5 -0
- fastapi_startkit/configuration/providers/ConfigurationProvider.py +16 -0
- fastapi_startkit/configuration/providers/__init__.py +1 -0
- fastapi_startkit/container/__init__.py +1 -0
- fastapi_startkit/container/container.py +494 -0
- fastapi_startkit/environment/environment.py +76 -0
- fastapi_startkit/exceptions/DD.py +38 -0
- fastapi_startkit/exceptions/ExceptionHandler.py +76 -0
- fastapi_startkit/exceptions/__init__.py +38 -0
- fastapi_startkit/exceptions/exceptionite/__init__.py +0 -0
- fastapi_startkit/exceptions/exceptionite/blocks.py +101 -0
- fastapi_startkit/exceptions/exceptionite/controllers.py +13 -0
- fastapi_startkit/exceptions/exceptionite/solutions.py +66 -0
- fastapi_startkit/exceptions/exceptionite/tabs.py +19 -0
- fastapi_startkit/exceptions/exceptions.py +218 -0
- fastapi_startkit/exceptions/handlers/DumpExceptionHandler.py +104 -0
- fastapi_startkit/exceptions/handlers/HttpExceptionHandler.py +28 -0
- fastapi_startkit/exceptions/handlers/ModelNotFoundHandler.py +13 -0
- fastapi_startkit/facades/Auth.py +5 -0
- fastapi_startkit/facades/Auth.pyi +32 -0
- fastapi_startkit/facades/Broadcast.py +5 -0
- fastapi_startkit/facades/Cache.py +5 -0
- fastapi_startkit/facades/Config.py +5 -0
- fastapi_startkit/facades/Config.pyi +14 -0
- fastapi_startkit/facades/Dump.py +5 -0
- fastapi_startkit/facades/Dump.pyi +26 -0
- fastapi_startkit/facades/Facade.py +5 -0
- fastapi_startkit/facades/Gate.py +5 -0
- fastapi_startkit/facades/Gate.pyi +32 -0
- fastapi_startkit/facades/Hash.py +5 -0
- fastapi_startkit/facades/Hash.pyi +28 -0
- fastapi_startkit/facades/Loader.py +5 -0
- fastapi_startkit/facades/Loader.pyi +30 -0
- fastapi_startkit/facades/Mail.py +5 -0
- fastapi_startkit/facades/Mail.pyi +14 -0
- fastapi_startkit/facades/Notification.py +5 -0
- fastapi_startkit/facades/Notification.pyi +25 -0
- fastapi_startkit/facades/Queue.py +5 -0
- fastapi_startkit/facades/Queue.pyi +10 -0
- fastapi_startkit/facades/RateLimiter.py +5 -0
- fastapi_startkit/facades/RateLimiter.pyi +43 -0
- fastapi_startkit/facades/Request.py +5 -0
- fastapi_startkit/facades/Request.pyi +88 -0
- fastapi_startkit/facades/Response.py +5 -0
- fastapi_startkit/facades/Response.pyi +68 -0
- fastapi_startkit/facades/Session.py +5 -0
- fastapi_startkit/facades/Session.pyi +59 -0
- fastapi_startkit/facades/Storage.py +5 -0
- fastapi_startkit/facades/Storage.pyi +12 -0
- fastapi_startkit/facades/Url.py +5 -0
- fastapi_startkit/facades/Url.pyi +22 -0
- fastapi_startkit/facades/View.py +5 -0
- fastapi_startkit/facades/View.pyi +54 -0
- fastapi_startkit/facades/__init__.py +19 -0
- fastapi_startkit/loader/Loader.py +78 -0
- fastapi_startkit/loader/__init__.py +1 -0
- fastapi_startkit/providers/ConfigurationProvider.py +13 -0
- fastapi_startkit/providers/Provider.py +14 -0
- fastapi_startkit/providers/__init__.py +4 -0
- fastapi_startkit/utils/__init__.py +0 -0
- fastapi_startkit/utils/collections.py +545 -0
- fastapi_startkit/utils/console.py +39 -0
- fastapi_startkit/utils/data/mime.types +1863 -0
- fastapi_startkit/utils/filesystem.py +100 -0
- fastapi_startkit/utils/http.py +101 -0
- fastapi_startkit/utils/location.py +90 -0
- fastapi_startkit/utils/str.py +120 -0
- fastapi_startkit/utils/structures.py +97 -0
- fastapi_startkit/utils/time.py +58 -0
- fastapi_startkit-0.1.0.dist-info/METADATA +13 -0
- fastapi_startkit-0.1.0.dist-info/RECORD +76 -0
- fastapi_startkit-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import pathlib
|
|
4
|
+
import mimetypes
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def make_directory(directory):
|
|
8
|
+
"""Create a directory at the given path for a file if it does not exist"""
|
|
9
|
+
if not os.path.isfile(directory):
|
|
10
|
+
if not os.path.exists(os.path.dirname(directory)):
|
|
11
|
+
# Create the path to the model if it does not exist
|
|
12
|
+
os.makedirs(os.path.dirname(directory))
|
|
13
|
+
|
|
14
|
+
return True
|
|
15
|
+
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def file_exists(directory):
|
|
20
|
+
"""Create a directory at the given path for a file if it does not exist"""
|
|
21
|
+
return os.path.exists(os.path.dirname(directory))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def make_full_directory(directory):
|
|
25
|
+
"""Create all directories to the given path if they do not exist"""
|
|
26
|
+
if not os.path.isfile(directory):
|
|
27
|
+
if not os.path.exists(directory):
|
|
28
|
+
# Create the path to the model if it does not exist
|
|
29
|
+
os.makedirs(directory)
|
|
30
|
+
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def creation_date(path_to_file):
|
|
37
|
+
"""Try to get the date that a file was created, falling back to when it was
|
|
38
|
+
last modified if that isn't possible.
|
|
39
|
+
"""
|
|
40
|
+
if platform.system() == "Windows":
|
|
41
|
+
return os.path.getctime(path_to_file)
|
|
42
|
+
else:
|
|
43
|
+
stat = os.stat(path_to_file)
|
|
44
|
+
try:
|
|
45
|
+
return stat.st_birthtime
|
|
46
|
+
except AttributeError:
|
|
47
|
+
# We're probably on Linux. No easy way to get creation dates here,
|
|
48
|
+
# so we'll settle for when its content was last modified.
|
|
49
|
+
return stat.st_mtime
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def modified_date(path_to_file):
|
|
53
|
+
if platform.system() == "Windows":
|
|
54
|
+
return os.path.getmtime(path_to_file)
|
|
55
|
+
else:
|
|
56
|
+
stat = os.stat(path_to_file)
|
|
57
|
+
try:
|
|
58
|
+
return stat.st_mtime
|
|
59
|
+
except AttributeError:
|
|
60
|
+
# We're probably on Linux. No easy way to get creation dates here,
|
|
61
|
+
# so we'll settle for when its content was last modified.
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def render_stub_file(stub_file, name):
|
|
66
|
+
"""Read stub file, replace placeholders and return content."""
|
|
67
|
+
with open(stub_file, "r") as f:
|
|
68
|
+
content = f.read()
|
|
69
|
+
content = content.replace("__class__", name)
|
|
70
|
+
return content
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_module_dir(module_file):
|
|
74
|
+
return os.path.dirname(os.path.realpath(module_file))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
mimetypes.init([os.path.join(get_module_dir(__file__), "data/mime.types")])
|
|
78
|
+
|
|
79
|
+
KNOWN_MIME_TYPES = mimetypes.types_map.keys()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_extension(filepath: str, without_dot=False) -> str:
|
|
83
|
+
"""Get file extension from a filepath. If without_dot=True the . prefix will be removed from
|
|
84
|
+
the extension."""
|
|
85
|
+
extension_parts = pathlib.Path(filepath).suffixes
|
|
86
|
+
extension = ""
|
|
87
|
+
if extension_parts:
|
|
88
|
+
# try to join all the parts until only one part to check if it's a known extension
|
|
89
|
+
for i in range(len(extension_parts)):
|
|
90
|
+
try_extension = "".join(extension_parts[i:])
|
|
91
|
+
if try_extension in KNOWN_MIME_TYPES:
|
|
92
|
+
extension = try_extension
|
|
93
|
+
break
|
|
94
|
+
# if no known extension found, return the last part as the extension
|
|
95
|
+
if not extension:
|
|
96
|
+
extension = extension_parts[-1]
|
|
97
|
+
|
|
98
|
+
if without_dot:
|
|
99
|
+
extension = extension[1:]
|
|
100
|
+
return extension
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Helpers for working with HTTP."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
HTTP_STATUS_CODES = {
|
|
5
|
+
100: "100 Continue",
|
|
6
|
+
101: "101 Switching Protocol",
|
|
7
|
+
102: "102 Processing",
|
|
8
|
+
103: "Early Hints",
|
|
9
|
+
200: "200 OK",
|
|
10
|
+
201: "201 Created",
|
|
11
|
+
202: "202 Accepted",
|
|
12
|
+
203: "203 Non-Authoritative Information",
|
|
13
|
+
204: "204 No Content",
|
|
14
|
+
205: "205 Reset Content",
|
|
15
|
+
206: "206 Partial Content",
|
|
16
|
+
207: "207 Multi-Status",
|
|
17
|
+
208: "208 Multi-Status",
|
|
18
|
+
226: "226 IM Used",
|
|
19
|
+
300: "300 Multiple Choice",
|
|
20
|
+
301: "301 Moved Permanently",
|
|
21
|
+
302: "302 Found",
|
|
22
|
+
303: "303 See Other",
|
|
23
|
+
304: "304 Not Modified",
|
|
24
|
+
307: "307 Temporary Redirect",
|
|
25
|
+
308: "308 Permanent Redirect",
|
|
26
|
+
400: "400 Bad Request",
|
|
27
|
+
401: "401 Unauthorized",
|
|
28
|
+
402: "402 Payment Required",
|
|
29
|
+
403: "403 Forbidden",
|
|
30
|
+
404: "404 Not Found",
|
|
31
|
+
405: "405 Method Not Allowed",
|
|
32
|
+
406: "406 Not Acceptable",
|
|
33
|
+
407: "407 Proxy Authentication Required",
|
|
34
|
+
408: "408 Request Timeout",
|
|
35
|
+
409: "409 Conflict",
|
|
36
|
+
410: "410 Gone",
|
|
37
|
+
411: "411 Length Required",
|
|
38
|
+
412: "412 Precondition Failed",
|
|
39
|
+
413: "413 Payload Too Large",
|
|
40
|
+
414: "414 URI Too Long",
|
|
41
|
+
415: "415 Unsupported Media Type",
|
|
42
|
+
416: "416 Requested Range Not Satisfiable",
|
|
43
|
+
417: "417 Expectation Failed",
|
|
44
|
+
418: "418 I'm a teapot",
|
|
45
|
+
421: "421 Misdirected Request",
|
|
46
|
+
422: "422 Unprocessable Entity",
|
|
47
|
+
423: "423 Locked",
|
|
48
|
+
424: "424 Failed Dependency",
|
|
49
|
+
425: "425 Too Early",
|
|
50
|
+
426: "426 Upgrade Required",
|
|
51
|
+
428: "428 Precondition Required",
|
|
52
|
+
429: "429 Too Many Requests",
|
|
53
|
+
431: "431 Request Header Fields Too Large",
|
|
54
|
+
451: "451 Unavailable For Legal Reasons",
|
|
55
|
+
500: "500 Internal Server Error",
|
|
56
|
+
501: "501 Not Implemented",
|
|
57
|
+
502: "502 Bad Gateway",
|
|
58
|
+
503: "503 Service Unavailable",
|
|
59
|
+
504: "504 Gateway Timeout",
|
|
60
|
+
505: "505 HTTP Version Not Supported",
|
|
61
|
+
506: "506 Variant Also Negotiates",
|
|
62
|
+
507: "507 Insufficient Storage",
|
|
63
|
+
508: "508 Loop Detected",
|
|
64
|
+
510: "510 Not Extended",
|
|
65
|
+
511: "511 Network Authentication Required",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def generate_wsgi(wsgi={}, path="/", query_string="", method="GET"):
|
|
70
|
+
"""Generate the WSGI environment dictionary that we receive from a HTTP request."""
|
|
71
|
+
import io
|
|
72
|
+
|
|
73
|
+
data = {
|
|
74
|
+
"wsgi.version": (1, 0),
|
|
75
|
+
"wsgi.multithread": False,
|
|
76
|
+
"wsgi.multiprocess": True,
|
|
77
|
+
"wsgi.run_once": False,
|
|
78
|
+
"wsgi.input": io.BytesIO(),
|
|
79
|
+
"SERVER_SOFTWARE": "gunicorn/19.7.1",
|
|
80
|
+
"REQUEST_METHOD": method,
|
|
81
|
+
"QUERY_STRING": query_string,
|
|
82
|
+
"RAW_URI": path,
|
|
83
|
+
"SERVER_PROTOCOL": "HTTP/1.1",
|
|
84
|
+
"HTTP_HOST": "127.0.0.1:8000",
|
|
85
|
+
"HTTP_ACCEPT": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
86
|
+
"HTTP_UPGRADE_INSECURE_REQUESTS": "1",
|
|
87
|
+
"HTTP_COOKIE": "",
|
|
88
|
+
"HTTP_USER_AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/604.4.7 (KHTML, like Gecko) Version/11.0.2 Safari/604.4.7",
|
|
89
|
+
"HTTP_ACCEPT_LANGUAGE": "en-us",
|
|
90
|
+
"HTTP_ACCEPT_ENCODING": "gzip, deflate",
|
|
91
|
+
"HTTP_CONNECTION": "keep-alive",
|
|
92
|
+
"wsgi.url_scheme": "http",
|
|
93
|
+
"REMOTE_ADDR": "127.0.0.1",
|
|
94
|
+
"REMOTE_PORT": "62241",
|
|
95
|
+
"SERVER_NAME": "127.0.0.1",
|
|
96
|
+
"SERVER_PORT": "8000",
|
|
97
|
+
"PATH_INFO": path,
|
|
98
|
+
"SCRIPT_NAME": "",
|
|
99
|
+
}
|
|
100
|
+
data.update(wsgi)
|
|
101
|
+
return data
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Helpers to resolve absolute paths to the different app resources using a configured
|
|
2
|
+
location."""
|
|
3
|
+
from os.path import join, abspath
|
|
4
|
+
|
|
5
|
+
from .str import as_filepath
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _build_path(location_key, relative_path, absolute):
|
|
9
|
+
from wsgi import application
|
|
10
|
+
|
|
11
|
+
relative_dir = join(as_filepath(application.make(location_key)), relative_path)
|
|
12
|
+
return abspath(relative_dir) if absolute else relative_dir
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def base_path(relative_path=""):
|
|
16
|
+
"""Build the absolute path to the project root directory or build the absolute path to a
|
|
17
|
+
given file relative to the project root directory."""
|
|
18
|
+
return abspath(relative_path)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def views_path(relative_path="", absolute=True):
|
|
22
|
+
"""Build the absolute path to the project views directory or build the absolute path to a given
|
|
23
|
+
file relative to the project views directory.
|
|
24
|
+
|
|
25
|
+
The relative path can be returned instead by setting absolute=False."""
|
|
26
|
+
return _build_path("views.location", relative_path, absolute)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def controllers_path(relative_path="", absolute=True):
|
|
30
|
+
"""Build the absolute path to the project controllers directory or build the absolute path to a given
|
|
31
|
+
file relative to the project controllers directory.
|
|
32
|
+
|
|
33
|
+
The relative path can be returned instead by setting absolute=False."""
|
|
34
|
+
return _build_path("controllers.location", relative_path, absolute)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def mailables_path(relative_path="", absolute=True):
|
|
38
|
+
"""Build the absolute path to the project controllers directory or build the absolute path to a given
|
|
39
|
+
file relative to the project controllers directory.
|
|
40
|
+
|
|
41
|
+
The relative path can be returned instead by setting absolute=False."""
|
|
42
|
+
return _build_path("mailables.location", relative_path, absolute)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def config_path(relative_path="", absolute=True):
|
|
46
|
+
"""Build the absolute path to the project configuration directory or build the absolute path to a given
|
|
47
|
+
file relative to the project configuration directory.
|
|
48
|
+
|
|
49
|
+
The relative path can be returned instead by setting absolute=False."""
|
|
50
|
+
return _build_path("config.location", relative_path, absolute)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def migrations_path(relative_path="", absolute=True):
|
|
54
|
+
"""Build the absolute path to the project migrations directory or build the absolute path to a given
|
|
55
|
+
file relative to the project migrations directory.
|
|
56
|
+
|
|
57
|
+
The relative path can be returned instead by setting absolute=False."""
|
|
58
|
+
return _build_path("migrations.location", relative_path, absolute)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def seeds_path(relative_path="", absolute=True):
|
|
62
|
+
"""Build the absolute path to the project seeds directory or build the absolute path to a given
|
|
63
|
+
file relative to the project seeds directory.
|
|
64
|
+
|
|
65
|
+
The relative path can be returned instead by setting absolute=False."""
|
|
66
|
+
return _build_path("seeds.location", relative_path, absolute)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def jobs_path(relative_path="", absolute=True):
|
|
70
|
+
"""Build the absolute path to the project jobs directory or build the absolute path to a given
|
|
71
|
+
file relative to the project jobs directory.
|
|
72
|
+
|
|
73
|
+
The relative path can be returned instead by setting absolute=False."""
|
|
74
|
+
return _build_path("jobs.location", relative_path, absolute)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def resources_path(relative_path="", absolute=True):
|
|
78
|
+
"""Build the absolute path to the project resources directory or build the absolute path to a given
|
|
79
|
+
file relative to the project resources directory.
|
|
80
|
+
|
|
81
|
+
The relative path can be returned instead by setting absolute=False."""
|
|
82
|
+
return _build_path("resources.location", relative_path, absolute)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def models_path(relative_path="", absolute=True):
|
|
86
|
+
"""Build the absolute path to the project models directory or build the absolute path to a given
|
|
87
|
+
file relative to the project models directory.
|
|
88
|
+
|
|
89
|
+
The relative path can be returned instead by setting absolute=False."""
|
|
90
|
+
return _build_path("models.location", relative_path, absolute)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""String generators and helpers"""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import string
|
|
5
|
+
from urllib import parse
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def random_string(length=4):
|
|
10
|
+
"""Generate a random string based on the given length.
|
|
11
|
+
|
|
12
|
+
Keyword Arguments:
|
|
13
|
+
length {int} -- The amount of the characters to generate (default: {4})
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
string
|
|
17
|
+
"""
|
|
18
|
+
return "".join(
|
|
19
|
+
random.choice(string.ascii_uppercase + string.digits) for _ in range(length)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def modularize(file_path, suffix=".py"):
|
|
24
|
+
"""Transforms a file path to a dotted path. On UNIX paths contains / and on Windows \\.
|
|
25
|
+
|
|
26
|
+
Keyword Arguments:
|
|
27
|
+
file_path {str} -- A file path such app/controllers
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
value {str} -- a dotted path such as app.controllers
|
|
31
|
+
"""
|
|
32
|
+
# if the file had the .py extension remove it as it's not needed for a module
|
|
33
|
+
return removesuffix(file_path.replace("/", ".").replace("\\", "."), suffix)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def as_filepath(dotted_path):
|
|
37
|
+
"""Inverse of modularize, transforms a dotted path to a file path (with /).
|
|
38
|
+
|
|
39
|
+
Keyword Arguments:
|
|
40
|
+
dotted_path {str} -- A dotted path such app.controllers
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
value {str} -- a file path such as app/controllers
|
|
44
|
+
"""
|
|
45
|
+
return dotted_path.replace(".", "/")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def removeprefix(string, prefix):
|
|
49
|
+
"""Implementation of str.removeprefix() function available for Python versions lower than 3.9."""
|
|
50
|
+
if string.startswith(prefix):
|
|
51
|
+
return string[len(prefix) :]
|
|
52
|
+
else:
|
|
53
|
+
return string
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def removesuffix(string, suffix):
|
|
57
|
+
"""Implementation of str.removesuffix() function available for Python versions lower than 3.9."""
|
|
58
|
+
if suffix and string.endswith(suffix):
|
|
59
|
+
return string[: -len(suffix)]
|
|
60
|
+
else:
|
|
61
|
+
return string
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def match(string: str, ref_string: str) -> str:
|
|
65
|
+
"""Check if a given string matches a reference string. Wildcard '*' can be used at start, end
|
|
66
|
+
or middle of the string."""
|
|
67
|
+
if ref_string.startswith("*"):
|
|
68
|
+
ref_string = ref_string.replace("*", "")
|
|
69
|
+
return string.endswith(ref_string)
|
|
70
|
+
elif ref_string.endswith("*"):
|
|
71
|
+
ref_string = ref_string.replace("*", "")
|
|
72
|
+
return string.startswith(ref_string)
|
|
73
|
+
elif "*" in ref_string:
|
|
74
|
+
split_search = ref_string.split("*")
|
|
75
|
+
return string.startswith(split_search[0]) and string.endswith(split_search[1])
|
|
76
|
+
else:
|
|
77
|
+
return ref_string == string
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def add_query_params(url: str, query_params: dict) -> str:
|
|
81
|
+
"""Add query params dict to a given url (which can already contain some query parameters)."""
|
|
82
|
+
path_result = parse.urlsplit(url)
|
|
83
|
+
|
|
84
|
+
base_url = (
|
|
85
|
+
f"{path_result.scheme}://{path_result.hostname}" if path_result.hostname else ""
|
|
86
|
+
)
|
|
87
|
+
base_path = path_result.path
|
|
88
|
+
|
|
89
|
+
# parse existing query parameters if any
|
|
90
|
+
existing_query_params = dict(parse.parse_qsl(path_result.query))
|
|
91
|
+
all_query_params = {**existing_query_params, **query_params}
|
|
92
|
+
|
|
93
|
+
# add query parameters to url if any
|
|
94
|
+
if all_query_params:
|
|
95
|
+
base_path += "?" + parse.urlencode(all_query_params)
|
|
96
|
+
|
|
97
|
+
result_url = f"{base_url}{base_path}"
|
|
98
|
+
|
|
99
|
+
# add fragment if exists
|
|
100
|
+
if path_result.fragment:
|
|
101
|
+
result_url = f"{result_url}#{path_result.fragment}"
|
|
102
|
+
|
|
103
|
+
return result_url
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_controller_name(controller: "str|Any") -> str:
|
|
107
|
+
"""Get a controller string name from a controller argument used in routes."""
|
|
108
|
+
# controller is a class or class.method
|
|
109
|
+
if hasattr(controller, "__qualname__"):
|
|
110
|
+
if "." in controller.__qualname__:
|
|
111
|
+
controller_str = controller.__qualname__.replace(".", "@")
|
|
112
|
+
else:
|
|
113
|
+
controller_str = f"{controller.__qualname__}@__call__"
|
|
114
|
+
# controller is an instance, so the method will automatically be __call__
|
|
115
|
+
elif not isinstance(controller, str):
|
|
116
|
+
controller_str = f"{controller.__class__.__qualname__}@__call__"
|
|
117
|
+
# controller is anything else: "Controller@method"
|
|
118
|
+
else:
|
|
119
|
+
controller_str = str(controller)
|
|
120
|
+
return controller_str
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Helpers for multiple data structures"""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
from importlib.abc import Loader
|
|
5
|
+
from dotty_dict import dotty
|
|
6
|
+
|
|
7
|
+
from ..exceptions.exceptions import LoaderNotFound
|
|
8
|
+
|
|
9
|
+
from .str import modularize
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load(path, object_name=None, default=None, raise_exception=False):
|
|
13
|
+
"""Load the given object from a Python module located at path and returns a default
|
|
14
|
+
value if not found. If no object name is provided, loads the module.
|
|
15
|
+
|
|
16
|
+
Arguments:
|
|
17
|
+
path {str} -- A file path or a dotted path of a Python module
|
|
18
|
+
object {str} -- The object name to load in this module (None)
|
|
19
|
+
default {str} -- The default value to return if object not found in module (None)
|
|
20
|
+
Returns:
|
|
21
|
+
{object} -- The value (or default) read in the module or the module if no object name
|
|
22
|
+
"""
|
|
23
|
+
# modularize path if needed
|
|
24
|
+
module_path = modularize(path)
|
|
25
|
+
# module = pydoc.locate(dotted_path)
|
|
26
|
+
try:
|
|
27
|
+
module = importlib.import_module(module_path)
|
|
28
|
+
except Exception as e:
|
|
29
|
+
error_message = (
|
|
30
|
+
f"'{module_path}' not found OR error when importing this module: {str(e)}"
|
|
31
|
+
)
|
|
32
|
+
print("Warning: " + error_message)
|
|
33
|
+
|
|
34
|
+
if raise_exception:
|
|
35
|
+
raise LoaderNotFound(error_message)
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
if object_name is None:
|
|
39
|
+
return module
|
|
40
|
+
else:
|
|
41
|
+
try:
|
|
42
|
+
return getattr(module, object_name)
|
|
43
|
+
except KeyError:
|
|
44
|
+
if raise_exception:
|
|
45
|
+
raise LoaderNotFound(f"{object_name} not found in {module_path}")
|
|
46
|
+
else:
|
|
47
|
+
return default
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def data(dictionary={}):
|
|
51
|
+
"""Transform the given dictionary to be read/written with dot notation.
|
|
52
|
+
|
|
53
|
+
Arguments:
|
|
54
|
+
dictionary {dict} -- a dictionary structure
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
{dict} -- A dot dictionary
|
|
58
|
+
"""
|
|
59
|
+
return dotty(dictionary)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def data_get(dictionary, key, default=None):
|
|
63
|
+
"""Read dictionary value from key using nested notation.
|
|
64
|
+
|
|
65
|
+
Arguments:
|
|
66
|
+
dictionary {dict} -- a dictionary structure
|
|
67
|
+
key {str} -- the dotted (or not) key to look for
|
|
68
|
+
default {object} -- the default value to return if the key does not exist (None)
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
value {object}
|
|
72
|
+
"""
|
|
73
|
+
# dotty dict uses : instead of * for wildcards
|
|
74
|
+
dotty_key = key.replace("*", ":")
|
|
75
|
+
return data(dictionary).get(dotty_key, default)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def data_set(dictionary, key, value, overwrite=True):
|
|
79
|
+
"""Set dictionary value at key using nested notation. Values are overridden by default but
|
|
80
|
+
this behaviour can be changed by passing overwrite=False.
|
|
81
|
+
The dictionary is edited in place but is also returned.
|
|
82
|
+
|
|
83
|
+
Arguments:
|
|
84
|
+
dictionary {dict} -- a dictionary structure
|
|
85
|
+
key {str} -- the dotted (or not) key to set
|
|
86
|
+
value {object} -- the value to set at key
|
|
87
|
+
overwrite {bool} -- override the value if key exists in dictionary (True)
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
dictionary {dict} -- the edited dictionary
|
|
91
|
+
"""
|
|
92
|
+
if "*" in key:
|
|
93
|
+
raise ValueError("You cannot set values with wildcards *")
|
|
94
|
+
if not overwrite and data_get(dictionary, key):
|
|
95
|
+
return
|
|
96
|
+
data(dictionary)[key] = value
|
|
97
|
+
return dictionary
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Time related helpers"""
|
|
2
|
+
import pendulum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def cookie_expire_time(str_time):
|
|
6
|
+
"""Take a string like 1 month or 5 minutes and returns a datetime formatted with cookie format.
|
|
7
|
+
|
|
8
|
+
Arguments:
|
|
9
|
+
str_time {string} -- Could be values like 1 second or 3 minutes
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
str -- Cookie expiration time (Thu, 21 Oct 2021 07:28:00)
|
|
13
|
+
"""
|
|
14
|
+
instance = parse_human_time(str_time)
|
|
15
|
+
return instance.format("ddd, DD MMM YYYY HH:mm:ss")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_human_time(str_time):
|
|
19
|
+
"""Take a string like 1 month or 5 minutes and returns a pendulum instance.
|
|
20
|
+
|
|
21
|
+
Arguments:
|
|
22
|
+
str_time {string} -- Could be values like 1 second or 3 minutes
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
pendulum -- Returns Pendulum instance
|
|
26
|
+
"""
|
|
27
|
+
if str_time == "now":
|
|
28
|
+
return pendulum.now("GMT")
|
|
29
|
+
|
|
30
|
+
if str_time != "expired":
|
|
31
|
+
number = int(str_time.split(" ")[0])
|
|
32
|
+
length = str_time.split(" ")[1]
|
|
33
|
+
|
|
34
|
+
if length in ("second", "seconds"):
|
|
35
|
+
return pendulum.now("GMT").add(seconds=number)
|
|
36
|
+
elif length in ("minute", "minutes"):
|
|
37
|
+
return pendulum.now("GMT").add(minutes=number)
|
|
38
|
+
elif length in ("hour", "hours"):
|
|
39
|
+
return pendulum.now("GMT").add(hours=number)
|
|
40
|
+
elif length in ("day", "days"):
|
|
41
|
+
return pendulum.now("GMT").add(days=number)
|
|
42
|
+
elif length in ("week", "weeks"):
|
|
43
|
+
return pendulum.now("GMT").add(weeks=number)
|
|
44
|
+
elif length in ("month", "months"):
|
|
45
|
+
return pendulum.now("GMT").add(months=number)
|
|
46
|
+
elif length in ("year", "years"):
|
|
47
|
+
return pendulum.now("GMT").add(years=number)
|
|
48
|
+
|
|
49
|
+
return None
|
|
50
|
+
else:
|
|
51
|
+
return pendulum.now("GMT").subtract(years=20)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def migration_timestamp():
|
|
55
|
+
"""Return current time formatted for creating migration filenames.
|
|
56
|
+
Example: 2021_01_09_043202
|
|
57
|
+
"""
|
|
58
|
+
return pendulum.now().format("YYYY_MM_DD_HHmmss")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-startkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Core Masonite components (Container, Config, Loader)
|
|
5
|
+
Author: Bedram Tamang
|
|
6
|
+
Author-email: tmgbedu@gmail.com
|
|
7
|
+
Requires-Python: >=3.12,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Requires-Dist: dotty-dict (>=1.3.1)
|
|
13
|
+
Requires-Dist: fastapi[standard] (>=0.124.4,<0.125.0)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
fastapi_startkit/__init__.py,sha256=ObTUylrPyR4uTL4oz6CJfgs0Mk73TxEjPiVVDfn8dDQ,64
|
|
2
|
+
fastapi_startkit/application.py,sha256=WluaoQ7J18actW7N6y5Uf_7VR7oqj0WXXZG7KbvbRL8,1077
|
|
3
|
+
fastapi_startkit/configuration/Configuration.py,sha256=XpvbFVMQsiNTxWoT0mxDOC8IQONDvZpH-GcMwLdasH8,2772
|
|
4
|
+
fastapi_startkit/configuration/__init__.py,sha256=o9cbhMDID-lec6tqPHX8_2csC2eYZYP1Z5c3igzjeIQ,69
|
|
5
|
+
fastapi_startkit/configuration/helpers.py,sha256=yfoP_7xCFw_6dRLQu82GzdEZMRm8P79q76HJRDSc6PA,98
|
|
6
|
+
fastapi_startkit/configuration/providers/ConfigurationProvider.py,sha256=uW3tu7asNk1CzkfkrFHqb0rf1a-PwlZJs42yXujGQhI,372
|
|
7
|
+
fastapi_startkit/configuration/providers/__init__.py,sha256=CBjuHUetZl0Wd7Jgp1Qg5mFjRAiIq8e_socQQccXlUc,57
|
|
8
|
+
fastapi_startkit/container/__init__.py,sha256=zfF4wNDfI-_iFYISsQc_gUjoHKV6AaPlO4ljevEFAow,33
|
|
9
|
+
fastapi_startkit/container/container.py,sha256=t664orgo2zC4aCJhMOlVB41GUmoOJWOdGMtbQlfb9FM,16946
|
|
10
|
+
fastapi_startkit/environment/environment.py,sha256=HN4zxmTs9-gm-CwBa0sr418P7isPLnZTFH-j6iD1wgY,2634
|
|
11
|
+
fastapi_startkit/exceptions/DD.py,sha256=WgBlhCalfx4eY7RGPrx5NTsnF4s8Jr30cCVht5d_q2U,1080
|
|
12
|
+
fastapi_startkit/exceptions/ExceptionHandler.py,sha256=keGkPZY7BvCP1PMUpq8iC5WuzDtNC_FoHaFcYzIMzVw,2924
|
|
13
|
+
fastapi_startkit/exceptions/__init__.py,sha256=TBJJYH1tmIurA239fxCmBPCrHsyTP0ZaBmn63BpaoOs,1098
|
|
14
|
+
fastapi_startkit/exceptions/exceptionite/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
fastapi_startkit/exceptions/exceptionite/blocks.py,sha256=SDIIBAykKvKrjOl9beeT6z3eqJMoVI06n1yl4XMz8zI,2793
|
|
16
|
+
fastapi_startkit/exceptions/exceptionite/controllers.py,sha256=px_HUBpr9Ff09lEYq2wapbLncXzgS4P7i-z7w7UUQko,581
|
|
17
|
+
fastapi_startkit/exceptions/exceptionite/solutions.py,sha256=7PS192xcysA0w2UZBs6cvb6zn8ONNbvbR8Me-chqx8c,2352
|
|
18
|
+
fastapi_startkit/exceptions/exceptionite/tabs.py,sha256=iknhFIrK_51tveRURFS3wZ0_eT66R1mgGggsMPfteRI,445
|
|
19
|
+
fastapi_startkit/exceptions/exceptions.py,sha256=HNO_ud_eCqQoOMM0m4yQ1XWWwx5N1pkyeZT03091K2o,3569
|
|
20
|
+
fastapi_startkit/exceptions/handlers/DumpExceptionHandler.py,sha256=Dkzn9aX5vaSuBR0qvR5h8wQFVbUbLLquMkiud5Z7i0Y,3110
|
|
21
|
+
fastapi_startkit/exceptions/handlers/HttpExceptionHandler.py,sha256=b80QI4FgFN5QaTqx5Rz7PcC8H6FHy6k8J7FgqO-DarE,1084
|
|
22
|
+
fastapi_startkit/exceptions/handlers/ModelNotFoundHandler.py,sha256=MCJGkAa0_EJwU9WjJMVnbw-oLE_MAdMQ1A73LvObnz4,440
|
|
23
|
+
fastapi_startkit/facades/Auth.py,sha256=yLOZFMvG1HKbvd0OKkSVUNTXflWQC_o45IvcJDaVlIc,76
|
|
24
|
+
fastapi_startkit/facades/Auth.pyi,sha256=V3mv1CaW03hmLAa2hvxiB21dznVqlimImkXhxB1BshY,1142
|
|
25
|
+
fastapi_startkit/facades/Broadcast.py,sha256=LuAe-MJg7jTalJ3pSs1Q_FJae-TGZfQq7rrnEGmr0qQ,86
|
|
26
|
+
fastapi_startkit/facades/Cache.py,sha256=nCo_xCSHnBKz_pk8aiHGspjKypWrE6EGTQqXq62MWqI,78
|
|
27
|
+
fastapi_startkit/facades/Config.py,sha256=Q-2HlviEXRmzVSJ35S-lfr4b4_SOxitxtoL0Hn_1WHo,80
|
|
28
|
+
fastapi_startkit/facades/Config.pyi,sha256=1xIbzbhGrcLVjfDzSModminRoG_CjwPpdjMqIZU3FjQ,399
|
|
29
|
+
fastapi_startkit/facades/Dump.py,sha256=rnHsG1ceW4p4FT6jNPSIlXCSZiWrf_IBMXdwV-wOFqM,78
|
|
30
|
+
fastapi_startkit/facades/Dump.pyi,sha256=MdEIxM3UYlz5-DExdiN0cUu6kOwqvcz8Vkvgvmia92c,936
|
|
31
|
+
fastapi_startkit/facades/Facade.py,sha256=rSpVuH4ehojA9OjSv7q8rYPvQtVOFYN5fUDBSVXtDVQ,175
|
|
32
|
+
fastapi_startkit/facades/Gate.py,sha256=6cJZMHRH1z95As5-svvwTSFm9ZeEJfWKKnLkrA7muXM,76
|
|
33
|
+
fastapi_startkit/facades/Gate.pyi,sha256=ACKCDXW_x2eJ0IDHnlLL-QJAu8eQOJ2seph2Bv5IKgk,1382
|
|
34
|
+
fastapi_startkit/facades/Hash.py,sha256=kMOa5ydml2wMyfeCiRckDn9j0CqNxR6GtShvSVmpNjE,76
|
|
35
|
+
fastapi_startkit/facades/Hash.pyi,sha256=zfIzKSX-OXl-WFEXhAuDJTBitf-yIyS7AcQb8YBeXgU,1121
|
|
36
|
+
fastapi_startkit/facades/Loader.py,sha256=zGpoa0rfpQySGYG6kCkGlDIqYAeqKV9p0W_Up3qperQ,80
|
|
37
|
+
fastapi_startkit/facades/Loader.pyi,sha256=5va6whUqnFGRvtW13o-LCfLl5lJWvu7S3eWqqh2qoU4,1289
|
|
38
|
+
fastapi_startkit/facades/Mail.py,sha256=VePnQMH3tCC-C6x2MzrAmJ9yuKmAq6jf0HExoC9ALN4,76
|
|
39
|
+
fastapi_startkit/facades/Mail.pyi,sha256=HLjPw5cdnQnTxrEr2beEyfjNynUXAI7jia_V-9PX1nU,430
|
|
40
|
+
fastapi_startkit/facades/Notification.py,sha256=GgnFpoP_ZVqxQbLMYGqROlKBKSelGMpNh4HSQ1KlgqE,92
|
|
41
|
+
fastapi_startkit/facades/Notification.pyi,sha256=YCKT3oV6jdUDIQvvdJdCfZWQQLwzUSgbrnA6KjgeMjk,902
|
|
42
|
+
fastapi_startkit/facades/Queue.py,sha256=zCARcIhbRSPOASPjNl3Y-sT30ZomnpKO65nxhcK4hlc,78
|
|
43
|
+
fastapi_startkit/facades/Queue.pyi,sha256=5j6KhiO04h9yxobzo27CbZuvX3OU7Bq6vllnjMUirFc,364
|
|
44
|
+
fastapi_startkit/facades/RateLimiter.py,sha256=-85JDDz1aBQ3tzUvMNQvSTJ91sASGJM8bXmO9KpYsd0,83
|
|
45
|
+
fastapi_startkit/facades/RateLimiter.pyi,sha256=65KACncDSR-13NTxMtJwmFAJ7qMZ9T8eBUaSNm2UUcY,1770
|
|
46
|
+
fastapi_startkit/facades/Request.py,sha256=dW1LMWAhCwoqGhOiLBgXqAhZHzd868neqcBcabZsE30,82
|
|
47
|
+
fastapi_startkit/facades/Request.pyi,sha256=ecQ1K49JAtcHbc5kp0LhORzOfu1KXkpfxPiCu3TCEPM,3472
|
|
48
|
+
fastapi_startkit/facades/Response.py,sha256=WeFinS3pADucCOCrbUQpmGIs5lTH0Yiq_5UnWq4YAVU,84
|
|
49
|
+
fastapi_startkit/facades/Response.pyi,sha256=ROUaSHWzyVjdTa6-CZ8vJc_zKDxdl9t2O-U9LmirUm8,2630
|
|
50
|
+
fastapi_startkit/facades/Session.py,sha256=2A4bEOJLeuctUhol0xawQWffvr_71B0AbyA-j6430ls,82
|
|
51
|
+
fastapi_startkit/facades/Session.pyi,sha256=3oLikb3epJB775_dhIuqt7Vrqa7yIAYLTujKcqAzYnE,2056
|
|
52
|
+
fastapi_startkit/facades/Storage.py,sha256=iMdvlfA5fPT6lJhBYdlzuisHGM8E26nWpqm4RhB1GU8,82
|
|
53
|
+
fastapi_startkit/facades/Storage.pyi,sha256=3U_hPjHI9dvf1CG0NbgvnepolzUkhJwaqDEpv3ep8hQ,409
|
|
54
|
+
fastapi_startkit/facades/Url.py,sha256=j_y_DdLw08xPFt9Fn09V2H9SAKRDyR9s43QWxAR51sg,74
|
|
55
|
+
fastapi_startkit/facades/Url.pyi,sha256=QKgjH6wfqEEdsyLO2mMHUhOBhWcC9kmaOSr0PKnQ5jg,960
|
|
56
|
+
fastapi_startkit/facades/View.py,sha256=J9gB1VTByvfzSFFB9H7Ppvg7Tej_9uo1VHJHQa70dUs,76
|
|
57
|
+
fastapi_startkit/facades/View.pyi,sha256=Pp9ml6h9hoNnEFJjtxW0w2w_Nsc9TQ5DGK1XEO29Gis,2158
|
|
58
|
+
fastapi_startkit/facades/__init__.py,sha256=yj_dPGoCipuFRrxW-lgl_NhnzdN4ZdS9PN2yxW4htm0,517
|
|
59
|
+
fastapi_startkit/loader/Loader.py,sha256=5rYY3oURkNSiyfypfjGiJg-8HagflPQVHXoVayJWmhU,3131
|
|
60
|
+
fastapi_startkit/loader/__init__.py,sha256=GWjMnz8OzbJJL2x_DaoaeNzZ-ESYQNeyLnYLAl4psxE,27
|
|
61
|
+
fastapi_startkit/providers/ConfigurationProvider.py,sha256=MSt-AV28skbNY_iESL8r8zQbpufdlyriHrL95wUFbDo,422
|
|
62
|
+
fastapi_startkit/providers/Provider.py,sha256=o1pVTzEl516Kx0qe30K67phpUR6ygDdGHRKklyI3-iI,279
|
|
63
|
+
fastapi_startkit/providers/__init__.py,sha256=j78PJlEuaeaEO4xCsfNDPrWnHqOnwc2plbHMzKJG7_I,137
|
|
64
|
+
fastapi_startkit/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
65
|
+
fastapi_startkit/utils/collections.py,sha256=QBXCcNpoowY9JSrccDZ6YArxAhZHUxBxA7WxguVA-rM,14697
|
|
66
|
+
fastapi_startkit/utils/console.py,sha256=KeFNVeFWmi_OaRVOC1PTbuBM5kWmv_twmDXWrK0lYtU,1054
|
|
67
|
+
fastapi_startkit/utils/data/mime.types,sha256=YxY5l5GdijuD3RsffyPpEkzV-GYJmMN02kLGQbiGX1k,61134
|
|
68
|
+
fastapi_startkit/utils/filesystem.py,sha256=UnEFo8r4gR09OcriVaGh5cDScC5jpxAlg0rnrZ-3BNE,3165
|
|
69
|
+
fastapi_startkit/utils/http.py,sha256=AKfDx-8aY_YQh-ERHN97bpG-qUdwzN7gAsQphfGjEU0,3323
|
|
70
|
+
fastapi_startkit/utils/location.py,sha256=xh6KWfPCYrhkZEr0s6zrn-3NgyOO0Tf1HOUdiZDMTVU,3817
|
|
71
|
+
fastapi_startkit/utils/str.py,sha256=8_WsHSaOKkWA4GuQUaiOzROYihGkIdXZs_txYi79ro0,3958
|
|
72
|
+
fastapi_startkit/utils/structures.py,sha256=VwRIcsNR1PhdUBtSzmwqxgXvBedKHOHV6T901_ZKOfY,3142
|
|
73
|
+
fastapi_startkit/utils/time.py,sha256=ItbzAowY5MyTBQX6bMvgDh1WciR61ldTYyUPmKq1SSs,1882
|
|
74
|
+
fastapi_startkit-0.1.0.dist-info/METADATA,sha256=kKNsod_LvvMmvQR_e3rsxa0Cu1xZhiLBNSWejoYPiU0,496
|
|
75
|
+
fastapi_startkit-0.1.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
76
|
+
fastapi_startkit-0.1.0.dist-info/RECORD,,
|