Compare commits

...

10 Commits

Author SHA1 Message Date
886458fb29 Update CLI to point to prod
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
2024-10-05 17:37:26 +01:00
b310357fa8 Fix password updating 2024-10-05 17:36:43 +01:00
6769fdd435 Add Sentry to API 2024-10-05 17:01:54 +01:00
cc4d3fffcd Add linter for API 2024-10-05 16:50:58 +01:00
fcec32baea Add webargs for API 2024-10-05 16:37:56 +01:00
509201aa37 Updated Python deps 2024-10-05 15:06:31 +01:00
57014c8b44 Add linter 2024-10-05 14:31:08 +01:00
6af38a12f8 Update yarn/build system 2024-10-05 14:28:55 +01:00
2c734135eb updated components and styling 2024-10-05 14:26:37 +01:00
a015ef776d updated web dependencies 2024-10-05 13:55:25 +01:00
185 changed files with 3330 additions and 3055 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.sw*
.DS_Store

View File

@ -1,4 +1,4 @@
pipeline: steps:
buildweb: buildweb:
group: build group: build
image: node image: node
@ -33,4 +33,5 @@ pipeline:
- s3cmd -c /root/.s3cfg sync --no-mime-magic --guess-mime-type dist/* s3://dotty.cloud - s3cmd -c /root/.s3cfg sync --no-mime-magic --guess-mime-type dist/* s3://dotty.cloud
- 'curl -X POST -H "AccessKey: $BUNNY_KEY" https://api.bunny.net/pullzone/782735/purgeCache' - 'curl -X POST -H "AccessKey: $BUNNY_KEY" https://api.bunny.net/pullzone/782735/purgeCache'
branches: main when:
branch: main

View File

@ -1,78 +1,109 @@
import os, datetime, hashlib import os
import datetime
import hashlib
from flask import jsonify from flask import jsonify
from bson.objectid import ObjectId from bson.objectid import ObjectId
import jwt, bcrypt import jwt
import bcrypt
from util import database from util import database
jwt_secret = os.environ.get('JWT_SECRET') jwt_secret = os.environ.get("JWT_SECRET")
def get_user_context(request, token): def get_user_context(request, token):
if not token: return None if not token:
try: return None
id = jwt.decode(token, jwt_secret, algorithms=['HS256'])['sub'] try:
if id: id = jwt.decode(token, jwt_secret, algorithms=["HS256"])["sub"]
db = database.get_db() if id:
user = db.users.find_one({'_id': ObjectId(id), 'keys': token}) db = database.get_db()
if user: user['currentToken'] = token user = db.users.find_one({"_id": ObjectId(id), "keys": token})
return user if user:
except Exception as e: user["currentToken"] = token
return None return user
except Exception:
return None
def generate_api_token(user_id): def generate_api_token(user_id):
payload = { payload = {"iat": datetime.datetime.utcnow(), "sub": str(user_id)}
'iat': datetime.datetime.utcnow(), return jwt.encode(payload, jwt_secret, algorithm="HS256")
'sub': str(user_id)
}
return jwt.encode(payload, jwt_secret, algorithm='HS256')
def create(data): def create(data):
if not data: if not data:
return jsonify({'success': False, 'message': 'Invalid request'}), 400 return jsonify({"success": False, "message": "Invalid request"}), 400
email = data.get('email', '').lower().strip() email = data.get("email", "").lower().strip()
password = data.get('password', '') password = data.get("password", "")
db = database.get_db() db = database.get_db()
existing = db.users.find_one({'email': email}) existing = db.users.find_one({"email": email})
if not email or not password: if not email or not password:
return jsonify({'success': False, 'message': 'Invalid request'}), 400 return jsonify({"success": False, "message": "Invalid request"}), 400
if existing: if existing:
return jsonify({'success': False, 'message': 'An account with that email address already exists'}), 400 return jsonify(
hashed_password = bcrypt.hashpw(hashlib.sha256(password.encode('utf-8')).hexdigest().encode('utf-8'), bcrypt.gensalt()) {
user_id = ObjectId() "success": False,
token = generate_api_token(user_id) "message": "An account with that email address already exists",
db.users.insert_one({'_id': user_id, 'email': email, 'password': hashed_password.decode('utf-8'), 'createdAt': datetime.datetime.utcnow(), 'keys': [token]}) }
return token ), 400
hashed_password = bcrypt.hashpw(
hashlib.sha256(password.encode("utf-8")).hexdigest().encode("utf-8"),
bcrypt.gensalt(),
)
user_id = ObjectId()
token = generate_api_token(user_id)
db.users.insert_one(
{
"_id": user_id,
"email": email,
"password": hashed_password.decode("utf-8"),
"createdAt": datetime.datetime.utcnow(),
"keys": [token],
}
)
return token
def login(data): def login(data):
if not data: if not data:
return jsonify({'success': False, 'message': 'Invalid request'}), 400 return jsonify({"success": False, "message": "Invalid request"}), 400
email = data.get('email', '').lower().strip() email = data.get("email", "").lower().strip()
password = data.get('password', '') password = data.get("password", "")
if not email or not password: if not email or not password:
return jsonify({'success': False, 'message': 'Invalid request'}), 400 return jsonify({"success": False, "message": "Invalid request"}), 400
db = database.get_db() db = database.get_db()
existing = db.users.find_one({'email': email}) existing = db.users.find_one({"email": email})
if not existing: if not existing:
return jsonify({'success': False, 'message': 'Email address or password incorrect'}), 400 return jsonify(
{"success": False, "message": "Email address or password incorrect"}
), 400
incoming_pw = hashlib.sha256(data["password"].encode("utf-8")).hexdigest()
if bcrypt.checkpw(
incoming_pw.encode("utf-8"), existing["password"].encode("utf-8")
):
token = generate_api_token(existing["_id"])
db.users.update_one({"_id": existing["_id"]}, {"$addToSet": {"keys": token}})
return token
return jsonify(
{"success": False, "message": "Email address or password incorrect"}
), 400
incoming_pw = hashlib.sha256(data['password'].encode('utf-8')).hexdigest()
if bcrypt.checkpw(incoming_pw.encode('utf-8'), existing['password'].encode('utf-8')):
token = generate_api_token(existing['_id'])
db.users.update_one({'_id': existing['_id']}, {'$addToSet': {'keys': token}})
return token
return jsonify({'success': False, 'message': 'Email address or password incorrect'}), 400
def logout(user): def logout(user):
db = database.get_db() db = database.get_db()
db.users.update_one({'_id': user['_id']}, {'$pull': {'keys': user['currentToken']}}) db.users.update_one({"_id": user["_id"]}, {"$pull": {"keys": user["currentToken"]}})
return jsonify({'success': True}), 200 return jsonify({"success": True}), 200
def logout_others(user): def logout_others(user):
db = database.get_db() db = database.get_db()
keys = filter(lambda k: k != user['currentToken'], user.get('keys', [])) keys = filter(lambda k: k != user["currentToken"], user.get("keys", []))
db.users.update_one({'_id': user['_id']}, {'$set': {'keys': list(keys)}}) db.users.update_one({"_id": user["_id"]}, {"$set": {"keys": list(keys)}})
return jsonify({'success': True}), 200 return jsonify({"success": True}), 200
def logout_all(user): def logout_all(user):
db = database.get_db() db = database.get_db()
db.users.update_one({'_id': user['_id']}, {'$set': {'keys': []}}) db.users.update_one({"_id": user["_id"]}, {"$set": {"keys": []}})
return jsonify({'success': True}), 200 return jsonify({"success": True}), 200

View File

@ -1,64 +1,123 @@
import sentry_sdk
from flask import Flask, jsonify, request, g from flask import Flask, jsonify, request, g
from webargs import fields, validate
from webargs.flaskparser import use_args
from util import util from util import util
import files, accounts, passwords import files
import accounts
import passwords
sentry_sdk.init(
dsn="https://6a7be9c67251f0b715d7d477c5ae2cef@o4508066290532352.ingest.de.sentry.io/4508070957875280",
traces_sample_rate=1.0,
profiles_sample_rate=1.0,
)
app = Flask(__name__) app = Flask(__name__)
# Error handlers
@app.errorhandler(404) @app.errorhandler(404)
def handle_404(e): def handle_404(e):
return jsonify({'message': 'Resource not found'}), 404 return jsonify({"message": "Resource not found"}), 404
@app.errorhandler(422)
def handle_unprocessable_entity(e):
return jsonify(
{
"validations": e.data.get("messages"),
}
), 422
@app.errorhandler(429) @app.errorhandler(429)
def handle_429(e): def handle_429(e):
return jsonify({'message': 'Rate limit exceeded', 'Allowed limit': e.description}), 429 return jsonify(
{"message": "Rate limit exceeded", "Allowed limit": e.description}
), 429
@app.errorhandler(500) @app.errorhandler(500)
def handle_500(): def handle_500():
return jsonify({'message': 'There was a problem with this request. Please try again later.'}), 500 return jsonify(
{"message": "There was a problem with this request. Please try again later."}
), 500
@app.route('/accounts', methods=['POST'])
def accounts_route():
if request.method == 'POST':
return accounts.create(request.get_json())
@app.route('/accounts/password/tokens', methods=['POST', 'PUT']) # Route handlers
@app.route("/accounts", methods=["POST"])
@use_args(
{
"email": fields.Email(required=True),
"password": fields.Str(required=True, validate=validate.Length(min=8)),
}
)
def accounts_route(args):
return accounts.create(args)
@app.route("/accounts/password/tokens", methods=["POST"])
def password_tokens_route(): def password_tokens_route():
if request.method == 'POST':
return passwords.create_reset_token() return passwords.create_reset_token()
if request.method == 'PUT':
return passwords.update(request.get_json())
@app.route('/tokens', methods=['POST'])
def tokens_post_route():
if request.method == 'POST':
return accounts.login(request.get_json())
@app.route('/tokens', methods=['DELETE']) @app.route("/accounts/password/tokens", methods=["PUT"])
@use_args({"password": fields.Str(), "token": fields.Str()})
def password_tokens_route_put(args):
return passwords.update(args)
@app.route("/accounts/password", methods=["PUT"])
@use_args(
{
"currentPassword": fields.Str(required=True),
"password": fields.Str(required=True, validate=validate.Length(min=8)),
}
)
@util.auth
def password_route_put(args):
return passwords.update(args, user=g.user)
@app.route("/tokens", methods=["POST"])
@use_args({"email": fields.Email(required=True), "password": fields.Str(required=True)})
def tokens_post_route(args):
return accounts.login(args)
@app.route("/tokens", methods=["DELETE"])
@util.auth @util.auth
def tokens_delete_route(): def tokens_delete_route():
if request.method == 'DELETE': if request.method == "DELETE":
return accounts.logout(g.user) return accounts.logout(g.user)
@app.route('/tokens/all', methods=['DELETE'])
@app.route("/tokens/all", methods=["DELETE"])
@util.auth @util.auth
def tokens_all_delete_route(): def tokens_all_delete_route():
if request.method == 'DELETE': if request.method == "DELETE":
return accounts.logout_all(g.user) return accounts.logout_all(g.user)
@app.route('/files', methods=['POST', 'GET'])
@app.route("/files", methods=["POST", "GET"])
@util.auth @util.auth
def files_route(): def files_route():
if request.method == 'POST': if request.method == "POST":
return files.create(g.user) return files.create(g.user)
if request.method == 'GET': if request.method == "GET":
return files.get(g.user) return files.get(g.user)
@app.route('/files/<key>', methods=['PUT', 'GET', 'DELETE'])
@app.route("/files/<key>", methods=["PUT", "GET", "DELETE"])
@util.auth @util.auth
def file_route(key): def file_route(key):
if request.method == 'PUT': if request.method == "PUT":
return files.update_file(g.user, key) return files.update_file(g.user, key)
if request.method == 'GET': if request.method == "GET":
return files.get_file(g.user, key) return files.get_file(g.user, key)
if request.method == 'DELETE': if request.method == "DELETE":
return files.delete_file(g.user, key) return files.delete_file(g.user, key)

View File

@ -4,3 +4,4 @@ export FLASK_RUN_PORT="9500"
export MONGO_URL="mongodb://localhost" export MONGO_URL="mongodb://localhost"
export MONGO_DATABASE="dotty" export MONGO_DATABASE="dotty"
export JWT_SECRET="devsecret" export JWT_SECRET="devsecret"
export JWT_PASSWORD_SECRET="devsecret"

View File

@ -1,57 +1,85 @@
import os, datetime import datetime
from flask import request, jsonify, Response from flask import request, jsonify, Response
from util import database from util import database
max_size = 512 * 1024 max_size = 512 * 1024
def create(user): def create(user):
default_path = request.headers.get('Default-Path') default_path = request.headers.get("Default-Path")
key = request.headers.get('Key') key = request.headers.get("Key")
db = database.get_db() db = database.get_db()
existing = db.configs.find_one({'key': key, 'user': str(user['_id'])}) existing = db.configs.find_one({"key": key, "user": str(user["_id"])})
if existing: if existing:
return jsonify({'success': False, 'message': 'A key with this name already exists'}), 400 return jsonify(
data = request.get_data() {"success": False, "message": "A key with this name already exists"}
if len(data) > max_size: ), 400
return jsonify({'success': False, 'message': 'File is too large'}), 400 data = request.get_data()
if not key or not default_path or not data: if len(data) > max_size:
return jsonify({'success': False, 'message': 'Invalid request'}), 400 return jsonify({"success": False, "message": "File is too large"}), 400
now = datetime.datetime.utcnow() if not key or not default_path or not data:
db.configs.insert_one({'key': key, 'defaultPath': default_path, 'createdAt': now, 'updatedAt': now, 'object': data, 'user': str(user['_id'])}) return jsonify({"success": False, "message": "Invalid request"}), 400
return jsonify({'success': True}), 200 now = datetime.datetime.utcnow()
db.configs.insert_one(
{
"key": key,
"defaultPath": default_path,
"createdAt": now,
"updatedAt": now,
"object": data,
"user": str(user["_id"]),
}
)
return jsonify({"success": True}), 200
def get(user): def get(user):
db = database.get_db() db = database.get_db()
files_list = list(db.configs.find({'user': str(user['_id'])}, {'_id': 0, 'key': 1, 'defaultPath': 1})) files_list = list(
return jsonify(files_list), 200 db.configs.find(
{"user": str(user["_id"])}, {"_id": 0, "key": 1, "defaultPath": 1}
)
)
return jsonify(files_list), 200
def update_file(user, key): def update_file(user, key):
db = database.get_db() db = database.get_db()
existing = db.configs.find_one({'key': key, 'user': str(user['_id'])}) existing = db.configs.find_one({"key": key, "user": str(user["_id"])})
if not existing: if not existing:
return jsonify({'success': False, 'message': 'A file with this key could not be found'}), 404 return jsonify(
data = request.get_data() {"success": False, "message": "A file with this key could not be found"}
if len(data) > max_size: ), 404
return jsonify({'success': False, 'message': 'File is too large'}), 400 data = request.get_data()
if not key or not data: if len(data) > max_size:
return jsonify({'success': False, 'message': 'Invalid request'}), 400 return jsonify({"success": False, "message": "File is too large"}), 400
now = datetime.datetime.utcnow() if not key or not data:
db.configs.update_one({'_id': existing['_id']}, {'$set': {'updatedAt': now, 'object': data}}) return jsonify({"success": False, "message": "Invalid request"}), 400
return jsonify({'success': True}), 200 now = datetime.datetime.utcnow()
db.configs.update_one(
{"_id": existing["_id"]}, {"$set": {"updatedAt": now, "object": data}}
)
return jsonify({"success": True}), 200
def get_file(user, key): def get_file(user, key):
db = database.get_db() db = database.get_db()
existing = db.configs.find_one({'key': key, 'user': str(user['_id'])}) existing = db.configs.find_one({"key": key, "user": str(user["_id"])})
if not existing: if not existing:
return jsonify({'success': False, 'message': 'The specified file could not be found'}), 404 return jsonify(
resp = Response(existing['object']) {"success": False, "message": "The specified file could not be found"}
resp.headers['Default-Path'] = existing['defaultPath'] ), 404
return resp resp = Response(existing["object"])
resp.headers["Default-Path"] = existing["defaultPath"]
return resp
def delete_file(user, key): def delete_file(user, key):
db = database.get_db() db = database.get_db()
existing = db.configs.find_one({'key': key, 'user': str(user['_id'])}) existing = db.configs.find_one({"key": key, "user": str(user["_id"])})
if not existing: if not existing:
return jsonify({'success': False, 'message': 'The specified file could not be found'}), 404 return jsonify(
db.configs.delete_one({'key': key, 'user': str(user['_id'])}) {"success": False, "message": "The specified file could not be found"}
return jsonify({'success': True}), 200 ), 404
db.configs.delete_one({"key": key, "user": str(user["_id"])})
return jsonify({"success": True}), 200

4
api/lint.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
ruff format .
ruff check --fix .

View File

@ -1,49 +1,99 @@
import os, datetime, hashlib import os
import jwt, bcrypt import datetime
import hashlib
import jwt
import bcrypt
from bson.objectid import ObjectId from bson.objectid import ObjectId
from flask import request, jsonify, Response from flask import request, jsonify
from util import database, mail from util import database, mail
secret = os.environ.get('JWT_PASSWORD_SECRET') secret = os.environ.get("JWT_PASSWORD_SECRET")
def create_reset_token(): def create_reset_token():
data = request.get_data() data = request.get_data()
if not data: return jsonify({'success': False, 'message': 'Invalid request'}), 400 if not data:
db = database.get_db() return jsonify({"success": False, "message": "Invalid request"}), 400
user = db.users.find_one({'email': data.decode('ascii')}) db = database.get_db()
if not user: return jsonify({'success': True}) user = db.users.find_one({"email": data.decode("ascii")})
now = datetime.datetime.utcnow() if not user:
payload = { return jsonify({"success": True})
'iat': now, now = datetime.datetime.utcnow()
'exp': now + datetime.timedelta(hours = 3), payload = {
'sub': str(user['_id']) "iat": now,
} "exp": now + datetime.timedelta(hours=3),
token = jwt.encode(payload, secret, algorithm='HS256').decode("utf-8") "sub": str(user["_id"]),
db.users.update_one({'_id': user['_id']}, {'$set': {'passwordResetToken': token}}) }
try: token = jwt.encode(payload, secret, algorithm="HS256")
mail.send(user['email'], 'Password reset', f'Hi there,\n\nTo reset your password please use the following token when prompted by the dotty client:\n\n{token}\n\nThanks,\n\nDotty', 'Dotty Accounts <accounts@mail.dotty.cloud>') db.users.update_one({"_id": user["_id"]}, {"$set": {"passwordResetToken": token}})
return jsonify({'success': True}) try:
except Exception as e: mail.send(
print(e) user["email"],
return jsonify({'success': False, 'message': 'We were unable to send a password-reset email. Please try again later.'}), 500 "Password reset",
f"Hi there,\n\nTo reset your password please use the following token when prompted by the dotty client:\n\n{token}\n\nThanks,\n\nDotty",
"Dotty Accounts <accounts@mail.dotty.cloud>",
)
return jsonify({"success": True})
except Exception:
return jsonify(
{
"success": False,
"message": "We were unable to send a password-reset email. Please try again later.",
}
), 500
def update(data):
print(data)
if not data:
return jsonify({'success': False, 'message': 'Invalid request'}), 400
password = data.get('password')
token = data.get('token')
if not password or not token: return jsonify({'success': False, 'message': 'Invalid request'}), 400
db = database.get_db() def update(data, user=None):
user = None if not data:
try: return jsonify({"success": False, "message": "Invalid request"}), 400
user_id = jwt.decode(token, secret)['sub']
user = db.users.find_one({'_id': ObjectId(user_id)}) db = database.get_db()
if not user: raise Exception current_password = data.get("currentPassword")
except Exception as e: password = data.get("password")
print(e) token = data.get("token")
return jsonify({'success': False, 'message': 'Unable to update password. Your token may be invalid'}), 400 if not password:
hashed_password = bcrypt.hashpw(hashlib.sha256(password.encode('utf-8')).hexdigest().encode('utf-8'), bcrypt.gensalt()) return jsonify(
db.users.update_one({'_id': user['_id']}, {'$set': {'password': hashed_password.decode('utf-8')}}) {"success": False, "message": "You must supply a new password"}
return jsonify({'success': True}) ), 400
if token:
try:
user_id = jwt.decode(token, secret, algorithms="HS256")["sub"]
user = db.users.find_one({"_id": ObjectId(user_id)})
if not user:
raise Exception
except Exception:
return jsonify(
{
"success": False,
"message": "Unable to update password. Your token may be invalid",
}
), 400
elif user and current_password:
if not bcrypt.checkpw(
hashlib.sha256(current_password.encode("utf-8"))
.hexdigest()
.encode("utf-8"),
user["password"].encode("utf-8"),
):
return jsonify(
{
"success": False,
"message": "Unable to update password. Your current password is incorrect",
}
), 400
else:
return jsonify(
{
"success": False,
"message": "You must supply a token or current password.",
}
), 400
hashed_password = bcrypt.hashpw(
hashlib.sha256(password.encode("utf-8")).hexdigest().encode("utf-8"),
bcrypt.gensalt(),
)
db.users.update_one(
{"_id": user["_id"]}, {"$set": {"password": hashed_password.decode("utf-8")}}
)
return jsonify({"success": True})

422
api/poetry.lock generated
View File

@ -1,33 +1,39 @@
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]] [[package]]
name = "bcrypt" name = "bcrypt"
version = "4.0.1" version = "4.2.0"
description = "Modern password hashing for your software and your servers" description = "Modern password hashing for your software and your servers"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.7"
files = [ files = [
{file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, {file = "bcrypt-4.2.0-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399"},
{file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060"},
{file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7"},
{file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, {file = "bcrypt-4.2.0-cp37-abi3-win32.whl", hash = "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458"},
{file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, {file = "bcrypt-4.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5"},
{file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, {file = "bcrypt-4.2.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841"},
{file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68"},
{file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe"},
{file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2"},
{file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c"},
{file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae"},
{file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d"},
{file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e"},
{file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8"},
{file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, {file = "bcrypt-4.2.0-cp39-abi3-win32.whl", hash = "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34"},
{file = "bcrypt-4.2.0-cp39-abi3-win_amd64.whl", hash = "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9"},
{file = "bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:39e1d30c7233cfc54f5c3f2c825156fe044efdd3e0b9d309512cc514a263ec2a"},
{file = "bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db"},
{file = "bcrypt-4.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1ff39b78a52cf03fdf902635e4c81e544714861ba3f0efc56558979dd4f09170"},
{file = "bcrypt-4.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:373db9abe198e8e2c70d12b479464e0d5092cc122b20ec504097b5f2297ed184"},
{file = "bcrypt-4.2.0.tar.gz", hash = "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221"},
] ]
[package.extras] [package.extras]
@ -182,38 +188,38 @@ files = [
[[package]] [[package]]
name = "dnspython" name = "dnspython"
version = "2.4.2" version = "2.6.1"
description = "DNS toolkit" description = "DNS toolkit"
optional = false optional = false
python-versions = ">=3.8,<4.0" python-versions = ">=3.8"
files = [ files = [
{file = "dnspython-2.4.2-py3-none-any.whl", hash = "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8"}, {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"},
{file = "dnspython-2.4.2.tar.gz", hash = "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984"}, {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"},
] ]
[package.extras] [package.extras]
dnssec = ["cryptography (>=2.6,<42.0)"] dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"]
doh = ["h2 (>=4.1.0)", "httpcore (>=0.17.3)", "httpx (>=0.24.1)"] dnssec = ["cryptography (>=41)"]
doq = ["aioquic (>=0.9.20)"] doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"]
idna = ["idna (>=2.1,<4.0)"] doq = ["aioquic (>=0.9.25)"]
trio = ["trio (>=0.14,<0.23)"] idna = ["idna (>=3.6)"]
wmi = ["wmi (>=1.5.1,<2.0.0)"] trio = ["trio (>=0.23)"]
wmi = ["wmi (>=1.5.1)"]
[[package]] [[package]]
name = "flask" name = "flask"
version = "3.0.0" version = "3.0.3"
description = "A simple framework for building complex web applications." description = "A simple framework for building complex web applications."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "flask-3.0.0-py3-none-any.whl", hash = "sha256:21128f47e4e3b9d597a3e8521a329bf56909b690fcc3fa3e477725aa81367638"}, {file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"},
{file = "flask-3.0.0.tar.gz", hash = "sha256:cfadcdb638b609361d29ec22360d6070a77d7463dcb3ab08d2c2f2f168845f58"}, {file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"},
] ]
[package.dependencies] [package.dependencies]
blinker = ">=1.6.2" blinker = ">=1.6.2"
click = ">=8.1.3" click = ">=8.1.3"
importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""}
itsdangerous = ">=2.1.2" itsdangerous = ">=2.1.2"
Jinja2 = ">=3.1.2" Jinja2 = ">=3.1.2"
Werkzeug = ">=3.0.0" Werkzeug = ">=3.0.0"
@ -224,22 +230,23 @@ dotenv = ["python-dotenv"]
[[package]] [[package]]
name = "gunicorn" name = "gunicorn"
version = "21.2.0" version = "23.0.0"
description = "WSGI HTTP Server for UNIX" description = "WSGI HTTP Server for UNIX"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.7"
files = [ files = [
{file = "gunicorn-21.2.0-py3-none-any.whl", hash = "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0"}, {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"},
{file = "gunicorn-21.2.0.tar.gz", hash = "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"}, {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"},
] ]
[package.dependencies] [package.dependencies]
packaging = "*" packaging = "*"
[package.extras] [package.extras]
eventlet = ["eventlet (>=0.24.1)"] eventlet = ["eventlet (>=0.24.1,!=0.36.0)"]
gevent = ["gevent (>=1.4.0)"] gevent = ["gevent (>=1.4.0)"]
setproctitle = ["setproctitle"] setproctitle = ["setproctitle"]
testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"]
tornado = ["tornado (>=0.2)"] tornado = ["tornado (>=0.2)"]
[[package]] [[package]]
@ -253,25 +260,6 @@ files = [
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
] ]
[[package]]
name = "importlib-metadata"
version = "6.8.0"
description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"},
{file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"},
]
[package.dependencies]
zipp = ">=0.5"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
perf = ["ipython"]
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
[[package]] [[package]]
name = "itsdangerous" name = "itsdangerous"
version = "2.1.2" version = "2.1.2"
@ -359,6 +347,25 @@ files = [
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
] ]
[[package]]
name = "marshmallow"
version = "3.22.0"
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
optional = false
python-versions = ">=3.8"
files = [
{file = "marshmallow-3.22.0-py3-none-any.whl", hash = "sha256:71a2dce49ef901c3f97ed296ae5051135fd3febd2bf43afe0ae9a82143a494d9"},
{file = "marshmallow-3.22.0.tar.gz", hash = "sha256:4972f529104a220bb8637d595aa4c9762afbe7f7a77d82dc58c1615d70c5823e"},
]
[package.dependencies]
packaging = ">=17.0"
[package.extras]
dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"]
docs = ["alabaster (==1.0.0)", "autodocsumm (==0.2.13)", "sphinx (==8.0.2)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"]
tests = ["pytest", "pytz", "simplejson"]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "23.2" version = "23.2"
@ -372,131 +379,111 @@ files = [
[[package]] [[package]]
name = "pyjwt" name = "pyjwt"
version = "2.8.0" version = "2.9.0"
description = "JSON Web Token implementation in Python" description = "JSON Web Token implementation in Python"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"},
{file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"},
] ]
[package.extras] [package.extras]
crypto = ["cryptography (>=3.4.0)"] crypto = ["cryptography (>=3.4.0)"]
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
[[package]] [[package]]
name = "pymongo" name = "pymongo"
version = "4.5.0" version = "4.10.1"
description = "Python driver for MongoDB <http://www.mongodb.org>" description = "Python driver for MongoDB <http://www.mongodb.org>"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "pymongo-4.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2d4fa1b01fa7e5b7bb8d312e3542e211b320eb7a4e3d8dc884327039d93cb9e0"}, {file = "pymongo-4.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e699aa68c4a7dea2ab5a27067f7d3e08555f8d2c0dc6a0c8c60cfd9ff2e6a4b1"},
{file = "pymongo-4.5.0-cp310-cp310-manylinux1_i686.whl", hash = "sha256:dfcd2b9f510411de615ccedd47462dae80e82fdc09fe9ab0f0f32f11cf57eeb5"}, {file = "pymongo-4.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:70645abc714f06b4ad6b72d5bf73792eaad14e3a2cfe29c62a9c81ada69d9e4b"},
{file = "pymongo-4.5.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:3e33064f1984db412b34d51496f4ea785a9cff621c67de58e09fb28da6468a52"}, {file = "pymongo-4.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae2fd94c9fe048c94838badcc6e992d033cb9473eb31e5710b3707cba5e8aee2"},
{file = "pymongo-4.5.0-cp310-cp310-manylinux2014_i686.whl", hash = "sha256:33faa786cc907de63f745f587e9879429b46033d7d97a7b84b37f4f8f47b9b32"}, {file = "pymongo-4.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ded27a4a5374dae03a92e084a60cdbcecd595306555bda553b833baf3fc4868"},
{file = "pymongo-4.5.0-cp310-cp310-manylinux2014_ppc64le.whl", hash = "sha256:76a262c41c1a7cbb84a3b11976578a7eb8e788c4b7bfbd15c005fb6ca88e6e50"}, {file = "pymongo-4.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ecc2455e3974a6c429687b395a0bc59636f2d6aedf5785098cf4e1f180f1c71"},
{file = "pymongo-4.5.0-cp310-cp310-manylinux2014_s390x.whl", hash = "sha256:0f4b125b46fe377984fbaecf2af40ed48b05a4b7676a2ff98999f2016d66b3ec"}, {file = "pymongo-4.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920fee41f7d0259f5f72c1f1eb331bc26ffbdc952846f9bd8c3b119013bb52c"},
{file = "pymongo-4.5.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:40d5f6e853ece9bfc01e9129b228df446f49316a4252bb1fbfae5c3c9dedebad"}, {file = "pymongo-4.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0a15665b2d6cf364f4cd114d62452ce01d71abfbd9c564ba8c74dcd7bbd6822"},
{file = "pymongo-4.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:152259f0f1a60f560323aacf463a3642a65a25557683f49cfa08c8f1ecb2395a"}, {file = "pymongo-4.10.1-cp310-cp310-win32.whl", hash = "sha256:29e1c323c28a4584b7095378ff046815e39ff82cdb8dc4cc6dfe3acf6f9ad1f8"},
{file = "pymongo-4.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d64878d1659d2a5bdfd0f0a4d79bafe68653c573681495e424ab40d7b6d6d41"}, {file = "pymongo-4.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:88dc4aa45f8744ccfb45164aedb9a4179c93567bbd98a33109d7dc400b00eb08"},
{file = "pymongo-4.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1bb3a62395ffe835dbef3a1cbff48fbcce709c78bd1f52e896aee990928432b"}, {file = "pymongo-4.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:57ee6becae534e6d47848c97f6a6dff69e3cce7c70648d6049bd586764febe59"},
{file = "pymongo-4.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe48f50fb6348511a3268a893bfd4ab5f263f5ac220782449d03cd05964d1ae7"}, {file = "pymongo-4.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6f437a612f4d4f7aca1812311b1e84477145e950fdafe3285b687ab8c52541f3"},
{file = "pymongo-4.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7591a3beea6a9a4fa3080d27d193b41f631130e3ffa76b88c9ccea123f26dc59"}, {file = "pymongo-4.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a970fd3117ab40a4001c3dad333bbf3c43687d90f35287a6237149b5ccae61d"},
{file = "pymongo-4.5.0-cp310-cp310-win32.whl", hash = "sha256:3a7166d57dc74d679caa7743b8ecf7dc3a1235a9fd178654dddb2b2a627ae229"}, {file = "pymongo-4.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c4d0e7cd08ef9f8fbf2d15ba281ed55604368a32752e476250724c3ce36c72e"},
{file = "pymongo-4.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:21b953da14549ff62ea4ae20889c71564328958cbdf880c64a92a48dda4c9c53"}, {file = "pymongo-4.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca6f700cff6833de4872a4e738f43123db34400173558b558ae079b5535857a4"},
{file = "pymongo-4.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ead4f19d0257a756b21ac2e0e85a37a7245ddec36d3b6008d5bfe416525967dc"}, {file = "pymongo-4.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cec237c305fcbeef75c0bcbe9d223d1e22a6e3ba1b53b2f0b79d3d29c742b45b"},
{file = "pymongo-4.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aff6279e405dc953eeb540ab061e72c03cf38119613fce183a8e94f31be608f"}, {file = "pymongo-4.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3337804ea0394a06e916add4e5fac1c89902f1b6f33936074a12505cab4ff05"},
{file = "pymongo-4.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd4c8d6aa91d3e35016847cbe8d73106e3d1c9a4e6578d38e2c346bfe8edb3ca"}, {file = "pymongo-4.10.1-cp311-cp311-win32.whl", hash = "sha256:778ac646ce6ac1e469664062dfe9ae1f5c9961f7790682809f5ec3b8fda29d65"},
{file = "pymongo-4.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08819da7864f9b8d4a95729b2bea5fffed08b63d3b9c15b4fea47de655766cf5"}, {file = "pymongo-4.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:9df4ab5594fdd208dcba81be815fa8a8a5d8dedaf3b346cbf8b61c7296246a7a"},
{file = "pymongo-4.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a253b765b7cbc4209f1d8ee16c7287c4268d3243070bf72d7eec5aa9dfe2a2c2"}, {file = "pymongo-4.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fbedc4617faa0edf423621bb0b3b8707836687161210d470e69a4184be9ca011"},
{file = "pymongo-4.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8027c9063579083746147cf401a7072a9fb6829678076cd3deff28bb0e0f50c8"}, {file = "pymongo-4.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7bd26b2aec8ceeb95a5d948d5cc0f62b0eb6d66f3f4230705c1e3d3d2c04ec76"},
{file = "pymongo-4.5.0-cp311-cp311-win32.whl", hash = "sha256:9d2346b00af524757576cc2406414562cced1d4349c92166a0ee377a2a483a80"}, {file = "pymongo-4.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb104c3c2a78d9d85571c8ac90ec4f95bca9b297c6eee5ada71fabf1129e1674"},
{file = "pymongo-4.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:c3c3525ea8658ee1192cdddf5faf99b07ebe1eeaa61bf32821126df6d1b8072b"}, {file = "pymongo-4.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4924355245a9c79f77b5cda2db36e0f75ece5faf9f84d16014c0a297f6d66786"},
{file = "pymongo-4.5.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e5a27f348909235a106a3903fc8e70f573d89b41d723a500869c6569a391cff7"}, {file = "pymongo-4.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:11280809e5dacaef4971113f0b4ff4696ee94cfdb720019ff4fa4f9635138252"},
{file = "pymongo-4.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9a9a39b7cac81dca79fca8c2a6479ef4c7b1aab95fad7544cc0e8fd943595a2"}, {file = "pymongo-4.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5d55f2a82e5eb23795f724991cac2bffbb1c0f219c0ba3bf73a835f97f1bb2e"},
{file = "pymongo-4.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:496c9cbcb4951183d4503a9d7d2c1e3694aab1304262f831d5e1917e60386036"}, {file = "pymongo-4.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e974ab16a60be71a8dfad4e5afccf8dd05d41c758060f5d5bda9a758605d9a5d"},
{file = "pymongo-4.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23cc6d7eb009c688d70da186b8f362d61d5dd1a2c14a45b890bd1e91e9c451f2"}, {file = "pymongo-4.10.1-cp312-cp312-win32.whl", hash = "sha256:544890085d9641f271d4f7a47684450ed4a7344d6b72d5968bfae32203b1bb7c"},
{file = "pymongo-4.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fff7d17d30b2cd45afd654b3fc117755c5d84506ed25fda386494e4e0a3416e1"}, {file = "pymongo-4.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:dcc07b1277e8b4bf4d7382ca133850e323b7ab048b8353af496d050671c7ac52"},
{file = "pymongo-4.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6422b6763b016f2ef2beedded0e546d6aa6ba87910f9244d86e0ac7690f75c96"}, {file = "pymongo-4.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:90bc6912948dfc8c363f4ead54d54a02a15a7fee6cfafb36dc450fc8962d2cb7"},
{file = "pymongo-4.5.0-cp312-cp312-win32.whl", hash = "sha256:77cfff95c1fafd09e940b3fdcb7b65f11442662fad611d0e69b4dd5d17a81c60"}, {file = "pymongo-4.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:594dd721b81f301f33e843453638e02d92f63c198358e5a0fa8b8d0b1218dabc"},
{file = "pymongo-4.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:e57d859b972c75ee44ea2ef4758f12821243e99de814030f69a3decb2aa86807"}, {file = "pymongo-4.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0783e0c8e95397c84e9cf8ab092ab1e5dd7c769aec0ef3a5838ae7173b98dea0"},
{file = "pymongo-4.5.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2b0176f9233a5927084c79ff80b51bd70bfd57e4f3d564f50f80238e797f0c8a"}, {file = "pymongo-4.10.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fb6a72e88df46d1c1040fd32cd2d2c5e58722e5d3e31060a0393f04ad3283de"},
{file = "pymongo-4.5.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:89b3f2da57a27913d15d2a07d58482f33d0a5b28abd20b8e643ab4d625e36257"}, {file = "pymongo-4.10.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e3a593333e20c87415420a4fb76c00b7aae49b6361d2e2205b6fece0563bf40"},
{file = "pymongo-4.5.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:5caee7bd08c3d36ec54617832b44985bd70c4cbd77c5b313de6f7fce0bb34f93"}, {file = "pymongo-4.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72e2ace7456167c71cfeca7dcb47bd5dceda7db2231265b80fc625c5e8073186"},
{file = "pymongo-4.5.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:1d40ad09d9f5e719bc6f729cc6b17f31c0b055029719406bd31dde2f72fca7e7"}, {file = "pymongo-4.10.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ad05eb9c97e4f589ed9e74a00fcaac0d443ccd14f38d1258eb4c39a35dd722b"},
{file = "pymongo-4.5.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:076afa0a4a96ca9f77fec0e4a0d241200b3b3a1766f8d7be9a905ecf59a7416b"}, {file = "pymongo-4.10.1-cp313-cp313-win32.whl", hash = "sha256:ee4c86d8e6872a61f7888fc96577b0ea165eb3bdb0d841962b444fa36001e2bb"},
{file = "pymongo-4.5.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:3fa3648e4f1e63ddfe53563ee111079ea3ab35c3b09cd25bc22dadc8269a495f"}, {file = "pymongo-4.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:45ee87a4e12337353242bc758accc7fb47a2f2d9ecc0382a61e64c8f01e86708"},
{file = "pymongo-4.5.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:44ee985194c426ddf781fa784f31ffa29cb59657b2dba09250a4245431847d73"}, {file = "pymongo-4.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:442ca247f53ad24870a01e80a71cd81b3f2318655fd9d66748ee2bd1b1569d9e"},
{file = "pymongo-4.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b33c17d9e694b66d7e96977e9e56df19d662031483efe121a24772a44ccbbc7e"}, {file = "pymongo-4.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:23e1d62df5592518204943b507be7b457fb8a4ad95a349440406fd42db5d0923"},
{file = "pymongo-4.5.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d79ae3bb1ff041c0db56f138c88ce1dfb0209f3546d8d6e7c3f74944ecd2439"}, {file = "pymongo-4.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6131bc6568b26e7495a9f3ef2b1700566b76bbecd919f4472bfe90038a61f425"},
{file = "pymongo-4.5.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d67225f05f6ea27c8dc57f3fa6397c96d09c42af69d46629f71e82e66d33fa4f"}, {file = "pymongo-4.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdeba88c540c9ed0338c0b2062d9f81af42b18d6646b3e6dda05cf6edd46ada9"},
{file = "pymongo-4.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41771b22dd2822540f79a877c391283d4e6368125999a5ec8beee1ce566f3f82"}, {file = "pymongo-4.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15a624d752dd3c89d10deb0ef6431559b6d074703cab90a70bb849ece02adc6b"},
{file = "pymongo-4.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a1f26bc1f5ce774d99725773901820dfdfd24e875028da4a0252a5b48dcab5c"}, {file = "pymongo-4.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba164e73fdade9b4614a2497321c5b7512ddf749ed508950bdecc28d8d76a2d9"},
{file = "pymongo-4.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3236cf89d69679eaeb9119c840f5c7eb388a2110b57af6bb6baf01a1da387c18"}, {file = "pymongo-4.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9235fa319993405ae5505bf1333366388add2e06848db7b3deee8f990b69808e"},
{file = "pymongo-4.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e1f61355c821e870fb4c17cdb318669cfbcf245a291ce5053b41140870c3e5cc"}, {file = "pymongo-4.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e4a65567bd17d19f03157c7ec992c6530eafd8191a4e5ede25566792c4fe3fa2"},
{file = "pymongo-4.5.0-cp37-cp37m-win32.whl", hash = "sha256:49dce6957598975d8b8d506329d2a3a6c4aee911fa4bbcf5e52ffc6897122950"}, {file = "pymongo-4.10.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f1945d48fb9b8a87d515da07f37e5b2c35b364a435f534c122e92747881f4a7c"},
{file = "pymongo-4.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2227a08b091bd41df5aadee0a5037673f691e2aa000e1968b1ea2342afc6880"}, {file = "pymongo-4.10.1-cp38-cp38-win32.whl", hash = "sha256:345f8d340802ebce509f49d5833cc913da40c82f2e0daf9f60149cacc9ca680f"},
{file = "pymongo-4.5.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:435228d3c16a375274ac8ab9c4f9aef40c5e57ddb8296e20ecec9e2461da1017"}, {file = "pymongo-4.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:3a70d5efdc0387ac8cd50f9a5f379648ecfc322d14ec9e1ba8ec957e5d08c372"},
{file = "pymongo-4.5.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8e559116e4128630ad3b7e788e2e5da81cbc2344dee246af44471fa650486a70"}, {file = "pymongo-4.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15b1492cc5c7cd260229590be7218261e81684b8da6d6de2660cf743445500ce"},
{file = "pymongo-4.5.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:840eaf30ccac122df260b6005f9dfae4ac287c498ee91e3e90c56781614ca238"}, {file = "pymongo-4.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95207503c41b97e7ecc7e596d84a61f441b4935f11aa8332828a754e7ada8c82"},
{file = "pymongo-4.5.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b4fe46b58010115514b842c669a0ed9b6a342017b15905653a5b1724ab80917f"}, {file = "pymongo-4.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb99f003c720c6d83be02c8f1a7787c22384a8ca9a4181e406174db47a048619"},
{file = "pymongo-4.5.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:a8127437ebc196a6f5e8fddd746bd0903a400dc6b5ae35df672dd1ccc7170a2a"}, {file = "pymongo-4.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f2bc1ee4b1ca2c4e7e6b7a5e892126335ec8d9215bcd3ac2fe075870fefc3358"},
{file = "pymongo-4.5.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:2988ef5e6b360b3ff1c6d55c53515499de5f48df31afd9f785d788cdacfbe2d3"}, {file = "pymongo-4.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:93a0833c10a967effcd823b4e7445ec491f0bf6da5de0ca33629c0528f42b748"},
{file = "pymongo-4.5.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:e249190b018d63c901678053b4a43e797ca78b93fb6d17633e3567d4b3ec6107"}, {file = "pymongo-4.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f56707497323150bd2ed5d63067f4ffce940d0549d4ea2dfae180deec7f9363"},
{file = "pymongo-4.5.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:1240edc1a448d4ada4bf1a0e55550b6292420915292408e59159fd8bbdaf8f63"}, {file = "pymongo-4.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:409ab7d6c4223e5c85881697f365239dd3ed1b58f28e4124b846d9d488c86880"},
{file = "pymongo-4.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6d2a56fc2354bb6378f3634402eec788a8f3facf0b3e7d468db5f2b5a78d763"}, {file = "pymongo-4.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dac78a650dc0637d610905fd06b5fa6419ae9028cf4d04d6a2657bc18a66bbce"},
{file = "pymongo-4.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a0aade2b11dc0c326ccd429ee4134d2d47459ff68d449c6d7e01e74651bd255"}, {file = "pymongo-4.10.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1ec3fa88b541e0481aff3c35194c9fac96e4d57ec5d1c122376000eb28c01431"},
{file = "pymongo-4.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74c0da07c04d0781490b2915e7514b1adb265ef22af039a947988c331ee7455b"}, {file = "pymongo-4.10.1-cp39-cp39-win32.whl", hash = "sha256:e0e961923a7b8a1c801c43552dcb8153e45afa41749d9efbd3a6d33f45489f7a"},
{file = "pymongo-4.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3754acbd7efc7f1b529039fcffc092a15e1cf045e31f22f6c9c5950c613ec4d"}, {file = "pymongo-4.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:dabe8bf1ad644e6b93f3acf90ff18536d94538ca4d27e583c6db49889e98e48f"},
{file = "pymongo-4.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:631492573a1bef2f74f9ac0f9d84e0ce422c251644cd81207530af4aa2ee1980"}, {file = "pymongo-4.10.1.tar.gz", hash = "sha256:a9de02be53b6bb98efe0b9eda84ffa1ec027fcb23a2de62c4f941d9a2f2f3330"},
{file = "pymongo-4.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e2654d1278384cff75952682d17c718ecc1ad1d6227bb0068fd826ba47d426a5"},
{file = "pymongo-4.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:168172ef7856e20ec024fe2a746bfa895c88b32720138e6438fd765ebd2b62dd"},
{file = "pymongo-4.5.0-cp38-cp38-win32.whl", hash = "sha256:b25f7bea162b3dbec6d33c522097ef81df7c19a9300722fa6853f5b495aecb77"},
{file = "pymongo-4.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:b520aafc6cb148bac09ccf532f52cbd31d83acf4d3e5070d84efe3c019a1adbf"},
{file = "pymongo-4.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8543253adfaa0b802bfa88386db1009c6ebb7d5684d093ee4edc725007553d21"},
{file = "pymongo-4.5.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:bc5d8c3647b8ae28e4312f1492b8f29deebd31479cd3abaa989090fb1d66db83"},
{file = "pymongo-4.5.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:505f8519c4c782a61d94a17b0da50be639ec462128fbd10ab0a34889218fdee3"},
{file = "pymongo-4.5.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:53f2dda54d76a98b43a410498bd12f6034b2a14b6844ca08513733b2b20b7ad8"},
{file = "pymongo-4.5.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:9c04b9560872fa9a91251030c488e0a73bce9321a70f991f830c72b3f8115d0d"},
{file = "pymongo-4.5.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:58a63a26a1e3dc481dd3a18d6d9f8bd1d576cd1ffe0d479ba7dd38b0aeb20066"},
{file = "pymongo-4.5.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:f076b779aa3dc179aa3ed861be063a313ed4e48ae9f6a8370a9b1295d4502111"},
{file = "pymongo-4.5.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:1b1d7d9aabd8629a31d63cd106d56cca0e6420f38e50563278b520f385c0d86e"},
{file = "pymongo-4.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37df8f6006286a5896d1cbc3efb8471ced42e3568d38e6cb00857277047b0d63"},
{file = "pymongo-4.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56320c401f544d762fc35766936178fbceb1d9261cd7b24fbfbc8fb6f67aa8a5"},
{file = "pymongo-4.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbd705d5f3c3d1ff2d169e418bb789ff07ab3c70d567cc6ba6b72b04b9143481"},
{file = "pymongo-4.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80a167081c75cf66b32f30e2f1eaee9365af935a86dbd76788169911bed9b5d5"},
{file = "pymongo-4.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c42748ccc451dfcd9cef6c5447a7ab727351fd9747ad431db5ebb18a9b78a4d"},
{file = "pymongo-4.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf62da7a4cdec9a4b2981fcbd5e08053edffccf20e845c0b6ec1e77eb7fab61d"},
{file = "pymongo-4.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b5bbb87fa0511bd313d9a2c90294c88db837667c2bda2ea3fa7a35b59fd93b1f"},
{file = "pymongo-4.5.0-cp39-cp39-win32.whl", hash = "sha256:465fd5b040206f8bce7016b01d7e7f79d2fcd7c2b8e41791be9632a9df1b4999"},
{file = "pymongo-4.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:63d8019eee119df308a075b8a7bdb06d4720bf791e2b73d5ab0e7473c115d79c"},
{file = "pymongo-4.5.0.tar.gz", hash = "sha256:681f252e43b3ef054ca9161635f81b730f4d8cadd28b3f2b2004f5a72f853982"},
] ]
[package.dependencies] [package.dependencies]
dnspython = ">=1.16.0,<3.0.0" dnspython = ">=1.16.0,<3.0.0"
[package.extras] [package.extras]
aws = ["pymongo-auth-aws (<2.0.0)"] aws = ["pymongo-auth-aws (>=1.1.0,<2.0.0)"]
encryption = ["certifi", "pymongo[aws]", "pymongocrypt (>=1.6.0,<2.0.0)"] docs = ["furo (==2023.9.10)", "readthedocs-sphinx-search (>=0.3,<1.0)", "sphinx (>=5.3,<8)", "sphinx-autobuild (>=2020.9.1)", "sphinx-rtd-theme (>=2,<3)", "sphinxcontrib-shellcheck (>=1,<2)"]
encryption = ["certifi", "pymongo-auth-aws (>=1.1.0,<2.0.0)", "pymongocrypt (>=1.10.0,<2.0.0)"]
gssapi = ["pykerberos", "winkerberos (>=0.5.0)"] gssapi = ["pykerberos", "winkerberos (>=0.5.0)"]
ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"]
snappy = ["python-snappy"] snappy = ["python-snappy"]
test = ["pytest (>=8.2)", "pytest-asyncio (>=0.24.0)"]
zstd = ["zstandard"] zstd = ["zstandard"]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.31.0" version = "2.32.3"
description = "Python HTTP for Humans." description = "Python HTTP for Humans."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
] ]
[package.dependencies] [package.dependencies]
@ -509,6 +496,87 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"] socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "ruff"
version = "0.6.9"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"},
{file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"},
{file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"},
{file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"},
{file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"},
{file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"},
{file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"},
{file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"},
{file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"},
{file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"},
{file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"},
{file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"},
{file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"},
{file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"},
{file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"},
{file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"},
{file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"},
{file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"},
]
[[package]]
name = "sentry-sdk"
version = "2.15.0"
description = "Python client for Sentry (https://sentry.io)"
optional = false
python-versions = ">=3.6"
files = [
{file = "sentry_sdk-2.15.0-py2.py3-none-any.whl", hash = "sha256:8fb0d1a4e1a640172f31502e4503543765a1fe8a9209779134a4ac52d4677303"},
{file = "sentry_sdk-2.15.0.tar.gz", hash = "sha256:a599e7d3400787d6f43327b973e55a087b931ba2c592a7a7afa691f8eb5e75e2"},
]
[package.dependencies]
blinker = {version = ">=1.1", optional = true, markers = "extra == \"flask\""}
certifi = "*"
flask = {version = ">=0.11", optional = true, markers = "extra == \"flask\""}
markupsafe = {version = "*", optional = true, markers = "extra == \"flask\""}
urllib3 = ">=1.26.11"
[package.extras]
aiohttp = ["aiohttp (>=3.5)"]
anthropic = ["anthropic (>=0.16)"]
arq = ["arq (>=0.23)"]
asyncpg = ["asyncpg (>=0.23)"]
beam = ["apache-beam (>=2.12)"]
bottle = ["bottle (>=0.12.13)"]
celery = ["celery (>=3)"]
celery-redbeat = ["celery-redbeat (>=2)"]
chalice = ["chalice (>=1.16.0)"]
clickhouse-driver = ["clickhouse-driver (>=0.2.0)"]
django = ["django (>=1.8)"]
falcon = ["falcon (>=1.4)"]
fastapi = ["fastapi (>=0.79.0)"]
flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"]
grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"]
httpx = ["httpx (>=0.16.0)"]
huey = ["huey (>=2)"]
huggingface-hub = ["huggingface-hub (>=0.22)"]
langchain = ["langchain (>=0.0.210)"]
litestar = ["litestar (>=2.0.0)"]
loguru = ["loguru (>=0.5)"]
openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"]
opentelemetry = ["opentelemetry-distro (>=0.35b0)"]
opentelemetry-experimental = ["opentelemetry-distro"]
pure-eval = ["asttokens", "executing", "pure-eval"]
pymongo = ["pymongo (>=3.1)"]
pyspark = ["pyspark (>=2.4.4)"]
quart = ["blinker (>=1.1)", "quart (>=0.16.1)"]
rq = ["rq (>=0.6)"]
sanic = ["sanic (>=0.8)"]
sqlalchemy = ["sqlalchemy (>=1.2)"]
starlette = ["starlette (>=0.19.1)"]
starlite = ["starlite (>=1.48)"]
tornado = ["tornado (>=6)"]
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.0.6" version = "2.0.6"
@ -526,6 +594,27 @@ secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"] zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "webargs"
version = "8.6.0"
description = "Declarative parsing and validation of HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, Falcon, and aiohttp."
optional = false
python-versions = ">=3.8"
files = [
{file = "webargs-8.6.0-py3-none-any.whl", hash = "sha256:83da4d7105643d0a50499b06d98a6ade1a330ce66d039eaa51f715172c704aba"},
{file = "webargs-8.6.0.tar.gz", hash = "sha256:b8d098ab92bd74c659eca705afa31d681475f218cb15c1e57271fa2103c0547a"},
]
[package.dependencies]
marshmallow = ">=3.0.0"
packaging = ">=17.0"
[package.extras]
dev = ["pre-commit (>=3.5,<4.0)", "tox", "webargs[tests]"]
docs = ["Sphinx (==8.0.2)", "furo (==2024.8.6)", "sphinx-issues (==4.1.0)", "webargs[frameworks]"]
frameworks = ["Django (>=2.2.0)", "Flask (>=0.12.5)", "aiohttp (>=3.0.8)", "bottle (>=0.12.13)", "falcon (>=2.0.0)", "pyramid (>=1.9.1)", "tornado (>=4.5.2)"]
tests = ["pytest", "pytest-aiohttp (>=0.3.0)", "pytest-asyncio", "webargs[frameworks]", "webtest (==3.0.1)", "webtest-aiohttp (==2.0.0)"]
[[package]] [[package]]
name = "werkzeug" name = "werkzeug"
version = "3.0.0" version = "3.0.0"
@ -543,22 +632,7 @@ MarkupSafe = ">=2.1.1"
[package.extras] [package.extras]
watchdog = ["watchdog (>=2.3)"] watchdog = ["watchdog (>=2.3)"]
[[package]]
name = "zipp"
version = "3.17.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.8"
files = [
{file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"},
{file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.9" python-versions = "^3.12"
content-hash = "cf9ff69043142d8888762d6050e94cc19bbdfcce39425af14a189750ad4d4292" content-hash = "958a271fe7a1c368156256979dde90b40cffe79186308a61c2207ea25767fcd9"

View File

@ -5,17 +5,22 @@ description = "Dotty API"
authors = ["Will <will@dotty.cloud>"] authors = ["Will <will@dotty.cloud>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.9" python = "^3.12"
flask = "^3.0.0" flask = "^3.0.3"
bcrypt = "^4.0.1" bcrypt = "^4.2.0"
pyjwt = "^2.8.0" pyjwt = "^2.9.0"
pymongo = "^4.5.0" pymongo = "^4.10.1"
requests = "^2.31.0" requests = "^2.32.3"
dnspython = "^2.4.2" dnspython = "^2.6.1"
gunicorn = "^21.2.0" gunicorn = "^23.0.0"
webargs = "^8.6.0"
sentry-sdk = {extras = ["flask"], version = "^2.15.0"}
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
[tool.poetry.group.dev.dependencies]
ruff = "^0.6.9"
[build-system] [build-system]
requires = ["poetry>=0.12"] requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api" build-backend = "poetry.masonry.api"

View File

@ -3,8 +3,9 @@ from pymongo import MongoClient
db = None db = None
def get_db(): def get_db():
global db global db
if db is None: if db is None:
db = MongoClient(os.environ['MONGO_URL'])[os.environ['MONGO_DATABASE']] db = MongoClient(os.environ["MONGO_URL"])[os.environ["MONGO_DATABASE"]]
return db return db

View File

@ -1,14 +1,23 @@
import os import os
import requests import requests
key = os.environ.get('MAILGUN_KEY') key = os.environ.get("MAILGUN_KEY")
request_url = 'https://api.eu.mailgun.net/v3/mail.dotty.cloud/messages' request_url = "https://api.eu.mailgun.net/v3/mail.dotty.cloud/messages"
def send(recipient, subject, message, from_address = 'Dotty <noreply@mail.dotty.cloud>'):
response = requests.post(request_url, auth=('api', key), data={ def send(recipient, subject, message, from_address="Dotty <noreply@mail.dotty.cloud>"):
'from': from_address, if key:
'to': recipient, response = requests.post(
'subject': subject, request_url,
'text': message auth=("api", key),
}) data={
response.raise_for_status() "from": from_address,
"to": recipient,
"subject": subject,
"text": message,
},
)
response.raise_for_status()
else:
print("No mailgun key found")
print(message)

View File

@ -1,35 +1,22 @@
import json, datetime, re from flask import request, g, jsonify
from flask import Flask, request, g, Response, jsonify
from functools import wraps from functools import wraps
import accounts import accounts
def auth(route_function): def auth(route_function):
@wraps(route_function) @wraps(route_function)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if 'Authorization' not in request.headers: if "Authorization" not in request.headers:
return jsonify({'success': False, 'message': 'This resource requires authentication'}), 401 return jsonify(
g.user = accounts.get_user_context(request, request.headers['Authorization'].replace('Bearer ', '')) {"success": False, "message": "This resource requires authentication"}
if g.user is None: ), 401
return jsonify({'success': False, 'message': 'Invalid authorisation token'}), 401 g.user = accounts.get_user_context(
return route_function(*args, **kwargs) request, request.headers["Authorization"].replace("Bearer ", "")
return decorated_function )
if g.user is None:
return jsonify(
{"success": False, "message": "Invalid authorisation token"}
), 401
return route_function(*args, **kwargs)
def maybe_auth(route_function): return decorated_function
@wraps(route_function)
def decorated_function(*args, **kwargs):
if 'Authorization' in request.headers:
g.user = accounts.get_user_context(request, request.headers['Authorization'].replace('Bearer ', ''))
else: g.user = None
return route_function(*args, **kwargs)
return decorated_function
class JsonEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, (datetime.datetime, datetime.date)):
return obj.isoformat()
elif isinstance(obj, ObjectId):
return str(obj)
return json.JSONEncoder.default(self, obj)
def jsonify(*args, **kwargs):
return json.dumps(dict(*args, **kwargs), cls=JsonEncoder)

View File

@ -321,10 +321,11 @@ func Logout(arg string) error {
func ChangePassword(currentPassword *string, newPassword *string) error { func ChangePassword(currentPassword *string, newPassword *string) error {
m := make(map[string]string) m := make(map[string]string)
m["currentPassword"] = *currentPassword m["currentPassword"] = *currentPassword
m["newPassword"] = *newPassword m["password"] = *newPassword
data, _ := json.Marshal(m) data, _ := json.Marshal(m)
conf := *config.Load() conf := *config.Load()
_, reqErr := authenticatedRequest(&conf.AccessToken, "PUT", "/accounts/password", nil, strings.NewReader(string(data))) headers := map[string]string{"Content-Type": "application/json"};
_, reqErr := authenticatedRequest(&conf.AccessToken, "PUT", "/accounts/password", &headers, strings.NewReader(string(data)))
return reqErr return reqErr
} }

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More