From 946940fa926594e6fa3ace55c06a14364aa0148e Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Wed, 25 Mar 2026 02:47:12 +0530 Subject: [PATCH 01/10] feat: migrated from minio to rustfs --- docker-compose.yaml | 9 ------ src/paste/minio.py | 69 +++++++++++++++++++++++---------------------- 2 files changed, 35 insertions(+), 43 deletions(-) delete mode 100644 docker-compose.yaml diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index 8d69b32..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,9 +0,0 @@ -services: - myapp: - image: mrsunglasses/pastepy:latest - env_file: - - .env - ports: - - "8082:8080" - entrypoint: ["./docker-entrypoint.sh"] - command: ["pdm", "run", "start"] \ No newline at end of file diff --git a/src/paste/minio.py b/src/paste/minio.py index eef4343..23f1b0a 100644 --- a/src/paste/minio.py +++ b/src/paste/minio.py @@ -2,34 +2,40 @@ import uuid from typing import Optional -from minio import Minio -from minio.error import S3Error +import boto3 +from botocore.client import Config +from botocore.exceptions import ClientError from .config import get_settings -client = Minio( - get_settings().MINIO_CLIENT_LINK, - access_key=get_settings().MINIO_ACCESS_KEY, - secret_key=get_settings().MINIO_SECRET_KEY, - secure=True, +client = boto3.client( + "s3", + endpoint_url=get_settings().MINIO_CLIENT_LINK, + aws_access_key_id=get_settings().MINIO_ACCESS_KEY, + aws_secret_access_key=get_settings().MINIO_SECRET_KEY, + config=Config(signature_version="s3v4"), + region_name="us-east-1", ) +def create_bucket_if_not_exists(bucket_name: str = get_settings().MINIO_BUCKET_NAME) -> None: + try: + client.create_bucket(Bucket=bucket_name) + except ClientError as exc: + if exc.response["Error"]["Code"] != "BucketAlreadyOwnedByYou": + raise + + def get_object_data(object_name: str, bucket_name: str = get_settings().MINIO_BUCKET_NAME) -> str | None: response = None data = None try: - response = client.get_object(bucket_name, object_name) - data = response.read() - except S3Error as exc: + response = client.get_object(Bucket=bucket_name, Key=object_name) + data = response["Body"].read() + except ClientError as exc: raise Exception("error occured.", exc) except Exception as exc: raise FileNotFoundError(f"Failed to retrieve file '{object_name}' from bucket '{bucket_name}': {exc}") - finally: - if response: - response.close() - response.release_conn() - return data.decode("utf-8") @@ -37,7 +43,7 @@ def post_object_data( object_data: str, object_name: Optional[str] = None, bucket_name: str = get_settings().MINIO_BUCKET_NAME, -) -> str: +) -> str | None: try: if not object_name: object_name = str(uuid.uuid4()) @@ -46,21 +52,15 @@ def post_object_data( data_length = len(data_bytes) client.put_object( - bucket_name=bucket_name, - object_name=object_name, - data=io.BytesIO(data_bytes), - length=data_length, - content_type="text/plain", + Bucket=bucket_name, + Key=object_name, + Body=io.BytesIO(data_bytes), + ContentLength=data_length, + ContentType="text/plain", ) - # Generate the object URL using the proper method - object_url = client.get_presigned_url( - "GET", - bucket_name=bucket_name, - object_name=object_name, - ) - return object_url - except S3Error as exc: + return object_name + except ClientError as exc: raise Exception(f"Failed to upload file '{object_name}' to bucket '{bucket_name}': {exc}") @@ -68,18 +68,19 @@ def post_object_data_as_file( source_file_path: str, object_name: Optional[str] = None, bucket_name: str = get_settings().MINIO_BUCKET_NAME, -) -> None: +) -> str | None: try: if not object_name: object_name = str(uuid.uuid4()) - client.fput_object(bucket_name, object_name, source_file_path) - except S3Error as exc: + client.upload_file(source_file_path, bucket_name, object_name) + return object_name + except ClientError as exc: raise Exception(f"Failed to upload file '{object_name}' to bucket '{bucket_name}': {exc}") def delete_object_data(object_name: str, bucket_name: str = get_settings().MINIO_BUCKET_NAME) -> None: try: - client.remove_object(bucket_name, object_name) - except S3Error as exc: + client.delete_object(Bucket=bucket_name, Key=object_name) + except ClientError as exc: raise Exception(f"Failed to delete file '{object_name}' from bucket '{bucket_name}': {exc}") From be429ae5814f8bbad6c3858e4aa5269a0184dad9 Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Wed, 25 Mar 2026 02:49:51 +0530 Subject: [PATCH 02/10] feat: removed minio as deps and add boto3 --- .python-version | 2 +- pdm.lock | 300 +++++++++++++++++++++++++----------------------- pyproject.toml | 5 +- 3 files changed, 159 insertions(+), 148 deletions(-) diff --git a/.python-version b/.python-version index fc89ddd..2c07333 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.11.3 \ No newline at end of file +3.11 diff --git a/pdm.lock b/pdm.lock index ae7b60b..1643feb 100644 --- a/pdm.lock +++ b/pdm.lock @@ -2,10 +2,10 @@ # It is not intended for manual editing. [metadata] -groups = ["default", "hooks", "lint", "test", "typing"] +groups = ["default", "dev", "hooks", "lint", "test", "typing"] strategy = [] lock_version = "4.5.0" -content_hash = "sha256:7eea0575175e4b373f5e6471617a28e858ef8fe8bdb1d70d6eab7cc9f09bfe71" +content_hash = "sha256:0e5bc79be9041f277deb5cf176533b808e8d43fdd91aa97e9632fe2549caf263" [[metadata.targets]] requires_python = ">=3.10" @@ -56,42 +56,6 @@ files = [ {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, ] -[[package]] -name = "argon2-cffi" -version = "23.1.0" -requires_python = ">=3.7" -summary = "Argon2 for Python" -dependencies = [ - "argon2-cffi-bindings", - "typing-extensions; python_version < \"3.8\"", -] -files = [ - {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, - {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, -] - -[[package]] -name = "argon2-cffi-bindings" -version = "21.2.0" -requires_python = ">=3.6" -summary = "Low-level CFFI bindings for Argon2" -dependencies = [ - "cffi>=1.0.1", -] -files = [ - {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, - {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, -] - [[package]] name = "black" version = "25.1.0" @@ -127,6 +91,92 @@ files = [ {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, ] +[[package]] +name = "boto3" +version = "1.42.74" +requires_python = ">=3.9" +summary = "The AWS SDK for Python" +dependencies = [ + "botocore<1.43.0,>=1.42.74", + "jmespath<2.0.0,>=0.7.1", + "s3transfer<0.17.0,>=0.16.0", +] +files = [ + {file = "boto3-1.42.74-py3-none-any.whl", hash = "sha256:4bf89c044d618fe4435af854ab820f09dd43569c0df15d7beb0398f50b9aa970"}, +] + +[[package]] +name = "boto3-stubs" +version = "1.42.74" +requires_python = ">=3.9" +summary = "Type annotations for boto3 1.42.74 generated with mypy-boto3-builder 8.12.0" +dependencies = [ + "botocore-stubs", + "types-s3transfer", + "typing-extensions>=4.1.0; python_version < \"3.12\"", +] +files = [ + {file = "boto3_stubs-1.42.74-py3-none-any.whl", hash = "sha256:63b7ba180b3fe361dcae0a50dd57e1ac676149cf0c90be420fa067189bafa7c6"}, + {file = "boto3_stubs-1.42.74.tar.gz", hash = "sha256:781078235e61c78000035ece0a92befaaf846762b6a91becf6b2887331fd010d"}, +] + +[[package]] +name = "boto3-stubs-full" +version = "1.42.74" +requires_python = ">=3.9" +summary = "All-in-one type annotations for boto3 1.42.74 generated with mypy-boto3-builder 8.12.0" +dependencies = [ + "typing-extensions; python_version < \"3.12\"", +] +files = [ + {file = "boto3_stubs_full-1.42.74-py3-none-any.whl", hash = "sha256:a377f4696879d922de70bac504f67df45b1dacbf350ccd1c4a76a6fe02503f84"}, + {file = "boto3_stubs_full-1.42.74.tar.gz", hash = "sha256:9be73972fd625604566a27a55ed7f23747b9d29b1487316458239f273b8a3380"}, +] + +[[package]] +name = "boto3-stubs" +version = "1.42.74" +extras = ["full"] +requires_python = ">=3.9" +summary = "Type annotations for boto3 1.42.74 generated with mypy-boto3-builder 8.12.0" +dependencies = [ + "boto3-stubs-full<1.43.0,>=1.42.0", + "boto3-stubs==1.42.74", +] +files = [ + {file = "boto3_stubs-1.42.74-py3-none-any.whl", hash = "sha256:63b7ba180b3fe361dcae0a50dd57e1ac676149cf0c90be420fa067189bafa7c6"}, + {file = "boto3_stubs-1.42.74.tar.gz", hash = "sha256:781078235e61c78000035ece0a92befaaf846762b6a91becf6b2887331fd010d"}, +] + +[[package]] +name = "botocore" +version = "1.42.74" +requires_python = ">=3.9" +summary = "Low-level, data-driven core of boto 3." +dependencies = [ + "jmespath<2.0.0,>=0.7.1", + "python-dateutil<3.0.0,>=2.1", + "urllib3!=2.2.0,<3,>=1.25.4; python_version >= \"3.10\"", + "urllib3<1.27,>=1.25.4; python_version < \"3.10\"", +] +files = [ + {file = "botocore-1.42.74-py3-none-any.whl", hash = "sha256:3a76a8af08b5de82e51a0ae132394e226e15dbf21c8146ac3f7c1f881517a7a7"}, + {file = "botocore-1.42.74.tar.gz", hash = "sha256:9cf5cdffc6c90ed87b0fe184676806182588be0d0df9b363e9fe3e2923ac8e80"}, +] + +[[package]] +name = "botocore-stubs" +version = "1.42.41" +requires_python = ">=3.9" +summary = "Type annotations and code completion for botocore" +dependencies = [ + "types-awscrt", +] +files = [ + {file = "botocore_stubs-1.42.41-py3-none-any.whl", hash = "sha256:9423110fb0e391834bd2ed44ae5f879d8cb370a444703d966d30842ce2bcb5f0"}, + {file = "botocore_stubs-1.42.41.tar.gz", hash = "sha256:dbeac2f744df6b814ce83ec3f3777b299a015cbea57a2efc41c33b8c38265825"}, +] + [[package]] name = "certifi" version = "2023.11.17" @@ -137,64 +187,6 @@ files = [ {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] -[[package]] -name = "cffi" -version = "1.17.1" -requires_python = ">=3.8" -summary = "Foreign Function Interface for Python calling C code." -dependencies = [ - "pycparser", -] -files = [ - {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, - {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, - {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, - {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, - {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, - {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, - {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, - {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, - {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, - {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, - {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, -] - [[package]] name = "cfgv" version = "3.4.0" @@ -607,6 +599,16 @@ files = [ {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, ] +[[package]] +name = "jmespath" +version = "1.1.0" +requires_python = ">=3.9" +summary = "JSON Matching Expressions" +files = [ + {file = "jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64"}, + {file = "jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d"}, +] + [[package]] name = "limits" version = "3.7.0" @@ -698,23 +700,6 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] -[[package]] -name = "minio" -version = "7.2.15" -requires_python = ">=3.9" -summary = "MinIO Python SDK for Amazon S3 Compatible Cloud Storage" -dependencies = [ - "argon2-cffi", - "certifi", - "pycryptodome", - "typing-extensions", - "urllib3", -] -files = [ - {file = "minio-7.2.15-py3-none-any.whl", hash = "sha256:c06ef7a43e5d67107067f77b6c07ebdd68733e5aa7eed03076472410ca19d876"}, - {file = "minio-7.2.15.tar.gz", hash = "sha256:5247df5d4dca7bfa4c9b20093acd5ad43e82d8710ceb059d79c6eea970f49f79"}, -] - [[package]] name = "mypy" version = "1.14.1" @@ -928,39 +913,6 @@ files = [ {file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"}, ] -[[package]] -name = "pycparser" -version = "2.22" -requires_python = ">=3.8" -summary = "C parser in Python" -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - -[[package]] -name = "pycryptodome" -version = "3.21.0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -summary = "Cryptographic library for Python" -files = [ - {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4"}, - {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b"}, - {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e"}, - {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8"}, - {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1"}, - {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a"}, - {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2"}, - {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93"}, - {file = "pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764"}, - {file = "pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53"}, - {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8"}, - {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6"}, - {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0"}, - {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6"}, - {file = "pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297"}, -] - [[package]] name = "pydantic" version = "2.10.6" @@ -1107,6 +1059,19 @@ files = [ {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Extensions to the standard Python datetime module" +dependencies = [ + "six>=1.5", +] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + [[package]] name = "python-dotenv" version = "1.0.0" @@ -1230,6 +1195,19 @@ files = [ {file = "ruff-0.9.5.tar.gz", hash = "sha256:11aecd7a633932875ab3cb05a484c99970b9d52606ce9ea912b690b02653d56c"}, ] +[[package]] +name = "s3transfer" +version = "0.16.0" +requires_python = ">=3.9" +summary = "An Amazon S3 Transfer Manager" +dependencies = [ + "botocore<2.0a.0,>=1.37.4", +] +files = [ + {file = "s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe"}, + {file = "s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920"}, +] + [[package]] name = "setuptools" version = "69.0.2" @@ -1250,6 +1228,16 @@ files = [ {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] +[[package]] +name = "six" +version = "1.17.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Python 2 and 3 compatibility utilities" +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + [[package]] name = "slowapi" version = "0.1.9" @@ -1360,6 +1348,26 @@ files = [ {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"}, ] +[[package]] +name = "types-awscrt" +version = "0.31.3" +requires_python = ">=3.8" +summary = "Type annotations and code completion for awscrt" +files = [ + {file = "types_awscrt-0.31.3-py3-none-any.whl", hash = "sha256:e5ce65a00a2ab4f35eacc1e3d700d792338d56e4823ee7b4dbe017f94cfc4458"}, + {file = "types_awscrt-0.31.3.tar.gz", hash = "sha256:09d3eaf00231e0f47e101bd9867e430873bc57040050e2a3bd8305cb4fc30865"}, +] + +[[package]] +name = "types-s3transfer" +version = "0.16.0" +requires_python = ">=3.9" +summary = "Type annotations and code completion for s3transfer" +files = [ + {file = "types_s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:1c0cd111ecf6e21437cb410f5cddb631bfb2263b77ad973e79b9c6d0cb24e0ef"}, + {file = "types_s3transfer-0.16.0.tar.gz", hash = "sha256:b4636472024c5e2b62278c5b759661efeb52a81851cde5f092f24100b1ecb443"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" diff --git a/pyproject.toml b/pyproject.toml index 9b9904b..27a528a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,8 +10,8 @@ dependencies = [ "pygments>=2.17.2", "alembic>=1.14.1", "pydantic-settings>=2.7.1", - "minio>=7.2.15", "psycopg2-binary>=2.9.10", + "boto3>=1.42.74", ] requires-python = ">=3.10" readme = "README.md" @@ -50,3 +50,6 @@ hooks = [ "pre-commit>=3.6.0", "isort>=6.0.0", ] +dev = [ + "boto3-stubs[full]>=1.42.74", +] From fe0dda778f7f038289b405af55e2e67468bf9b68 Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Wed, 25 Mar 2026 02:50:36 +0530 Subject: [PATCH 03/10] chore: bump python version --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 430acb0..955828a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # Dockerfile # pull the official docker image -FROM python:3.11.1-slim AS builder +FROM python:3.11.3-slim AS builder # install PDM RUN pip install -U pip setuptools wheel From d388b14002840d171b4a894d7f07ff01a418e1c5 Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Wed, 25 Mar 2026 02:51:14 +0530 Subject: [PATCH 04/10] fix: delete s3 object, when delete a paste --- src/paste/main.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/paste/main.py b/src/paste/main.py index df04386..18fcb43 100644 --- a/src/paste/main.py +++ b/src/paste/main.py @@ -29,7 +29,7 @@ from .database import Session_Local, get_db from .logging import LogConfig from .middleware import LimitUploadSize -from .minio import get_object_data, post_object_data +from .minio import create_bucket_if_not_exists, delete_object_data, get_object_data, post_object_data from .models import Paste from .schema import HealthErrorResponse, HealthResponse, PasteCreate, PasteDetails, PasteResponse from .utils import _filter_object_name_from_link, extract_uuid @@ -58,6 +58,12 @@ async def delete_expired_urls() -> None: expired_urls = db.query(Paste).filter(Paste.expiresat <= current_time).all() for url in expired_urls: + if url.s3_link: + try: + object_name = _filter_object_name_from_link(url.s3_link) + delete_object_data(object_name) + except Exception as s3_err: + logger.error(f"Failed to delete S3 object for expired paste {url.pasteID}: {s3_err}") db.delete(url) db.commit() @@ -128,6 +134,11 @@ async def startup_event(): asyncio.create_task(delete_expired_urls()) +@app.on_event("startup") +async def startup_event(): + create_bucket_if_not_exists() + + origins: List[str] = ["*"] BASE_URL: str = get_settings().BASE_URL @@ -214,7 +225,7 @@ async def get_paste_data( content = data.content extension = data.extension else: - content = get_object_data(_filter_object_name_from_link(data.s3_link)) + content = get_object_data(data.s3_link) extension = data.extension extension = extension[1::] if extension.startswith(".") else extension @@ -346,16 +357,28 @@ async def delete_paste(uuid: str, db: Session = Depends(get_db)) -> PlainTextRes try: data = db.query(Paste).filter(Paste.pasteID == uuid).first() if data: + if data.s3_link: + try: + object_name = _filter_object_name_from_link(data.s3_link) + delete_object_data(object_name) + except Exception as s3_err: + logger.error(f"Failed to delete S3 object for paste {uuid}: {s3_err}") + raise HTTPException( + detail="Failed to delete associated S3 data.", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) db.delete(data) db.commit() return PlainTextResponse(f"File successfully deleted {uuid}") else: raise HTTPException(detail="File Not Found", status_code=status.HTTP_404_NOT_FOUND) + except HTTPException: + raise except Exception as e: db.rollback() + logger.error(f"Error deleting paste: {e}") raise HTTPException( - logger.error(f"Error deleting paste: {e}"), - detail="There is an error happend.", + detail="There is an error happened.", status_code=status.HTTP_409_CONFLICT, ) finally: From 0c4cea2543f2428ecfc870a5d11d40e0fb40ef59 Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Wed, 25 Mar 2026 02:51:32 +0530 Subject: [PATCH 05/10] feat: add docker compose file --- docker-compose.yml | 72 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d554014 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,72 @@ +services: + postgres: + image: postgres:15-alpine + container_name: postgres15 + restart: unless-stopped + environment: + POSTGRES_DB: pastedb + POSTGRES_USER: postgres + POSTGRES_PASSWORD: mytestpassword + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d pastedb"] + interval: 10s + timeout: 5s + retries: 5 + + rustfs: + image: rustfs/rustfs:1.0.0-alpha.89 + container_name: rustfs + restart: unless-stopped + user: root + ports: + - "9000:9000" # S3 API endpoint + - "9001:9001" # Console UI + security_opt: + - "no-new-privileges:true" + environment: + - RUSTFS_VOLUMES=/data{1...4} + - RUSTFS_ADDRESS=0.0.0.0:9000 + - RUSTFS_CONSOLE_ENABLE=true + - RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001 + - RUSTFS_ACCESS_KEY=minioadmin + - RUSTFS_SECRET_KEY=minioadmin123 + volumes: + - data1:/data1 + - data2:/data2 + - data3:/data3 + - data4:/data4 + # healthcheck: + # test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + # interval: 30s + # timeout: 10s + # retries: 5 + # start_period: 20s + + myapp: + build: + context: . + dockerfile: Dockerfile + environment: + MINIO_CLIENT_LINK: http://rustfs:9000 + MINIO_ACCESS_KEY: minioadmin + MINIO_SECRET_KEY: minioadmin123 + MINIO_BUCKET_NAME: pastebucket + BASE_URL: http://127.0.0.1:8082 + SQLALCHEMY_DATABASE_URL: postgresql://postgres:mytestpassword@postgres:5432/pastedb + ports: + - "8082:8080" + entrypoint: ["./docker-entrypoint.sh"] + command: ["pdm", "run", "dev"] + depends_on: + postgres: + condition: service_healthy +volumes: + postgres_data: + data1: + data2: + data3: + data4: From 0cf593f58bea4555fd8335243dc7eea69ecc5d56 Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Wed, 25 Mar 2026 03:45:14 +0530 Subject: [PATCH 06/10] fix: basedpyright errors --- src/paste/minio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/paste/minio.py b/src/paste/minio.py index 23f1b0a..82b004f 100644 --- a/src/paste/minio.py +++ b/src/paste/minio.py @@ -43,7 +43,7 @@ def post_object_data( object_data: str, object_name: Optional[str] = None, bucket_name: str = get_settings().MINIO_BUCKET_NAME, -) -> str | None: +) -> str: try: if not object_name: object_name = str(uuid.uuid4()) @@ -68,7 +68,7 @@ def post_object_data_as_file( source_file_path: str, object_name: Optional[str] = None, bucket_name: str = get_settings().MINIO_BUCKET_NAME, -) -> str | None: +) -> str: try: if not object_name: object_name = str(uuid.uuid4()) From 63975f3407fd994aa12eb24a2685b471c9c78f5d Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Wed, 25 Mar 2026 03:46:14 +0530 Subject: [PATCH 07/10] fix: basedpyright errors --- src/paste/main.py | 68 +++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/src/paste/main.py b/src/paste/main.py index 18fcb43..5206640 100644 --- a/src/paste/main.py +++ b/src/paste/main.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone from logging.config import dictConfig from pathlib import Path -from typing import Awaitable, List, Optional, Union +from typing import Awaitable, List, Optional, Union, cast from fastapi import Depends, FastAPI, File, Form, Header, HTTPException, Query, Request, Response, UploadFile, status from fastapi.middleware.cors import CORSMiddleware @@ -32,35 +32,37 @@ from .minio import create_bucket_if_not_exists, delete_object_data, get_object_data, post_object_data from .models import Paste from .schema import HealthErrorResponse, HealthResponse, PasteCreate, PasteDetails, PasteResponse -from .utils import _filter_object_name_from_link, extract_uuid +from .utils import extract_uuid # -------------------------------------------------------------------- # Logger # -------------------------------------------------------------------- -dictConfig(LogConfig()) +dictConfig(LogConfig().model_dump()) logger = logging.getLogger("paste") -# -------------------------------------------------------------------- +# -------------------------------------------------i------------------- # Background task to check and delete expired URLs # -------------------------------------------------------------------- async def delete_expired_urls() -> None: while True: + db: Optional[Session] = None try: - db: Session = Session_Local() + db = Session_Local() current_time = datetime.utcnow() # Find and delete expired URLs - expired_urls = db.query(Paste).filter(Paste.expiresat <= current_time).all() + expired_urls: List[Paste] = db.query(Paste).filter(Paste.expiresat <= current_time).all() for url in expired_urls: - if url.s3_link: + if url.s3_link is not None: try: - object_name = _filter_object_name_from_link(url.s3_link) + # using cast to to tell the type checker to treat it as an str + object_name: str = cast(str, url.s3_link) delete_object_data(object_name) except Exception as s3_err: logger.error(f"Failed to delete S3 object for expired paste {url.pasteID}: {s3_err}") @@ -72,8 +74,8 @@ async def delete_expired_urls() -> None: logger.error(f"Error in deletion task: {e}") finally: - db.close() - + if db is not None: + db.close() # Check every minute await asyncio.sleep(60) @@ -129,13 +131,10 @@ async def custom_http_exception_handler(request: Request, exc: StarletteHTTPExce # Startup event to begin background task +# TODO: migrate this to lifespan event from on_event @app.on_event("startup") async def startup_event(): asyncio.create_task(delete_expired_urls()) - - -@app.on_event("startup") -async def startup_event(): create_bucket_if_not_exists() @@ -165,7 +164,8 @@ async def startup_event(): @app.get("/", response_class=HTMLResponse) @limiter.limit("100/minute") async def indexpage(request: Request) -> Response: - logger.debug(f"Received request from {request.client.host}") + client_host = request.client.host if request.client is not None else "unknown" + logger.debug(f"Received request from {client_host}") logger.info(f"Hit at home page - Method: {request.method}") return templates.TemplateResponse("index.html", {"request": request}) @@ -216,17 +216,26 @@ async def get_paste_data( try: uuid = extract_uuid(uuid) - data = db.query(Paste).filter(Paste.pasteID == uuid).first() + data: Optional[Paste] = db.query(Paste).filter(Paste.pasteID == uuid).first() content: Optional[str] = None extension: Optional[str] = None - if not data.s3_link: - content = data.content - extension = data.extension + if data is None: + raise HTTPException(detail="Paste not found", status_code=status.HTTP_404_NOT_FOUND) + + if data.s3_link is not None: + content = get_object_data(cast(str, data.s3_link)) + extension = cast(str, data.extension) else: - content = get_object_data(data.s3_link) - extension = data.extension + content = cast(str, data.content) + extension = cast(str, data.extension) + + if content is None: + raise HTTPException(detail="Paste content is unavailable", status_code=status.HTTP_404_NOT_FOUND) + + if extension is None: + extension = "" extension = extension[1::] if extension.startswith(".") else extension @@ -321,7 +330,7 @@ async def post_as_a_file( ) content = await file.read() - file_content = content.decode("utf-8") + file_content: str = content.decode("utf-8") if len(content) > 102400: s3_link: str = post_object_data(file_content) @@ -356,10 +365,11 @@ async def delete_paste(uuid: str, db: Session = Depends(get_db)) -> PlainTextRes uuid = extract_uuid(uuid) try: data = db.query(Paste).filter(Paste.pasteID == uuid).first() - if data: - if data.s3_link: + if data is not None: + if data.s3_link is not None: try: - object_name = _filter_object_name_from_link(data.s3_link) + # using cast to ensure data.s3_link is a str before passing to delete_object_data + object_name = cast(str, data.s3_link) delete_object_data(object_name) except Exception as s3_err: logger.error(f"Failed to delete S3 object for paste {uuid}: {s3_err}") @@ -475,8 +485,8 @@ async def get_paste_details(request: Request, uuid: str, db: Session = Depends(g return JSONResponse( content=PasteDetails( uuid=uuid, - content=data.content, - extension=data.extension, + content=cast(str, data.content), + extension=cast(str, data.extension), ).model_dump(), status_code=status.HTTP_200_OK, ) @@ -535,7 +545,7 @@ async def create_paste(request: Request, paste: PasteCreate, db: Session = Depen db.refresh(file) _uuid = file.pasteID return JSONResponse( - content=PasteResponse(uuid=_uuid, url=f"{BASE_URL}/paste/{_uuid}").model_dump(), + content=PasteResponse(uuid=cast(str, _uuid), url=f"{BASE_URL}/paste/{_uuid}").model_dump(), status_code=status.HTTP_201_CREATED, ) else: @@ -549,7 +559,7 @@ async def create_paste(request: Request, paste: PasteCreate, db: Session = Depen db.refresh(file) _uuid = file.pasteID return JSONResponse( - content=PasteResponse(uuid=_uuid, url=f"{BASE_URL}/paste/{_uuid}").model_dump(), + content=PasteResponse(uuid=cast(str, _uuid), url=f"{BASE_URL}/paste/{_uuid}").model_dump(), status_code=status.HTTP_201_CREATED, ) except HTTPException: From c2a0c19a47a720fb3d64ffd7efde0a0322b5326e Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Wed, 8 Apr 2026 02:36:11 +0530 Subject: [PATCH 08/10] fix: import error for alembic --- alembic/env.py | 19 ++++++++++--------- docker-entrypoint.sh | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/alembic/env.py b/alembic/env.py index 371975e..15b6d68 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -3,20 +3,23 @@ import sys from logging.config import fileConfig -from sqlalchemy import engine_from_config -from sqlalchemy import pool +from sqlalchemy import engine_from_config, pool from alembic import context -# Add the src directory to Python path +# Add the src directory to Python path explicitly before importing app modules current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) src_dir = os.path.join(current_dir, "src") -sys.path.append(src_dir) +if src_dir not in sys.path: + sys.path.insert(0, src_dir) -# Import your models and Base -from paste.database import Base +# Import your models and Base after path setup +from paste.config import get_settings # noqa: E402 +from paste.database import Base # noqa: E402 + +# Import all your models here so Base.metadata is populated +from paste.models import Paste # noqa: F401, E402 -# Import all your models here # this is the Alembic Config object config = context.config @@ -24,8 +27,6 @@ if config.config_file_name is not None: fileConfig(config.config_file_name) -from paste.config import get_settings - config.set_main_option("sqlalchemy.url", get_settings().SQLALCHEMY_DATABASE_URL) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 43a392f..903adc3 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -5,4 +5,4 @@ set -e pdm run migrate # Execute the main command -exec "$@" \ No newline at end of file +exec "$@" From 35194cc881097c24fb4b5ffe51178afa280dd7ce Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Wed, 8 Apr 2026 02:36:46 +0530 Subject: [PATCH 09/10] fix: minor issues and add makefile --- Dockerfile | 24 +++++++------- Makefile | 33 ++++++++++++++++++++ docker-compose.yml => dev/docker-compose.yml | 11 ++----- docker-entrypoint.sh | 4 +-- src/paste/main.py | 2 -- 5 files changed, 51 insertions(+), 23 deletions(-) create mode 100644 Makefile rename docker-compose.yml => dev/docker-compose.yml (86%) diff --git a/Dockerfile b/Dockerfile index 955828a..06551d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,24 @@ -# Dockerfile - # pull the official docker image -FROM python:3.11.3-slim AS builder +FROM python:3.11.3-slim # install PDM -RUN pip install -U pip setuptools wheel -RUN pip install pdm +RUN pip install -U pip setuptools wheel && \ + pip install pdm -# copy files -COPY pyproject.toml pdm.lock README.md /project/ -COPY . /project/ +WORKDIR /project +# copy dependency files first for better layer caching +COPY pyproject.toml pdm.lock README.md ./ -WORKDIR /project +# install dependencies (this layer is cached unless lock file changes) +RUN pdm install --no-self -RUN pdm install -RUN chmod +x docker-entrypoint.sh +# copy the rest of the source code +COPY . . +RUN chmod +x /project/docker-entrypoint.sh EXPOSE 8080 + +ENTRYPOINT ["/project/docker-entrypoint.sh"] CMD ["pdm", "run", "start"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..baddd94 --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +.PHONY: help sync run dev test make-migration migrate docker-dev docker-dev-down docker-dev-build + +.DEFAULT_GOAL := help + +help: ## Show this help message + @awk 'BEGIN {FS = ":.*##"; printf "Usage:\n make \033[36m\033[0m\n\nTargets:\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) + +sync: pyproject.toml ## Install dependencies via pdm sync + pdm sync + +run: pyproject.toml ## Run the application + pdm run start + +dev: pyproject.toml ## Run the application in development mode + pdm run dev + +test: pyproject.toml ## Run the test suite + pdm run test + +make-migration: pyproject.toml ## Generate a new database migration + pdm run make_migration + +migrate: pyproject.toml ## Apply database migrations + pdm run migrate + +docker-dev-run: dev/docker-compose.yml ## Start development containers + docker-compose -f dev/docker-compose.yml up + +docker-dev-down: dev/docker-compose.yml ## Stop development containers + docker-compose -f dev/docker-compose.yml down + +docker-dev-build: dev/docker-compose.yml ## Build development containers + docker-compose -f dev/docker-compose.yml build diff --git a/docker-compose.yml b/dev/docker-compose.yml similarity index 86% rename from docker-compose.yml rename to dev/docker-compose.yml index d554014..65b44e4 100644 --- a/docker-compose.yml +++ b/dev/docker-compose.yml @@ -39,16 +39,10 @@ services: - data2:/data2 - data3:/data3 - data4:/data4 - # healthcheck: - # test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - # interval: 30s - # timeout: 10s - # retries: 5 - # start_period: 20s myapp: build: - context: . + context: ../ dockerfile: Dockerfile environment: MINIO_CLIENT_LINK: http://rustfs:9000 @@ -59,11 +53,12 @@ services: SQLALCHEMY_DATABASE_URL: postgresql://postgres:mytestpassword@postgres:5432/pastedb ports: - "8082:8080" - entrypoint: ["./docker-entrypoint.sh"] + entrypoint: ["/project/docker-entrypoint.sh"] command: ["pdm", "run", "dev"] depends_on: postgres: condition: service_healthy + volumes: postgres_data: data1: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 903adc3..225b017 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,8 +1,8 @@ #!/bin/sh set -e -# Run migrations +echo "Running database migrations..." pdm run migrate -# Execute the main command +echo "Starting application..." exec "$@" diff --git a/src/paste/main.py b/src/paste/main.py index 5206640..d14702b 100644 --- a/src/paste/main.py +++ b/src/paste/main.py @@ -21,8 +21,6 @@ from sqlalchemy import text from sqlalchemy.orm import Session from starlette.exceptions import HTTPException as StarletteHTTPException -from starlette.requests import Request -from starlette.responses import Response from . import __author__, __contact__, __url__, __version__ from .config import get_settings From fdc99cb0f41e74ebd82e017c39e49cf1aee19947 Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Wed, 8 Apr 2026 02:41:48 +0530 Subject: [PATCH 10/10] fix: tests and ruff error in pre-commit --- tests/test_api.py | 72 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index f8a6b41..07f7357 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,26 +1,66 @@ +from collections.abc import Generator + from fastapi.testclient import TestClient -from src.paste.main import app -from typing import Optional +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import StaticPool + +from src.paste.database import Base +from src.paste.main import app, get_db + +SQLALCHEMY_DATABASE_URL = "sqlite://" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False}, + poolclass=StaticPool, +) +TestingSessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False) + + +def override_get_db() -> Generator: + db = TestingSessionLocal() + try: + yield db + finally: + db.close() + + +def setup_module() -> None: + Base.metadata.create_all(bind=engine) + app.dependency_overrides[get_db] = override_get_db + + +def teardown_module() -> None: + app.dependency_overrides.clear() + Base.metadata.drop_all(bind=engine) -client: TestClient = TestClient(app) -paste_id: Optional[str] = None +def test_get_health_route(monkeypatch) -> None: + monkeypatch.setattr("src.paste.main.create_bucket_if_not_exists", lambda: None) + with TestClient(app) as client: + response = client.get("/health") -def test_get_health_route() -> None: - response = client.get("/health") assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "ok" + assert payload["database"] == "connected" + assert "db_response_time_ms" in payload -def test_paste_api_route() -> None: - respose = client.post( - "/api/paste", - json={ - "content": "Hello-World", - }, - ) - paste_id = respose.text - assert respose.status_code == 201 +def test_paste_api_route(monkeypatch) -> None: + monkeypatch.setattr("src.paste.main.create_bucket_if_not_exists", lambda: None) + with TestClient(app) as client: + response = client.post( + "/api/paste", + json={ + "content": "Hello-World", + }, + ) -print(paste_id) + assert response.status_code == 201 + payload = response.json() + assert "uuid" in payload + assert payload["url"].endswith(payload["uuid"])