diff --git a/.env.local b/.env.local deleted file mode 100644 index e9d7637..0000000 --- a/.env.local +++ /dev/null @@ -1,9 +0,0 @@ -MODE=local -MONGO_URI=mongodb://:@127.0.0.1:27017/?authSource=admin&retryWrites=true&w=majority -DOMAIN=https://rzro.link -PORT=8001 -API_VERSION="" -APP_NAMe="LOCAL" - -MONGO_URI="mongodb://@:127.0.0.1:27017/?retryWrites=true&w=majority&appName=" -DATABASE_NAME=":@127.0.0.1:27017/?authSource=admin&retryWrites=true&w=majority +DOMAIN=https://localhost:8001 +PORT=8001 +API_VERSION="" +APP_NAMe="LOCAL" \ No newline at end of file diff --git a/.githooks/post-checkout b/.githooks/post-checkout index 0da5c19..23ecd57 100644 --- a/.githooks/post-checkout +++ b/.githooks/post-checkout @@ -1,14 +1,14 @@ -#!/bin/bash -set -e - -# DRY RUN GUARD -if [ "$DRY_RUN" = "1" ]; then - echo "[DRY RUN] Hook executed: $(basename "$0")" - exit 0 -fi - -git diff --name-only HEAD@{1} HEAD | grep -q "poetry.lock" || exit 0 - -echo "[post-checkout] regenerating requirements.txt" - -poetry export -f requirements.txt -o requirements.txt --without-hashes +#!/bin/bash +set -e + +# DRY RUN GUARD +if [ "$DRY_RUN" = "1" ]; then + echo "[DRY RUN] Hook executed: $(basename "$0")" + exit 0 +fi + +git diff --name-only HEAD@{1} HEAD | grep -q "poetry.lock" || exit 0 + +echo "[post-checkout] regenerating requirements.txt" + +poetry export -f requirements.txt -o requirements.txt --without-hashes diff --git a/.githooks/post-merge b/.githooks/post-merge index b2e9b63..459056a 100644 --- a/.githooks/post-merge +++ b/.githooks/post-merge @@ -1,12 +1,12 @@ -#!/bin/bash -set -e - -# DRY RUN GUARD -if [ "$DRY_RUN" = "1" ]; then - echo "[DRY RUN] Hook executed: $(basename "$0")" - exit 0 -fi - -git diff --name-only HEAD@{1} HEAD | grep -q "poetry.lock" || exit 0 -echo "[post-merge] regenerating requirements.txt" -poetry export -f requirements.txt -o requirements.txt --without-hashes +#!/bin/bash +set -e + +# DRY RUN GUARD +if [ "$DRY_RUN" = "1" ]; then + echo "[DRY RUN] Hook executed: $(basename "$0")" + exit 0 +fi + +git diff --name-only HEAD@{1} HEAD | grep -q "poetry.lock" || exit 0 +echo "[post-merge] regenerating requirements.txt" +poetry export -f requirements.txt -o requirements.txt --without-hashes diff --git a/.githooks/pre-commit b/.githooks/pre-commit index c5f23b2..9d23212 100644 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,52 +1,52 @@ -#!/bin/bash - -# ---- DRY RUN MODE ---- -if [ "$DRY_RUN" = "1" ]; then - echo "[DRY RUN] Hook executed: $(basename "$0")" - exit 0 -fi -# --------------------- -# Step 1: Validate branch naming convention -BRANCH_NAME=$(git symbolic-ref --short HEAD) -echo "Current branch: $BRANCH_NAME" - -# Define the branches to exclude -EXCLUDED_BRANCHES="develop main release" - -# Check if the current branch is in the excluded list -for EXCLUDED_BRANCH in $EXCLUDED_BRANCHES; do - if [ "$BRANCH_NAME" = "$EXCLUDED_BRANCH" ]; then - echo "Skipping pre-commit checks for branch: $BRANCH_NAME" - exit 0 - fi -done - -# Pre-commit checks (e.g., linting, testing, etc.) -echo "Running pre-commit checks for branch: $BRANCH_NAME" - -echo "$BRANCH_NAME" | grep -Eq '^(feature|bugfix|hotfix)/[A-Za-z]+-[0-9]+' -if [ $? -ne 0 ]; then - echo "Error: Branch name '$BRANCH_NAME' is invalid." - echo "Use a valid branch naming convention, e.g., feature/XYZ-123-description." - exit 1 -fi - -# Step 2: Run poetry linting tools -echo "Running poetry linting tools..." -if ! poetry run flake8 .; then - echo "flake8 failed. Aborting commit." - exit 1 -fi - -if ! poetry run black .; then - echo "black failed. Aborting commit." - exit 1 -fi - -#if ! poetry run mypy .; then -# echo "mypy failed. Aborting commit." -# exit 1 -#fi - -echo "Pre-commit checks passed!" -exit 0 +#!/bin/bash + +# ---- DRY RUN MODE ---- +if [ "$DRY_RUN" = "1" ]; then + echo "[DRY RUN] Hook executed: $(basename "$0")" + exit 0 +fi +# --------------------- +# Step 1: Validate branch naming convention +BRANCH_NAME=$(git symbolic-ref --short HEAD) +echo "Current branch: $BRANCH_NAME" + +# Define the branches to exclude +EXCLUDED_BRANCHES="develop main release" + +# Check if the current branch is in the excluded list +for EXCLUDED_BRANCH in $EXCLUDED_BRANCHES; do + if [ "$BRANCH_NAME" = "$EXCLUDED_BRANCH" ]; then + echo "Skipping pre-commit checks for branch: $BRANCH_NAME" + exit 0 + fi +done + +# Pre-commit checks (e.g., linting, testing, etc.) +echo "Running pre-commit checks for branch: $BRANCH_NAME" + +echo "$BRANCH_NAME" | grep -Eq '^(feature|bugfix|hotfix)/[A-Za-z]+-[0-9]+' +if [ $? -ne 0 ]; then + echo "Error: Branch name '$BRANCH_NAME' is invalid." + echo "Use a valid branch naming convention, e.g., feature/XYZ-123-description." + exit 1 +fi + +# Step 2: Run poetry linting tools +echo "Running poetry linting tools..." +if ! poetry run flake8 .; then + echo "flake8 failed. Aborting commit." + exit 1 +fi + +if ! poetry run black .; then + echo "black failed. Aborting commit." + exit 1 +fi + +#if ! poetry run mypy .; then +# echo "mypy failed. Aborting commit." +# exit 1 +#fi + +echo "Pre-commit checks passed!" +exit 0 diff --git a/.githooks/prepare-commit-msg b/.githooks/prepare-commit-msg index 2ff1237..613ecef 100644 --- a/.githooks/prepare-commit-msg +++ b/.githooks/prepare-commit-msg @@ -1,17 +1,17 @@ -#!/bin/bash - -# This script prepends the branch name to the commit message -# Skip certain branches (configurable) -if [ -z "$BRANCHES_TO_SKIP" ]; then - BRANCHES_TO_SKIP="master develop release" -fi - -BRANCH_NAME=$(git symbolic-ref --short HEAD) -BRANCH_NAME="${BRANCH_NAME##*/}" - -BRANCH_EXCLUDED=$(printf "%s\n" $BRANCHES_TO_SKIP | grep -c "^$BRANCH_NAME$") -BRANCH_IN_COMMIT=$(grep -c "\[$BRANCH_NAME\]" "$1") - -if [ -n "$BRANCH_NAME" ] && ! [ $BRANCH_EXCLUDED -eq 1 ] && ! [ $BRANCH_IN_COMMIT -ge 1 ]; then - sed -i.bak -e "1s/^/[$BRANCH_NAME]: /" "$1" -fi +#!/bin/bash + +# This script prepends the branch name to the commit message +# Skip certain branches (configurable) +if [ -z "$BRANCHES_TO_SKIP" ]; then + BRANCHES_TO_SKIP="master develop release" +fi + +BRANCH_NAME=$(git symbolic-ref --short HEAD) +BRANCH_NAME="${BRANCH_NAME##*/}" + +BRANCH_EXCLUDED=$(printf "%s\n" $BRANCHES_TO_SKIP | grep -c "^$BRANCH_NAME$") +BRANCH_IN_COMMIT=$(grep -c "\[$BRANCH_NAME\]" "$1") + +if [ -n "$BRANCH_NAME" ] && ! [ $BRANCH_EXCLUDED -eq 1 ] && ! [ $BRANCH_IN_COMMIT -ge 1 ]; then + sed -i.bak -e "1s/^/[$BRANCH_NAME]: /" "$1" +fi diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 29a3fb8..5cf06b4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,6 @@ - -* @recursivezero - -# You can also use email addresses if you prefer - -docs/* recursivelyzero@gmail.com + +* @recursivezero + +# You can also use email addresses if you prefer + +docs/* recursivelyzero@gmail.com diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index e98ea5f..156bb8f 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -1,153 +1,153 @@ -# Contributor Covenant Code of Conduct - -**Table of Contents:** - -- [Contributor Covenant Code of Conduct](#contributor-covenant-code-of-conduct) - - [Summary](#summary) - - [Our Pledge](#our-pledge) - - [Our Standards](#our-standards) - - [Enforcement Responsibilities](#enforcement-responsibilities) - - [Scope](#scope) - - [Enforcement](#enforcement) - - [Enforcement Guidelines](#enforcement-guidelines) - - [1. Correction](#1-correction) - - [2. Warning](#2-warning) - - [3. Temporary Ban](#3-temporary-ban) - - [4. Permanent Ban](#4-permanent-ban) - - [Attribution](#attribution) - -**Version**: 1.0.0 - -## Summary - -As contributors and maintainers of this projects, we will respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities. - -Communication regarding the projects through any channel must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. Courtesy and respect shall be extended to -everyone involved in this project. Our experiences as individuals differs widely, and as such contributors are expected to be respectful of differing viewpoints and ideas. - -We expect all contributors to uphold our standards of conduct. If any member of the community violates this code of conduct, the Embedded Artistry team and project maintainers will take action. We reserve the right to remove issues, comments, -and PRs that do not comply with our conduct standards. Repeated or significant offenses will result in blocked accounts and disassociation with our projects and the Embedded Artistry community. - -If you are subject to or witness unacceptable behavior, or have any other concerns, please [email us][contact]. - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity -and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -- Demonstrating empathy and kindness toward other people -- Being respectful of differing opinions, viewpoints, and experiences -- Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -- Focusing on what is best not just for us as individuals, but for the - overall community - -Examples of unacceptable behavior include: - -- The use of sexualized language or imagery, and sexual attention or - advances of any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or email - address, without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -[_this email_][contact]. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -> [!CAUTION] -> Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series -of actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or -permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within -the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.0, available at . - -Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct -see the FAQ at . -Translations are available at . - -[contact]: mailto:recursivezero@outlook.com +# Contributor Covenant Code of Conduct + +**Table of Contents:** + +- [Contributor Covenant Code of Conduct](#contributor-covenant-code-of-conduct) + - [Summary](#summary) + - [Our Pledge](#our-pledge) + - [Our Standards](#our-standards) + - [Enforcement Responsibilities](#enforcement-responsibilities) + - [Scope](#scope) + - [Enforcement](#enforcement) + - [Enforcement Guidelines](#enforcement-guidelines) + - [1. Correction](#1-correction) + - [2. Warning](#2-warning) + - [3. Temporary Ban](#3-temporary-ban) + - [4. Permanent Ban](#4-permanent-ban) + - [Attribution](#attribution) + +**Version**: 1.0.0 + +## Summary + +As contributors and maintainers of this projects, we will respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities. + +Communication regarding the projects through any channel must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. Courtesy and respect shall be extended to +everyone involved in this project. Our experiences as individuals differs widely, and as such contributors are expected to be respectful of differing viewpoints and ideas. + +We expect all contributors to uphold our standards of conduct. If any member of the community violates this code of conduct, the Embedded Artistry team and project maintainers will take action. We reserve the right to remove issues, comments, +and PRs that do not comply with our conduct standards. Repeated or significant offenses will result in blocked accounts and disassociation with our projects and the Embedded Artistry community. + +If you are subject to or witness unacceptable behavior, or have any other concerns, please [email us][contact]. + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity +and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[_this email_][contact]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +> [!CAUTION] +> Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at . + +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct +see the FAQ at . +Translations are available at . + +[contact]: mailto:recursivezero@outlook.com diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index c01230e..fb64b0c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,199 +1,199 @@ -# Contribute - -This article explains how to contribute to project. Please read through the following guidelines. - -Write something nice and instructive as an intro. Talk about what kind of contributions you are interested in. - -> Welcome! We love receiving contributions from our community, so thanks for stopping by! There are many ways to contribute, including submitting bug reports, improving documentation, submitting feature requests, reviewing new submissions, -> or contributing code that can be incorporated into the project. - -## Summary - -> [!Note] -> Before participating in our community, please read our [code of conduct][coc]. -> By interacting with this repository, organization, or community you agree to abide by its terms. - -This document describes our development process. Following these guidelines shows that you respect the time and effort of the developers managing this project. In return, you will be shown respect in addressing your issue, -reviewing your changes, and incorporating your contributions. - -## Contributions - -There’s several ways to contribute, not just by writing code. If you have questions, see [support][support]. - -### Financial support - -It’s possible to support us financially by becoming a backer or sponsor through [Open Collective][collective]. - -### Improve docs - -As a user you’re perfect for helping us improve our docs.Typo corrections, error fixes, better explanations, new examples, etcetera. - -### Improve issues - -Some issues lack information, aren’t reproducible, or are just incorrect. You can help by trying to make them easier to resolve. -Existing issues might benefit from your unique experience or opinions. - -### Write code - -Code contributions are very welcome. -It’s probably a good idea to first post a question or open an issue to report a bug or suggest a new feature before creating a pull request. - -## Submitting an issue - -- The issue tracker is for issues. Use discussions for support -- Search the issue tracker (including closed issues) before opening a new issue -- Ensure you’re using the latest version of our packages -- Use a clear and descriptive title -- Include as much information as possible: steps to reproduce the issue, error message, version, operating system, etcetera -- The more time you put into an issue, the better we will be able to help you -- The best issue report is a proper reproduction step to prove it - -## Development Process - -What is your development process? - -> [!Tip] -> This project follows the basic git glow - -Check and follow [README][readme] file and run on your local. - -Talk about branches people should work on. Specifically, where is the starting point? `main`, `feature`, `hotfix` `task` etc. - -### Testing - -If you add code you need to add tests! We’ve learned the hard way that code without tests is undependable. If your pull request reduces our test coverage because it lacks tests then it will be rejected. - -Provide instructions for adding new tests. Provide instructions for running tests. - -```sh -npm run test -``` - -### Style Guidelines - -run below command - -```sh -npm run lint -``` - -### Code Formatting - -use code formatter in your IDE, add prettier and some other useful extension in your IDE. - -### Git Commit Guidelines - -below are the guidelines for your commit messages. - -- add clear message and with 50 lines -- prefix feature / issue number from issue page - -### Submitting a pull request - -- Run `npm test` locally to build, format, and test your changes -- Non-trivial changes are often best discussed in an issue first, to prevent you from doing unnecessary work -- For ambitious tasks, you should try to get your work in front of the community for feedback as soon as possible -- New features should be accompanied by tests and documentation -- Don’t include unrelated changes -- Test before submitting code by running `npm test` -- Write a convincing description of why we should land your pull request: it’s your job to convince us - -## Pull Request Process - -Add notes for pushing your branch: - -When you are ready to generate a pull request, either for preliminary review, or for consideration of merging into the project you must first push your local topic branch back up to GitHub: - -```sh -git push origin feature/branch-name -``` - -Include a note about submitting the PR: - -Once you've committed and pushed all of your changes to GitHub, go to the page for your fork on GitHub, select your development branch, and click the pull request button. -If you need to make any adjustments to your pull request, just push the updates to your branch. Your pull request will automatically track the changes on your development branch and update. - -1. Ensure any install or build dependencies are removed before the end of the layer when doing a build. -2. Update the README.md with details of changes to the interface, this includes new environment variables, exposed ports, useful file locations and container parameters. -3. Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). -4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you do not have permission to do that, you may request the second reviewer to merge it for you. - -### Review Process - -Who reviews it? Who needs to sign off before it’s accepted? When should a contributor expect to hear from you? How can contributors get commit access, if at all? - -- The core team looks at Pull Requests on a regular basis in a weekly triage meeting that we hold in a public domain. The is announced in the weekly status updates. -- Our Reviewer will provide constructive Feedback by writing Review Comments (RC). Pull Requester have to address all RC on time. -- After feedback has been given we expect responses within two weeks. After two weeks we may close the pull request if it isn't showing any activity. -- Except for critical, urgent or very small fixes, we try to leave pull requests open for most of the day or overnight if something comes in late in the day, so that multiple people have the chance to review/comment. - Anyone who reviews a pull request should leave a note to let others know that someone has looked at it. For larger commits, we like to have a +1 from someone else on the core team and/or from other contributor(s). - Please note if you reviewed the code or tested locally -- a +1 by itself will typically be interpreted as your thinking its a good idea, but not having reviewed in detail. - -Perhaps also provide the steps your team will use for checking a PR. Or discuss the steps run on your CI server if you have one. This will help developers understand how to investigate any failures or test the process on their own. - -### Addressing Feedback - -Once a PR has been submitted, your changes will be reviewed and constructive feedback may be provided. Feedback isn't meant as an attack, but to help make sure the highest-quality code makes it into our project. -Changes will be approved once required feedback has been addressed. - -If a maintainer asks you to "rebase" your PR, they're saying that a lot of code has changed, and that you need to update your fork so it's easier to merge. - -To update your forked repository, follow these steps: - -### Fetch upstream master and merge with your repo's main branch - -```sh -git fetch upstream -git checkout main -git merge upstream/main -``` - -#### If there were any new commits, rebase your development branch - -```sh -git checkout feature/branch-name -git rebase main -``` - -If too much code has changed for git to automatically apply your branches changes to the new master, you will need to manually resolve the merge conflicts yourself. - -Once your new branch has no conflicts and works correctly, you can override your old branch using this command: - -```sh -git push origin feature/branch-name -``` - -Note that this will overwrite the old branch on the server, so make sure you are happy with your changes first! - -## Community - -Do you have a mailing list, Google group, slack channel, IRC channel? Link to them here. - -Include Other Notes on how people can contribute - -- You can help us answer questions our users have here: -- You can help build and design our website here: -- You can help write blog posts about the project by: -- You can help with newsletters and internal communications by: - -- Create an example of the project in real world by building something or showing what others have built. -- Write about other people’s projects based on this project. Show how it’s used in daily life. Take screenshots and make videos! - -## Resources - -- [How to contribute to open source](https://opensource.guide/how-to-contribute/) -- [Making your first contribution](https://medium.com/@vadimdemedes/making-your-first-contribution-de6576ddb190) -- [Using pull requests](https://help.github.com/articles/about-pull-requests/) -- [GitHub help](https://help.github.com) -- [git commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) - -## Author - -© RecursiveZero Private Limited - - - -[collective]: https://opencollective.com/recursivezero -[readme]: https://github.com/recursivezero/tiny/blob/main/README.md -[support]: https://github.com/recursivezero/tiny/blob/main/.github/SUPPORT.md -[coc]: https://github.com/recursivezero/tiny/blob/main/.github/CODE_OF_CONDUCT.md +# Contribute + +This article explains how to contribute to project. Please read through the following guidelines. + +Write something nice and instructive as an intro. Talk about what kind of contributions you are interested in. + +> Welcome! We love receiving contributions from our community, so thanks for stopping by! There are many ways to contribute, including submitting bug reports, improving documentation, submitting feature requests, reviewing new submissions, +> or contributing code that can be incorporated into the project. + +## Summary + +> [!Note] +> Before participating in our community, please read our [code of conduct][coc]. +> By interacting with this repository, organization, or community you agree to abide by its terms. + +This document describes our development process. Following these guidelines shows that you respect the time and effort of the developers managing this project. In return, you will be shown respect in addressing your issue, +reviewing your changes, and incorporating your contributions. + +## Contributions + +There’s several ways to contribute, not just by writing code. If you have questions, see [support][support]. + +### Financial support + +It’s possible to support us financially by becoming a backer or sponsor through [Open Collective][collective]. + +### Improve docs + +As a user you’re perfect for helping us improve our docs.Typo corrections, error fixes, better explanations, new examples, etcetera. + +### Improve issues + +Some issues lack information, aren’t reproducible, or are just incorrect. You can help by trying to make them easier to resolve. +Existing issues might benefit from your unique experience or opinions. + +### Write code + +Code contributions are very welcome. +It’s probably a good idea to first post a question or open an issue to report a bug or suggest a new feature before creating a pull request. + +## Submitting an issue + +- The issue tracker is for issues. Use discussions for support +- Search the issue tracker (including closed issues) before opening a new issue +- Ensure you’re using the latest version of our packages +- Use a clear and descriptive title +- Include as much information as possible: steps to reproduce the issue, error message, version, operating system, etcetera +- The more time you put into an issue, the better we will be able to help you +- The best issue report is a proper reproduction step to prove it + +## Development Process + +What is your development process? + +> [!Tip] +> This project follows the basic git glow + +Check and follow [README][readme] file and run on your local. + +Talk about branches people should work on. Specifically, where is the starting point? `main`, `feature`, `hotfix` `task` etc. + +### Testing + +If you add code you need to add tests! We’ve learned the hard way that code without tests is undependable. If your pull request reduces our test coverage because it lacks tests then it will be rejected. + +Provide instructions for adding new tests. Provide instructions for running tests. + +```sh +npm run test +``` + +### Style Guidelines + +run below command + +```sh +npm run lint +``` + +### Code Formatting + +use code formatter in your IDE, add prettier and some other useful extension in your IDE. + +### Git Commit Guidelines + +below are the guidelines for your commit messages. + +- add clear message and with 50 lines +- prefix feature / issue number from issue page + +### Submitting a pull request + +- Run `npm test` locally to build, format, and test your changes +- Non-trivial changes are often best discussed in an issue first, to prevent you from doing unnecessary work +- For ambitious tasks, you should try to get your work in front of the community for feedback as soon as possible +- New features should be accompanied by tests and documentation +- Don’t include unrelated changes +- Test before submitting code by running `npm test` +- Write a convincing description of why we should land your pull request: it’s your job to convince us + +## Pull Request Process + +Add notes for pushing your branch: + +When you are ready to generate a pull request, either for preliminary review, or for consideration of merging into the project you must first push your local topic branch back up to GitHub: + +```sh +git push origin feature/branch-name +``` + +Include a note about submitting the PR: + +Once you've committed and pushed all of your changes to GitHub, go to the page for your fork on GitHub, select your development branch, and click the pull request button. +If you need to make any adjustments to your pull request, just push the updates to your branch. Your pull request will automatically track the changes on your development branch and update. + +1. Ensure any install or build dependencies are removed before the end of the layer when doing a build. +2. Update the README.md with details of changes to the interface, this includes new environment variables, exposed ports, useful file locations and container parameters. +3. Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). +4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you do not have permission to do that, you may request the second reviewer to merge it for you. + +### Review Process + +Who reviews it? Who needs to sign off before it’s accepted? When should a contributor expect to hear from you? How can contributors get commit access, if at all? + +- The core team looks at Pull Requests on a regular basis in a weekly triage meeting that we hold in a public domain. The is announced in the weekly status updates. +- Our Reviewer will provide constructive Feedback by writing Review Comments (RC). Pull Requester have to address all RC on time. +- After feedback has been given we expect responses within two weeks. After two weeks we may close the pull request if it isn't showing any activity. +- Except for critical, urgent or very small fixes, we try to leave pull requests open for most of the day or overnight if something comes in late in the day, so that multiple people have the chance to review/comment. + Anyone who reviews a pull request should leave a note to let others know that someone has looked at it. For larger commits, we like to have a +1 from someone else on the core team and/or from other contributor(s). + Please note if you reviewed the code or tested locally -- a +1 by itself will typically be interpreted as your thinking its a good idea, but not having reviewed in detail. + +Perhaps also provide the steps your team will use for checking a PR. Or discuss the steps run on your CI server if you have one. This will help developers understand how to investigate any failures or test the process on their own. + +### Addressing Feedback + +Once a PR has been submitted, your changes will be reviewed and constructive feedback may be provided. Feedback isn't meant as an attack, but to help make sure the highest-quality code makes it into our project. +Changes will be approved once required feedback has been addressed. + +If a maintainer asks you to "rebase" your PR, they're saying that a lot of code has changed, and that you need to update your fork so it's easier to merge. + +To update your forked repository, follow these steps: + +### Fetch upstream master and merge with your repo's main branch + +```sh +git fetch upstream +git checkout main +git merge upstream/main +``` + +#### If there were any new commits, rebase your development branch + +```sh +git checkout feature/branch-name +git rebase main +``` + +If too much code has changed for git to automatically apply your branches changes to the new master, you will need to manually resolve the merge conflicts yourself. + +Once your new branch has no conflicts and works correctly, you can override your old branch using this command: + +```sh +git push origin feature/branch-name +``` + +Note that this will overwrite the old branch on the server, so make sure you are happy with your changes first! + +## Community + +Do you have a mailing list, Google group, slack channel, IRC channel? Link to them here. + +Include Other Notes on how people can contribute + +- You can help us answer questions our users have here: +- You can help build and design our website here: +- You can help write blog posts about the project by: +- You can help with newsletters and internal communications by: + +- Create an example of the project in real world by building something or showing what others have built. +- Write about other people’s projects based on this project. Show how it’s used in daily life. Take screenshots and make videos! + +## Resources + +- [How to contribute to open source](https://opensource.guide/how-to-contribute/) +- [Making your first contribution](https://medium.com/@vadimdemedes/making-your-first-contribution-de6576ddb190) +- [Using pull requests](https://help.github.com/articles/about-pull-requests/) +- [GitHub help](https://help.github.com) +- [git commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) + +## Author + +© RecursiveZero Private Limited + + + +[collective]: https://opencollective.com/recursivezero +[readme]: https://github.com/recursivezero/tiny/blob/main/README.md +[support]: https://github.com/recursivezero/tiny/blob/main/.github/SUPPORT.md +[coc]: https://github.com/recursivezero/tiny/blob/main/.github/CODE_OF_CONDUCT.md diff --git a/.github/DISCUSSION_TEMPLATE/announcements.yml b/.github/DISCUSSION_TEMPLATE/announcements.yml index fdb1089..ee9c3fd 100644 --- a/.github/DISCUSSION_TEMPLATE/announcements.yml +++ b/.github/DISCUSSION_TEMPLATE/announcements.yml @@ -1,38 +1,38 @@ -title: "[General] " -labels: ["General Introduction"] -body: - - type: markdown - attributes: - value: | - introduce yourself! - - - type: textarea - id: improvements - attributes: - label: Top 3 improvements - description: "What are the top 3 improvements we could make to this project?" - value: | - 1. - 2. - 3. - ... - render: bash - validations: - required: true - - - type: textarea - id: has-id - attributes: - label: write us suggestion - description: A description about suggestions to help you - validations: - required: true - - - type: checkboxes - id: terms - attributes: - label: Before submitting - description: By submitting this announcement, you agree to follow our [Contributing Guidelines](https://github.com/recursivezero/tiny/blob/main/.github/CONTRIBUTING.md). - options: - - label: I've made research efforts and searched the documentation - required: true +title: "[General] " +labels: ["General Introduction"] +body: + - type: markdown + attributes: + value: | + introduce yourself! + + - type: textarea + id: improvements + attributes: + label: Top 3 improvements + description: "What are the top 3 improvements we could make to this project?" + value: | + 1. + 2. + 3. + ... + render: bash + validations: + required: true + + - type: textarea + id: has-id + attributes: + label: write us suggestion + description: A description about suggestions to help you + validations: + required: true + + - type: checkboxes + id: terms + attributes: + label: Before submitting + description: By submitting this announcement, you agree to follow our [Contributing Guidelines](https://github.com/recursivezero/tiny/blob/main/.github/CONTRIBUTING.md). + options: + - label: I've made research efforts and searched the documentation + required: true diff --git a/.github/DISCUSSION_TEMPLATE/ideas.yml b/.github/DISCUSSION_TEMPLATE/ideas.yml index 64387a9..3df6db8 100644 --- a/.github/DISCUSSION_TEMPLATE/ideas.yml +++ b/.github/DISCUSSION_TEMPLATE/ideas.yml @@ -1,47 +1,47 @@ -title: "[Idea]" -labels: ["Share your Idea"] -body: - - type: textarea - id: idea - attributes: - label: Idea highlight - description: "What are the idea we could make to this project?" - value: - render: bash - validations: - required: true - - - type: dropdown - id: improvement - attributes: - label: Which area of this project could be most improved? - options: - - Documentation - - Pull request review time - - Bug fix time - - Enhancement - validations: - required: true - - - type: input - id: id - attributes: - label: email - description: your contact email - validations: - required: false - - - type: checkboxes - id: terms - attributes: - label: Before submitting - description: By submitting this idea, you agree to follow our [Contributing Guidelines](https://github.com/recursivezero/tiny/blob/main/.github/CONTRIBUTING.md). - options: - - label: I've made research efforts and searched the documentation - required: true - - - type: markdown - attributes: - value: | - ### Thank you - _we will contact you_ **soon** +title: "[Idea]" +labels: ["Share your Idea"] +body: + - type: textarea + id: idea + attributes: + label: Idea highlight + description: "What are the idea we could make to this project?" + value: + render: bash + validations: + required: true + + - type: dropdown + id: improvement + attributes: + label: Which area of this project could be most improved? + options: + - Documentation + - Pull request review time + - Bug fix time + - Enhancement + validations: + required: true + + - type: input + id: id + attributes: + label: email + description: your contact email + validations: + required: false + + - type: checkboxes + id: terms + attributes: + label: Before submitting + description: By submitting this idea, you agree to follow our [Contributing Guidelines](https://github.com/recursivezero/tiny/blob/main/.github/CONTRIBUTING.md). + options: + - label: I've made research efforts and searched the documentation + required: true + + - type: markdown + attributes: + value: | + ### Thank you + _we will contact you_ **soon** diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml index a61018f..c727bf7 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml @@ -1,77 +1,77 @@ -name: Bug Report -description: File a bug report to help us improve. -title: "🐛 " -labels: ["bug"] -assignees: - - "xkeshav" - -body: - - type: textarea - id: problem - attributes: - label: What happened? - description: | - Please provide as much info as possible. - placeholder: Tell us what you see! - value: A bug happened - validations: - required: true - - - type: textarea - id: expected - attributes: - label: What did you expect to happen? - description: | - Please provide expected result/output. - placeholder: Tell us what is expected ! - validations: - required: true - - - type: textarea - id: additional - attributes: - label: Anything else we need to know? - description: | - Please provide other details if it is necessary. - placeholder: Software version and device details! - validations: - required: false - - - type: dropdown - id: browsers - attributes: - label: What browsers are you seeing the problem on? - multiple: true - options: - - Firefox - - Chrome - - Safari - - Microsoft Edge - - Other - - - type: textarea - id: logs - attributes: - label: Relevant log output - description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. - render: shell - validations: - required: false - - - type: input - id: contact - attributes: - label: Contact Details - description: How can we get in touch with you if we need more info? - placeholder: ex. email@example.com - validations: - required: false - - - type: checkboxes - id: terms - attributes: - label: Code of Conduct - description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/recursivezero/tiny/blob/main/README.md). - options: - - label: I agree to follow this project's Code of Conduct - required: true +name: Bug Report +description: File a bug report to help us improve. +title: "🐛 " +labels: ["bug"] +assignees: + - "xkeshav" + +body: + - type: textarea + id: problem + attributes: + label: What happened? + description: | + Please provide as much info as possible. + placeholder: Tell us what you see! + value: A bug happened + validations: + required: true + + - type: textarea + id: expected + attributes: + label: What did you expect to happen? + description: | + Please provide expected result/output. + placeholder: Tell us what is expected ! + validations: + required: true + + - type: textarea + id: additional + attributes: + label: Anything else we need to know? + description: | + Please provide other details if it is necessary. + placeholder: Software version and device details! + validations: + required: false + + - type: dropdown + id: browsers + attributes: + label: What browsers are you seeing the problem on? + multiple: true + options: + - Firefox + - Chrome + - Safari + - Microsoft Edge + - Other + + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: false + + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? + placeholder: ex. email@example.com + validations: + required: false + + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/recursivezero/tiny/blob/main/README.md). + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml b/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml index e209432..7516de1 100644 --- a/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml +++ b/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml @@ -1,22 +1,22 @@ -name: Enhancement Tracking Issue -description: Provide supporting details for a feature in development -title: "🪡 " -labels: [enhancement] -assignees: - - "recursivezero" -body: - - type: textarea - id: feature - attributes: - label: What would you like to be added? - description: | - Feature requests are unlikely to make progress as issues. - validations: - required: true - - - type: textarea - id: rationale - attributes: - label: Why is this needed? - validations: - required: true +name: Enhancement Tracking Issue +description: Provide supporting details for a feature in development +title: "🪡 " +labels: [enhancement] +assignees: + - "recursivezero" +body: + - type: textarea + id: feature + attributes: + label: What would you like to be added? + description: | + Feature requests are unlikely to make progress as issues. + validations: + required: true + + - type: textarea + id: rationale + attributes: + label: Why is this needed? + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md index baaccc7..cfa9954 100644 --- a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md @@ -1,19 +1,19 @@ ---- -name: Feature request -about: "Suggest a feature for this project" -title: "❇️ " -labels: ["enhancement"] -assignees: ["recursivezero"] ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. +--- +name: Feature request +about: "Suggest a feature for this project" +title: "❇️ " +labels: ["enhancement"] +assignees: ["recursivezero"] +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/QUESTION.md b/.github/ISSUE_TEMPLATE/QUESTION.md index a905acc..fbe1e12 100644 --- a/.github/ISSUE_TEMPLATE/QUESTION.md +++ b/.github/ISSUE_TEMPLATE/QUESTION.md @@ -1,15 +1,15 @@ ---- -name: Question -about: Use this template to ask a question about the project -title: "❓ " -labels: question -assignees: recursivezero ---- - -## Question - -State your question - -## Sample Code - -Please include relevant code snippets or files that provide context for your question. +--- +name: Question +about: Use this template to ask a question about the project +title: "❓ " +labels: question +assignees: recursivezero +--- + +## Question + +State your question + +## Sample Code + +Please include relevant code snippets or files that provide context for your question. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 136b402..09c2909 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ -blank_issues_enabled: false -contact_links: - - name: GitHub Community Support - url: https://github.com/orgs/community/discussions - about: Please ask and answer questions here. - - name: Social Media Support - url: https://twitter.com/recursivezero - about: Please connect on social media here. +blank_issues_enabled: false +contact_links: + - name: GitHub Community Support + url: https://github.com/orgs/community/discussions + about: Please ask and answer questions here. + - name: Social Media Support + url: https://twitter.com/recursivezero + about: Please connect on social media here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a8cc1d1..9114c19 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,41 +1,41 @@ -# Pull Request Template - -## Description - -Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. - -Fixes # (issue) - -## Type of change - -Please delete options that are not relevant. - -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] This change requires a documentation update - -## How Has This Been Tested? - -Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration - -- [ ] Test A -- [ ] Test B - -**Test Configuration**: - -- Browser: -- Device: -- Toolchain: - -## Checklist - -- [ ] My code follows the style guidelines of this project -- [ ] I have performed a self-review of my own code -- [ ] I have commented my code, particularly in hard-to-understand areas -- [ ] I have made corresponding changes to the documentation -- [ ] My changes generate no new warnings -- [ ] I have added tests that prove my fix is effective or that my feature works -- [ ] New and existing unit tests pass locally with my changes -- [ ] Any dependent changes have been merged and published in downstream modules -- [ ] I have checked my code and corrected any misspellings +# Pull Request Template + +## Description + +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. + +Fixes # (issue) + +## Type of change + +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +## How Has This Been Tested? + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration + +- [ ] Test A +- [ ] Test B + +**Test Configuration**: + +- Browser: +- Device: +- Toolchain: + +## Checklist + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules +- [ ] I have checked my code and corrected any misspellings diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 4ffec2a..16c0887 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,37 +1,37 @@ -# Security Policy - -## Scope - -Keeping users safe and secure is a top priority for us.We welcome the contribution of external security researchers. - -If you believe you’ve found a security or vulnerability issue in the repo we encourage you to notify us. - -There are no hard and fast rules to determine if a bug is worth reporting as a security issue or a “regular” issue. -When in doubt, please do send us a report. - -## How to submit a report - -Security issues can be reported by sending [an email][contact]. - -The team will acknowledge your email within 48 hours. You will receive a more detailed response within 96 hours. - -We will create a maintainer security advisory on GitHub to discuss internally, and when needed, invite you to the advisory. - -## Purpose - -- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our services -- Only interact with accounts you own or with explicit permission of the account holder. If you do encounter Personally Identifiable Information (PII) contact us immediately, - do not proceed with access, and immediately purge any local information -- Provide us with a reasonable amount of time to resolve vulnerabilities prior to any disclosure to the public or a third-party -- We will consider activities conducted consistent with this policy to constitute “authorized” conduct and will not pursue civil action or initiate a complaint to law enforcement. - We will help to the extent we can if legal action is initiated by a third party against you - -Please submit a report to us before engaging in conduct that may be inconsistent with or unaddressed by this policy. - -## Preferences - -- Please provide detailed reports with reproducible steps and a clearly defined impact -- Submit one vulnerability per report -- Social engineering (such as phishing, vishing, smishing) is prohibited - -[contact]: mailto:recursivelyzero@gmail.com +# Security Policy + +## Scope + +Keeping users safe and secure is a top priority for us.We welcome the contribution of external security researchers. + +If you believe you’ve found a security or vulnerability issue in the repo we encourage you to notify us. + +There are no hard and fast rules to determine if a bug is worth reporting as a security issue or a “regular” issue. +When in doubt, please do send us a report. + +## How to submit a report + +Security issues can be reported by sending [an email][contact]. + +The team will acknowledge your email within 48 hours. You will receive a more detailed response within 96 hours. + +We will create a maintainer security advisory on GitHub to discuss internally, and when needed, invite you to the advisory. + +## Purpose + +- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our services +- Only interact with accounts you own or with explicit permission of the account holder. If you do encounter Personally Identifiable Information (PII) contact us immediately, + do not proceed with access, and immediately purge any local information +- Provide us with a reasonable amount of time to resolve vulnerabilities prior to any disclosure to the public or a third-party +- We will consider activities conducted consistent with this policy to constitute “authorized” conduct and will not pursue civil action or initiate a complaint to law enforcement. + We will help to the extent we can if legal action is initiated by a third party against you + +Please submit a report to us before engaging in conduct that may be inconsistent with or unaddressed by this policy. + +## Preferences + +- Please provide detailed reports with reproducible steps and a clearly defined impact +- Submit one vulnerability per report +- Social engineering (such as phishing, vishing, smishing) is prohibited + +[contact]: mailto:recursivelyzero@gmail.com diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md index 284ee48..e89776f 100644 --- a/.github/SUPPORT.md +++ b/.github/SUPPORT.md @@ -1,50 +1,50 @@ -# Support - -This article explains where to get help with remark. -Please read through the following guidelines. - -> [!Note] -> before participating in our community, please read our [code of conduct][coc]. -> By interacting with this repository, organization, or community you agree to abide by its terms. - -## Asking quality questions - -Questions can go to [GitHub discussions][dicussion]. - -Help us help you! - -Spend time framing questions and add links and resources. -Spending the extra time up front can help save everyone time in the long run. - -> [!Tip] -> Here are some tips - -- [Talk to us][chat]! -- Don’t fall for the [XY problem][xy] -- Search to find out if a similar question has been asked -- Try to define what you need help with: - - Is there something in particular you want to do? - - What problem are you encountering and what steps have you taken to try and fix it? - - Is there a concept you don’t understand? -- Provide sample code, such as a [CodeSandbox][cs] or [StackBlitz][sb] or a small video, if possible -- Screenshots can help, but if there’s important text such as code or error messages in them, please also provide those as text -- The more time you put into asking your question, the better we can help you - -## Contributions - -See [`contributing.md`][contributing] on how to contribute. - -## License - -© [Keshav Mohta][author] - - - -[author]: https://xkeshav.com -[coc]: https://github.com/recursivezero/tiny/blob/main/.github/CODE_OF_CONDUCT.md -[chat]: https://github.com/recursivezero/tiny/discussions/new?category=general -[dicussion]: https://github.com/recursivezero/tiny/discussions/new?category=q-a -[contributing]: https://github.com/recursivezero/tiny/blob/main/.github/CONTRIBUTING.md -[xy]: https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem/66378#66378 -[cs]: https://codesandbox.io -[sb]: https://stackblitz.com +# Support + +This article explains where to get help with remark. +Please read through the following guidelines. + +> [!Note] +> before participating in our community, please read our [code of conduct][coc]. +> By interacting with this repository, organization, or community you agree to abide by its terms. + +## Asking quality questions + +Questions can go to [GitHub discussions][dicussion]. + +Help us help you! + +Spend time framing questions and add links and resources. +Spending the extra time up front can help save everyone time in the long run. + +> [!Tip] +> Here are some tips + +- [Talk to us][chat]! +- Don’t fall for the [XY problem][xy] +- Search to find out if a similar question has been asked +- Try to define what you need help with: + - Is there something in particular you want to do? + - What problem are you encountering and what steps have you taken to try and fix it? + - Is there a concept you don’t understand? +- Provide sample code, such as a [CodeSandbox][cs] or [StackBlitz][sb] or a small video, if possible +- Screenshots can help, but if there’s important text such as code or error messages in them, please also provide those as text +- The more time you put into asking your question, the better we can help you + +## Contributions + +See [`contributing.md`][contributing] on how to contribute. + +## License + +© [Keshav Mohta][author] + + + +[author]: https://xkeshav.com +[coc]: https://github.com/recursivezero/tiny/blob/main/.github/CODE_OF_CONDUCT.md +[chat]: https://github.com/recursivezero/tiny/discussions/new?category=general +[dicussion]: https://github.com/recursivezero/tiny/discussions/new?category=q-a +[contributing]: https://github.com/recursivezero/tiny/blob/main/.github/CONTRIBUTING.md +[xy]: https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem/66378#66378 +[cs]: https://codesandbox.io +[sb]: https://stackblitz.com diff --git a/.github/actions/format-issue-title/format.sh b/.github/actions/format-issue-title/format.sh index 8d1298c..e12499f 100755 --- a/.github/actions/format-issue-title/format.sh +++ b/.github/actions/format-issue-title/format.sh @@ -1,50 +1,50 @@ -#!/bin/bash -set -e - -ISSUE_NUMBER=$1 -OLD_TITLE=$2 -PREFIX=$3 -PLACEHOLDER=$4 -DRY_RUN=$5 - -# Fallback to default if prefix is empty -if [ -z "$PREFIX" ]; then - PREFIX="GEN" -fi - -YEAR=$(date +%y) -PADDED_NUM=$(printf "%04d" "$ISSUE_NUMBER") -IDENTIFIER="[${PREFIX}-${YEAR}${PADDED_NUM}]: " - -# Skip if already present -if echo "$OLD_TITLE" | grep -q "$IDENTIFIER"; then - echo "Identifier already present. Skipping." - exit 0 -fi - -# Replace placeholder at start if provided -if [ -n "$PLACEHOLDER" ] && echo "$OLD_TITLE" | grep -q "^$PLACEHOLDER"; then - NEW_TITLE=$(echo "$OLD_TITLE" | sed "s/^$PLACEHOLDER[[:space:]]*/$IDENTIFIER/") -else - # Insert after emoji if present - if [[ "$OLD_TITLE" =~ ^([[:space:]]*[^[:alnum:][:space:]]+[[:space:]]*)(.*) ]]; then - EMOJI="${BASH_REMATCH[1]}" - REST="${BASH_REMATCH[2]}" - NEW_TITLE="${EMOJI} ${IDENTIFIER}${REST}" - else - NEW_TITLE="${IDENTIFIER}${OLD_TITLE}" - fi -fi - -# Normalize spacing -NEW_TITLE=$(echo "$NEW_TITLE" | sed 's/ */ /g' | sed 's/^ *//;s/ *$//') - -echo "New title would be: $NEW_TITLE" - -if [ "$DRY_RUN" = "true" ]; then - echo "Dry-run enabled. Not updating title." - exit 0 -fi - -gh issue edit "$ISSUE_NUMBER" --title "$NEW_TITLE" --repo "${GITHUB_REPOSITORY}" -echo "Title updated successfully." +#!/bin/bash +set -e + +ISSUE_NUMBER=$1 +OLD_TITLE=$2 +PREFIX=$3 +PLACEHOLDER=$4 +DRY_RUN=$5 + +# Fallback to default if prefix is empty +if [ -z "$PREFIX" ]; then + PREFIX="GEN" +fi + +YEAR=$(date +%y) +PADDED_NUM=$(printf "%04d" "$ISSUE_NUMBER") +IDENTIFIER="[${PREFIX}-${YEAR}${PADDED_NUM}]: " + +# Skip if already present +if echo "$OLD_TITLE" | grep -q "$IDENTIFIER"; then + echo "Identifier already present. Skipping." + exit 0 +fi + +# Replace placeholder at start if provided +if [ -n "$PLACEHOLDER" ] && echo "$OLD_TITLE" | grep -q "^$PLACEHOLDER"; then + NEW_TITLE=$(echo "$OLD_TITLE" | sed "s/^$PLACEHOLDER[[:space:]]*/$IDENTIFIER/") +else + # Insert after emoji if present + if [[ "$OLD_TITLE" =~ ^([[:space:]]*[^[:alnum:][:space:]]+[[:space:]]*)(.*) ]]; then + EMOJI="${BASH_REMATCH[1]}" + REST="${BASH_REMATCH[2]}" + NEW_TITLE="${EMOJI} ${IDENTIFIER}${REST}" + else + NEW_TITLE="${IDENTIFIER}${OLD_TITLE}" + fi +fi + +# Normalize spacing +NEW_TITLE=$(echo "$NEW_TITLE" | sed 's/ */ /g' | sed 's/^ *//;s/ *$//') + +echo "New title would be: $NEW_TITLE" + +if [ "$DRY_RUN" = "true" ]; then + echo "Dry-run enabled. Not updating title." + exit 0 +fi + +gh issue edit "$ISSUE_NUMBER" --title "$NEW_TITLE" --repo "${GITHUB_REPOSITORY}" +echo "Title updated successfully." diff --git a/.github/workflows/comment-on-issue.yml b/.github/workflows/comment-on-issue.yml index fb98fa4..9c52701 100644 --- a/.github/workflows/comment-on-issue.yml +++ b/.github/workflows/comment-on-issue.yml @@ -2,7 +2,7 @@ name: "Comment on New Issue" on: issues: - types: [opened, labeled] + types: [opened] workflow_dispatch: inputs: diff --git a/.github/workflows/echo.yml b/.github/workflows/echo.yml index 602d55a..aef8b65 100644 --- a/.github/workflows/echo.yml +++ b/.github/workflows/echo.yml @@ -1,13 +1,13 @@ -name: Organization CI -on: - push: - branches: [$default-branch] - pull_request: - branches: [$default-branch] -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - name: Run a one-line script - run: echo Hello from Action-Club +name: Organization CI +on: + push: + branches: [$default-branch] + pull_request: + branches: [$default-branch] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Run a one-line script + run: echo Hello from Action-Club diff --git a/.github/workflows/format-issue-title.yml b/.github/workflows/format-issue-title.yml index 57881b9..e1fb2c6 100644 --- a/.github/workflows/format-issue-title.yml +++ b/.github/workflows/format-issue-title.yml @@ -2,7 +2,7 @@ name: "Auto Format Issue Title" on: issues: - types: [opened, labeled] + types: [opened, edited] permissions: issues: write diff --git a/.github/workflows/project-assign.yml b/.github/workflows/project-assign.yml new file mode 100644 index 0000000..7e1f4fd --- /dev/null +++ b/.github/workflows/project-assign.yml @@ -0,0 +1,19 @@ +name: Assign Project +run-name: Assign project under issue and pull requests + +on: + issues: + types: [opened] + pull_request: + types: [opened, reopened] + +permissions: + issues: write + +jobs: + sync: + uses: recursivezero/action-club/.github/workflows/auto-assign-project.yml@main + with: + project_number: 5 # That's it! + secrets: + PROJECT_PAT: ${{ secrets.PROJECT_PAT }} diff --git a/.gitignore b/.gitignore index fa1b42e..ae1cf18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,61 +1,61 @@ -__pycache__/ -*.py[cod] -*.pyo -*$py.class - -.venv* -venv/ -env/ -.venv* - - -.env -.env.* -!.env.local - - -instance/ -*.db -*.sqlite -*.sqlite3 - - -dump/ -*.bson -urls_export.json - - -.cache/ -.pytest_cache/ -.mypy_cache/ -.pytype/ -.coverage -htmlcov/ - - -*.log -logs/ -log/ - - -.vscode/ -.idea/ -*.swp -*.swo - - -.DS_Store -Thumbs.db - - -build/ -dist/ -.eggs/ -*.egg-info/ - - -poetry.lock - -*.tmp -*.temp -*.bak +__pycache__/ +*.py[cod] +*.pyo +*$py.class + +.venv* +venv/ +env/ +.venv* + + +.env +.env.* +!.env.sample + + +instance/ +*.db +*.sqlite +*.sqlite3 + + +dump/ +*.bson +urls_export.json + + +.cache/ +.pytest_cache/ +.mypy_cache/ +.pytype/ +.coverage +htmlcov/ + + +*.log +logs/ +log/ + + +.vscode/ +.idea/ +*.swp +*.swo + + +.DS_Store +Thumbs.db + + +build/ +dist/ +.eggs/ +*.egg-info/ + + +poetry.lock + +*.tmp +*.temp +*.bak diff --git a/.poetryignore b/.poetryignore index d553ebc..add52dd 100644 --- a/.poetryignore +++ b/.poetryignore @@ -1,41 +1,41 @@ -# Ignore Python cache and venv - -**pycache**/ -_.py[cod] -_.so -\*.egg-info/ -.venv/ -venv/ -env/ - -## Ignore build output - -build/ -dist/ -_.egg -_.whl - -## Ignore IDE/editor settings - -.vscode/ -.idea/ -_.swp -_.bak - -## Ignore local config and test logs - -.env -.env.\* -\*.log - -## Ignore test data/output - -tests/ -_.test._ -debug*\*.py -trace*\*.py - -## Ignore OS files - -.DS_Store -Thumbs.db +# Ignore Python cache and venv + +**pycache**/ +_.py[cod] +_.so +\*.egg-info/ +.venv/ +venv/ +env/ + +## Ignore build output + +build/ +dist/ +_.egg +_.whl + +## Ignore IDE/editor settings + +.vscode/ +.idea/ +_.swp +_.bak + +## Ignore local config and test logs + +.env +.env.\* +\*.log + +## Ignore test data/output + +tests/ +_.test._ +debug*\*.py +trace*\*.py + +## Ignore OS files + +.DS_Store +Thumbs.db diff --git a/.vscode/cspell.json b/.vscode/cspell.json index d612dba..a1bfc3d 100755 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -1,35 +1,35 @@ -{ - "dictionaryDefinitions": [ - { - "name": "projectTerms", - "path": "./dictionaries/project-words.txt" - }, - { - "name": "teamMember", - "path": "./dictionaries/team-member.txt" - } - ], - "dictionaries": [ - "projectTerms", - "teamMember" - ], - "languageSettings": [ - { - "languageId": "*", - "dictionaries": [ - "projectTerms" - ] - }, - { - "languageId": "json, md", - "dictionaries": [ - "teamMember" - ] - } - ], - "enableFiletypes": [ - "astro", - "jsx", - "tsx" - ] -} +{ + "dictionaryDefinitions": [ + { + "name": "projectTerms", + "path": "./dictionaries/project-words.txt" + }, + { + "name": "teamMember", + "path": "./dictionaries/team-member.txt" + } + ], + "dictionaries": [ + "projectTerms", + "teamMember" + ], + "languageSettings": [ + { + "languageId": "*", + "dictionaries": [ + "projectTerms" + ] + }, + { + "languageId": "json, md", + "dictionaries": [ + "teamMember" + ] + } + ], + "enableFiletypes": [ + "astro", + "jsx", + "tsx" + ] +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index c303d82..d7f443b 100755 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,19 +1,19 @@ -{ - "recommendations": [ - "streetsidesoftware.code-spell-checker", - "alefragnani.project-manager", - "esbenp.prettier-vscode", - "oderwat.indent-rainbow", - "davidanson.vscode-markdownlint", - "mkxml.vscode-filesize", - "christian-kohler.path-intellisense", - "artdiniz.quitcontrol-vscode", - "enkia.tokyo-night", - "dbaeumer.vscode-eslint", - "johnpapa.vscode-peacock", - "aaron-bond.better-comments", - "Heron.firefox-devtools-theme", - "GitHub.github-vscode-theme", - "xkeshav.css-color-collector" - ] -} +{ + "recommendations": [ + "streetsidesoftware.code-spell-checker", + "alefragnani.project-manager", + "esbenp.prettier-vscode", + "oderwat.indent-rainbow", + "davidanson.vscode-markdownlint", + "mkxml.vscode-filesize", + "christian-kohler.path-intellisense", + "artdiniz.quitcontrol-vscode", + "enkia.tokyo-night", + "dbaeumer.vscode-eslint", + "johnpapa.vscode-peacock", + "aaron-bond.better-comments", + "Heron.firefox-devtools-theme", + "GitHub.github-vscode-theme", + "xkeshav.css-color-collector" + ] +} diff --git a/.vscode/javascriptreact.json b/.vscode/javascriptreact.json index 604637e..012c647 100644 --- a/.vscode/javascriptreact.json +++ b/.vscode/javascriptreact.json @@ -1,26 +1,26 @@ -{ - "React: useState": { - "scope": "typescriptreact, typescript, javascript, javascriptreact", - "prefix": "rus", - "body": [ - "const [${1:state}, set${1/(.*)/${1:/capitalize}/}] = useState(${2:null});", - "$3" - ], - "description": "React Hook: useState" - }, - "React: Functional Component": { - "scope": "typescriptreact, typescript, javascript, javascriptreact", - "prefix": "rfc", - "body": [ - "import { Fragment } from 'react';\n", - "export type ${2:${1}Props} = { }\n", - "export const ${1:${TM_FILENAME_BASE}} = (props: React.FC<${2}>) => {", - "\tconsole.log({ props }); $BLOCK_COMMENT_START added to suppress lint error $BLOCK_COMMENT_END", - "\treturn (", - "\t\t$0", - "\t);", - "}" - ], - "description": "React Functional Component" - } +{ + "React: useState": { + "scope": "typescriptreact, typescript, javascript, javascriptreact", + "prefix": "rus", + "body": [ + "const [${1:state}, set${1/(.*)/${1:/capitalize}/}] = useState(${2:null});", + "$3" + ], + "description": "React Hook: useState" + }, + "React: Functional Component": { + "scope": "typescriptreact, typescript, javascript, javascriptreact", + "prefix": "rfc", + "body": [ + "import { Fragment } from 'react';\n", + "export type ${2:${1}Props} = { }\n", + "export const ${1:${TM_FILENAME_BASE}} = (props: React.FC<${2}>) => {", + "\tconsole.log({ props }); $BLOCK_COMMENT_START added to suppress lint error $BLOCK_COMMENT_END", + "\treturn (", + "\t\t$0", + "\t);", + "}" + ], + "description": "React Functional Component" + } } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index fda4174..8a5f03d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,24 +1,22 @@ -{ - "workbench.colorCustomizations": { - "activityBar.activeBackground": "#def6c4", - "activityBar.background": "#def6c4", - "activityBar.foreground": "#15202b", - "activityBar.inactiveForeground": "#15202b99", - "activityBarBadge.background": "#59a2e6", - "activityBarBadge.foreground": "#15202b", - "commandCenter.border": "#15202b99", - "sash.hoverBorder": "#def6c4", - "statusBar.background": "#c5ef98", - "statusBar.foreground": "#15202b", - "statusBarItem.hoverBackground": "#ace86c", - "statusBarItem.remoteBackground": "#c5ef98", - "statusBarItem.remoteForeground": "#15202b", - "titleBar.activeBackground": "#c5ef98", - "titleBar.activeForeground": "#15202b", - "titleBar.inactiveBackground": "#c5ef9899", - "titleBar.inactiveForeground": "#15202b99", - "sideBar.border": "#def6c4", - "tab.activeBorder": "#def6c4" - }, - "peacock.color": "#c5ef98" +{ + "workbench.colorCustomizations": { + "activityBar.activeBackground": "#def6c4", + "activityBar.background": "#def6c4", + "activityBar.foreground": "#15202b", + "activityBar.inactiveForeground": "#15202b99", + "activityBarBadge.background": "#59a2e6", + "activityBarBadge.foreground": "#15202b", + "commandCenter.border": "#15202b99", + "sash.hoverBorder": "#def6c4", + "statusBar.background": "#c5ef98", + "statusBar.foreground": "#15202b", + "statusBarItem.hoverBackground": "#ace86c", + "statusBarItem.remoteBackground": "#c5ef98", + "statusBarItem.remoteForeground": "#15202b", + "titleBar.activeBackground": "#c5ef98", + "titleBar.activeForeground": "#15202b", + "titleBar.inactiveBackground": "#c5ef9899", + "titleBar.inactiveForeground": "#15202b99" + }, + "peacock.color": "#c5ef98" } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index fdfa730..1877924 100755 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,12 +1,12 @@ -{ - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "2.0.0", - "tasks": [ - { - "label": "echo", - "type": "shell", - "command": "echo Hello" - } - ] +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "echo", + "type": "shell", + "command": "echo Hello" + } + ] } \ No newline at end of file diff --git a/.vscode/template.code-snippets b/.vscode/template.code-snippets index f9549de..b48f3f7 100644 --- a/.vscode/template.code-snippets +++ b/.vscode/template.code-snippets @@ -1,22 +1,22 @@ -{ - "console-obj": { - "scope": "javascript, javascriptreact, typescript, typescriptreact", - "prefix": "cl", - "body": [ - "console.log({ $0 });" - ], - "description": "destructured console log" - }, - "log-clip": { - "scope": "javascript, javascriptreact, typescript, typescriptreact", - "prefix": "log-clip", - "body": ["//${3: ${CLIPBOARD/(.*)\\./$2/g}}", "console.log({$CLIPBOARD})"], - "description": "clip to console" - }, - "createDocumentFragment": { - "scope": "javascript, javascriptreact, typescript, typescriptreact", - "prefix": "cdf", - "body": "const ${1:varName} = document.createDocumentFragment();${0}", - "description": "Creates an element using document.createDoumentFragment" - } +{ + "console-obj": { + "scope": "javascript, javascriptreact, typescript, typescriptreact", + "prefix": "cl", + "body": [ + "console.log({ $0 });" + ], + "description": "destructured console log" + }, + "log-clip": { + "scope": "javascript, javascriptreact, typescript, typescriptreact", + "prefix": "log-clip", + "body": ["//${3: ${CLIPBOARD/(.*)\\./$2/g}}", "console.log({$CLIPBOARD})"], + "description": "clip to console" + }, + "createDocumentFragment": { + "scope": "javascript, javascriptreact, typescript, typescriptreact", + "prefix": "cdf", + "body": "const ${1:varName} = document.createDocumentFragment();${0}", + "description": "Creates an element using document.createDoumentFragment" + } } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cf5522..ad57eb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,13 @@ -# ChangeLog - -All notable changes to this repository will be documented in this file. - -## [0.0.1] - -- Initial release. -- Health Files - -## [0.1.0] - -- Restructure folder structure -- added poetry dev script +# ChangeLog + +All notable changes to this repository will be documented in this file. + +## [0.0.1] + +- Initial release. +- Health Files + +## [0.1.0] + +- Restructure folder structure +- added poetry dev script diff --git a/LICENSE b/LICENSE index d38c94e..2426c1b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2024 RecursiveZero Private Limited - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2024 RecursiveZero Private Limited + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index bc8fe43..ff01fba 100644 --- a/README.md +++ b/README.md @@ -1,348 +1,290 @@ -# Tiny URL Generator - -> A modern, Bitly-style tiny URL web application built with FastAPI & MongoDB - -![Python](https://img.shields.io/badge/Python-3.10-blue.svg) -![MongoDB](https://img.shields.io/badge/Database-MongoDB-green.svg) -![License](https://img.shields.io/badge/License-MIT-yellow.svg) -![Status](https://img.shields.io/badge/Status-Active-success.svg) - ---- - -## Overview - -**tiny URL** is a sleek, fast, and modern URL shortening platform built using **FastAPI**, and **MongoDB**. -It converts long URLs into short, shareable links — just like Bitly. - -The project supports: - -- 🚀 **FastAPI REST API** (developers / integrations) - ---- - -## Features - -### User Features - -- Convert long URLs into short, unique codes -- Default checkbox QR code generation -- Clean Bitly-style result card -- Copy & share buttons -- Download URL button -- Share URL -- Copy button with animation -- Smooth URL validation and sanitization -- Auto Dark/Light Mode (saves preference) -- Mobile-friendly QR Codes -- Fully responsive design -- Recent URLs page - -### API & Developer Features - -- REST API for URL shortening -- API version endpoint -- Swagger / OpenAPI documentation -- API landing page -- Cache layer for fast redirects -- Graceful offline mode (no DB required) -- Clean startup lifecycle using FastAPI lifespan -- Optional MongoDB dependency - ---- - -## Short Code Generation Algorithm - -The app uses a Random Alphanumeric Short Code Generator. - -### Algorithm Details - -- Uses `string.ascii_letters + string.digits` -- Randomly picks characters -- Generates a 6-character short ID -- Checks MongoDB for collisions (if DB is enabled) -- Automatically regenerates on collision - -### Example - -```python -import random, string - -def generate_code(length=6): - chars = string.ascii_letters + string.digits - return ''.join(random.choice(chars) for _ in range(length)) -``` - ---- - -| Layer | Technology | -| ----------- | --------------------- | -| API Backend | FastAPI | -| Database | MongoDB | -| Frontend | HTML, CSS, Vanilla JS | -| API Server | Uvicorn | -| Validation | Pydantic v2 | -| CLI | Click | -| Data | JSON | - -| Layer | Technology | -| ----------- | --------------------- | -| UI Backend | FastAPI | -| API Backend | FastAPI | -| Database | MongoDB (Optional) | -| Frontend | HTML, CSS, Vanilla JS | -| QR Code | qrcode + Pillow | -| API Server | Uvicorn | -| Validation | Pydantic v2 | -| Env Mgmt | python-dotenv | -| Tooling | Poetry | - ---- - -## Project Folder Structure - -```text -Directory structure: -tiny/ -├── CHANGELOG.md -├── LICENSE -├── app/ -│ ├──__init__.py -│ ├── main.py -│ ├── cli.py -│ ├── api/ -| | └──__init__.py -│ │ └── fast_api.py -| ├──assets/images -│ ├── db/ -| | └──__init__.py -| | └──data.py -| ├── static/ -| | └── images -| | └── qr -| | └── style.css -| ├── templates/ -| | └── index.html -| | └── recent.html -│ ├── utils/ -| | └── __init__.py -| | └── _version.py -| | └── config.py -| | └── helper.py -| | └── lint.py -| | └── qr.py -| -├── docs/ -| └── build-test.md -| └─ run_with_curl.md -| -├── request/ -| └── mixed.json -| └── single.json -| └── urls.json -├── pyproject.toml -| └── poetry.lock -├──README.md -| └── CHANGELOG.md -| └── requirements.txt -├── tiny.code-workspace -└── .gitignore - -``` - -⚙️ How to Run the Project Locally - -## How to start - -```sh -poetry install -``` - -### 3. Install with other dependent packages - -```sh -poetry install --all-extras --with dev -``` - ---- - -## Running the App - -```sh -poetry run uvicorn app.main:app --reload -``` - -or - -```sh -poetry run tiny dev -``` - -Open: - - ---- - -## Environment Configuration - -``` -ENV=development -DOMAIN=http://127.0.0.1:8000 -MONGO_URI=mongodb://:@localhost:27017/tiny_url?authSource=tiny_url -DATABASE_NAME=tiny_url -``` - -Supported env files: - -- .env.development -- .env.local -- .env (production) - ---- - -## Offline Mode (No Database) - -TinyURL supports graceful offline mode. - -### What works - -- App starts normally -- UI loads -- Short URLs are generated -- QR codes are generated - -### What is disabled - -- Recent URLs page -- Persistent redirects after restart -- Visit count tracking - -Offline Mode activates automatically when: - -- MongoDB is down -- OR pymongo is not installed -- OR MONGO_URI is missing/invalid - -Log message: - -``` -⚠️ MongoDB connection failed. Running in NO-DB mode. -``` - ---- - -## Switching Modes - -### With MongoDB - -```sh -poetry install --with mongodb -sudo systemctl start mongod -poetry run tiny dev -``` - -### Without MongoDB - -```sh -sudo systemctl stop mongod -poetry run tiny dev -``` - -or - -```sh -poetry run pip uninstall pymongo -poetry run tiny dev -``` - ---- - -## REST API (FastAPI) - -### API Base URL - - - -### Swagger Docs - - - -### Shorten URL - -POST /api/shorten - -Request: - -```json -{ - "url": "https://example.com" -} -``` - -Response: - -```json -{ - "input_url": "https://example.com", - "output_url": "http://127.0.0.1:8000/AbX92p", - "created_on": "2026-01-03T13:25:10+00:00" -} -``` - -### API Version - -GET /api/version - -Response: - -```json -{ - "version": "0.1.0" -} -``` - ---- - -## Troubleshooting - -### Mongo auth error - -Encode special chars: - -@ ? %40 - -Example: - -``` -MONGO_URI=mongodb://user%40gmail.com:Pass%40123@localhost:27017/tiny_url?authSource=tiny_url -``` - ---- - -## WSL Notes - -```sh -sudo systemctl start mongod -poetry run uvicorn app.main:app --reload -``` - ---- - -## License - -📜Docs -[run_with_curl](run_with_curl) - -Screenshots: -Home Page: -![home page](./assets/images/home.png) -![home dark mode](./assets/images/home_dark.png) -![home page](./assets/images/valid.png) -![home layout](./assets/images/short_url.png) -![recent](./assets/images/recent.png) -tiny API Page: -![API](./assets/images/API_page.png) -![API1](./assets/images/api_page2.png) - -📜License - -[MIT](LICENSE) +# Tiny URL Generator + +> A modern, Bitly-style tiny URL web application built with FastAPI, optional MongoDB, and a sleek web UI. + +![Python](https://img.shields.io/badge/Python-3.11-blue.svg) +![FastAPI](https://img.shields.io/badge/FastAPI-Backend-teal.svg) +![MongoDB]() +![License](https://img.shields.io/badge/License-MIT-yellow.svg) +![Status](https://img.shields.io/badge/Status-Active-success.svg) + +--- + +## Overview + +Tiny URL is a sleek, fast, and modern URL shortening platform built using FastAPI with optional MongoDB persistence. +It converts long URLs into short, shareable links — just like Bitly. + +The project supports: + +- Web UI (FastAPI + Jinja templates) +- REST API (FastAPI) +- Offline Mode (No MongoDB required) + +This project is designed with: + +- Clean startup lifecycle (no racing configs) +- Optional database dependency +- Graceful degradation when MongoDB is unavailable +- In-memory cache fallback +- QR code generation with auto folder creation + +--- + +## Features + +### User Features + +- Convert long URLs into short, unique codes +- Default checkbox QR code generation +- Clean Bitly-style result card +- Copy & share buttons +- Download URL button +- URL validation and sanitization +- Fully responsive UI +- Recent URLs page (when DB is available) +- Visit count tracking (when DB is available) +- QR image auto-generation with logo +- Cache-accelerated redirects + +### API & Developer Features + +- REST API for URL shortening +- API version endpoint +- Swagger / OpenAPI documentation +- API landing page +- Cache layer for fast redirects +- Graceful offline mode (no DB required) +- Clean startup lifecycle using FastAPI lifespan +- Optional MongoDB dependency + +--- + +## Short Code Generation Algorithm + +The app uses a Random Alphanumeric Short Code Generator. + +### Algorithm Details + +- Uses `string.ascii_letters + string.digits` +- Randomly picks characters +- Generates a 6-character short ID +- Checks MongoDB for collisions (if DB is enabled) +- Automatically regenerates on collision + +### Example + +```python +import random, string + +def generate_code(length=6): + chars = string.ascii_letters + string.digits + return ''.join(random.choice(chars) for _ in range(length)) +``` + +--- + +## Tech Stack + +| Layer | Technology | +| ----------- | ----------------------- | +| UI Backend | FastAPI | +| API Backend | FastAPI | +| Database | MongoDB (Optional) | +| Cache | In-Memory (Python dict) | +| Frontend | HTML, CSS, Vanilla JS | +| QR Code | qrcode + Pillow | +| API Server | Uvicorn | +| Validation | Pydantic v2 | +| Env Mgmt | python-dotenv | +| Tooling | Poetry | + +--- + +[Project Tree](./docs/tree.md) + +## ⚙️ How to Run the Project Locally + +`Virtual Environment Configuration` + +```bash +poetry config virtualenvs.path /your/desired/path +``` + +## Environment Configuration + +Ensure below files are configured (create if not exist) properly to run the project; + +Supported env files: + +- .env.development +- .env.local +- .env (production) + +```text +ENV=development +DOMAIN=http://127.0.0.1:8000 +MONGO_URI=mongodb://:@localhost:27017/tiny_url?authSource=tiny_url +DATABASE_NAME=tiny_url +``` + +## Install Dependencies + +```bash +poetry lock --no-cache --regenerate +poetry install --all-extras --with dev +``` + +Or manually + +```bash +poetry install +``` + +## How to Run + +```bash +poetry run tiny dev +``` + +Access: + +## Run FastAPI Server + +```bash +poetry run tiny api +``` + +Access: + +## Offline Mode (No Database) + +TinyURL supports graceful offline mode. + +### What works + +- App starts normally +- UI loads +- Short URLs are generated +- QR codes are generated +- Redirects work from in-memory cache + +### What is disabled + +- Recent URLs page +- Persistent redirects after restart +- Visit count tracking + +Offline Mode activates automatically when: + +- MongoDB is down +- OR pymongo is not installed +- OR MONGO_URI is missing/invalid + +Log message: + +``` +⚠️ MongoDB connection failed. Running in NO-DB mode. +``` + +--- + +## Switching Modes + +### Without MongoDB + +```sh +sudo systemctl stop mongod +poetry run tiny dev +``` + +or + +```sh +poetry run pip uninstall pymongo +poetry run tiny dev +``` + +--- + +## Troubleshooting + +sometimes there might be chances that virtual environment get corrupted then delete the old virtual environment and start afresh. + +```sh +poetry env info +# this will provide virtual environment name +poetry env remove +``` + +### Mongo auth error + +Encode special chars: + +@ ? %40 + +Example: + +``` +MONGO_URI=mongodb://user%40gmail.com:Pass%40123@localhost:27017/tiny_url?authSource=tiny_url +``` + +--- + +## WSL Notes + +```sh +sudo systemctl start mongod +poetry run tiny dev +``` + +## Build & Packaging + +## Build Package + +```bash +poetry clean +poetry build +``` + +Artifacts in `dist/` + +- tiny-x.y.0-py3-none-any.whl +- tiny-x.y.0.tar.gz + +## Test Locally + +```bash +python -m venv .venv-dist +source .venv-dist/bin/activate +# Windows +.venv-dist\Scripts\activate +``` + +### Install package + +```bash +pip install dist/*.whl +pip install --upgrade dist/*.whl +``` + +## License + +📜Docs +[run_with_curl](docs/run_with_curl) + +Screenshots: +Home Page: +![home page](assets/images/home.png) +![home dark mode](assets/images/home_dark.png) +![home page](assets/images/valid.png) +![home layout](assets/images/short_url.png) +![recent](assets/images/recent.png) +tiny API Page: +![API](assets/images/API_page.png) +![API1](assets/images/api_page2.png) +No DB Mode: +![NO DB](assets/images/no-db.png) +📜License + +[MIT](LICENSE) diff --git a/app/__init__.py b/app/__init__.py index 5529dc1..683fb26 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,3 +1,3 @@ -from app.utils._version import get_version - -__version__ = get_version() +from app.utils._version import get_version + +__version__ = get_version() diff --git a/app/api/__init__.py b/app/api/__init__.py index 5529dc1..683fb26 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -1,3 +1,3 @@ -from app.utils._version import get_version - -__version__ = get_version() +from app.utils._version import get_version + +__version__ = get_version() diff --git a/app/api/fast_api.py b/app/api/fast_api.py index a20c51c..fad63e4 100644 --- a/app/api/fast_api.py +++ b/app/api/fast_api.py @@ -1,211 +1,211 @@ -import os -import re -import traceback -from datetime import datetime, timezone -from typing import Any - -from fastapi import APIRouter, FastAPI, Request -from fastapi.responses import HTMLResponse, JSONResponse -from pydantic import BaseModel, Field - -from app import __version__ -from app.db import data as db_data -from app.utils.helper import generate_code, is_valid_url, sanitize_url - -PyMongoError: Any -try: - from pymongo.errors import PyMongoError as _RealPyMongoError - - PyMongoError = _RealPyMongoError -except (ImportError, ModuleNotFoundError): - # 2. Fallback: Define our own only if the real one fails - class _FallbackPyMongoError(Exception): - pass - - # Assign our fallback to the same local name - PyMongoError = _FallbackPyMongoError - -SHORT_CODE_PATTERN = re.compile(r"^[A-Za-z0-9]{6}$") -MAX_URL_LENGTH = 2048 - -app = FastAPI( - title="Tiny API", - version=__version__, - description="Tiny URL Shortener API built with FastAPI", -) - -api_v1 = APIRouter(prefix=os.getenv("API_VERSION", "/api/v1"), tags=["v1"]) - - -@app.exception_handler(Exception) -async def global_exception_handler(request: Request, exc: Exception): - traceback.print_exc() - return JSONResponse( - status_code=500, - content={"success": False, "error": "INTERNAL_SERVER_ERROR"}, - ) - - -class ShortenRequest(BaseModel): - url: str = Field(..., examples=["https://abcdkbd.com"]) - - -class ShortenResponse(BaseModel): - success: bool = True - input_url: str - short_code: str - created_on: datetime - - -class ErrorResponse(BaseModel): - success: bool = False - error: str - input_url: str - message: str - - -class VersionResponse(BaseModel): - version: str - - -# ------------------------------------------------- -# Home -# ------------------------------------------------- -@app.get("/", response_class=HTMLResponse, tags=["Home"]) -async def read_root(_: Request): - return """ - - - 🌙 tiny API 🌙 - - - -
-

🚀 tiny API

-

FastAPI backend for the Tiny URL shortener

- View API Documentation -
- - - """ - - -@api_v1.post("/shorten", response_model=ShortenResponse, status_code=201) -def shorten_url(payload: ShortenRequest): - print(" SHORTEN ENDPOINT HIT ", payload.url) - raw_url = payload.url.strip() - - if len(raw_url) > MAX_URL_LENGTH: - return JSONResponse( - status_code=413, content={"success": False, "input_url": payload.url} - ) - - original_url = sanitize_url(raw_url) - - if not is_valid_url(original_url): - return JSONResponse( - status_code=400, - content={ - "success": False, - "error": "INVALID_URL", - "input_url": payload.url, - "message": "Invalid URL", - }, - ) - - if db_data.urls is None: - short_code = generate_code() - - return { - "success": True, - "input_url": original_url, - "short_code": short_code, - "created_on": datetime.now(timezone.utc), - } - - try: - existing = db_data.urls.find_one({"original_url": original_url}) - except PyMongoError: - existing = None - - if existing: - return { - "success": True, - "input_url": original_url, - "short_code": existing["short_code"], - "created_on": existing["created_at"], - } - - short_code = generate_code() - try: - db_data.urls.insert_one( - { - "short_code": short_code, - "original_url": original_url, - "created_at": datetime.now(timezone.utc), - } - ) - except PyMongoError: - pass - - return { - "success": True, - "input_url": original_url, - "short_code": short_code, - "created_on": datetime.now(timezone.utc), - } - - -@app.get("/version") -def api_version(): - return {"version": __version__} - - -@api_v1.get("/help") -def get_help(): - return {"message": "Welcome to Tiny API. Visit /docs for API documentation."} - - -app.include_router(api_v1) +import os +import re +import traceback +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from fastapi import APIRouter, FastAPI, Request +from fastapi.responses import HTMLResponse, JSONResponse +from pydantic import BaseModel, Field + +if TYPE_CHECKING: + from pymongo.errors import PyMongoError +else: + try: + from pymongo.errors import PyMongoError + except ImportError: + + class PyMongoError(Exception): + pass + + +from app import __version__ +from app.utils import data as db_data +from app.utils.cache import get_short_from_cache, set_cache_pair +from app.utils.helper import generate_code, is_valid_url, sanitize_url + +SHORT_CODE_PATTERN = re.compile(r"^[A-Za-z0-9]{6}$") +MAX_URL_LENGTH = 2048 + +app = FastAPI( + title="Tiny API", + version=__version__, + description="Tiny URL Shortener API built with FastAPI", +) + +api_v1 = APIRouter(prefix=os.getenv("API_VERSION", "/api/v1"), tags=["v1"]) + + +@app.exception_handler(Exception) +async def global_exception_handler(request: Request, exc: Exception): + traceback.print_exc() + return JSONResponse( + status_code=500, + content={"success": False, "error": "INTERNAL_SERVER_ERROR"}, + ) + + +class ShortenRequest(BaseModel): + url: str = Field(..., examples=["https://abcdkbd.com"]) + + +class ShortenResponse(BaseModel): + success: bool = True + input_url: str + short_code: str + created_on: datetime + + +class ErrorResponse(BaseModel): + success: bool = False + error: str + input_url: str + message: str + + +class VersionResponse(BaseModel): + version: str + + +# ------------------------------------------------- +# Home +# ------------------------------------------------- +@app.get("/", response_class=HTMLResponse, tags=["Home"]) +async def read_root(_: Request): + return """ + + + 🌙 tiny API 🌙 + + + +
+

🚀 tiny API

+

FastAPI backend for the Tiny URL shortener

+ View API Documentation +
+ + + """ + + +@api_v1.post("/shorten", response_model=ShortenResponse, status_code=201) +def shorten_url(payload: ShortenRequest): + print(" SHORTEN ENDPOINT HIT ", payload.url) + raw_url = payload.url.strip() + + if len(raw_url) > MAX_URL_LENGTH: + return JSONResponse( + status_code=413, content={"success": False, "input_url": payload.url} + ) + + original_url = sanitize_url(raw_url) + + if not is_valid_url(original_url): + return JSONResponse( + status_code=400, + content={ + "success": False, + "error": "INVALID_URL", + "input_url": payload.url, + "message": "Invalid URL", + }, + ) + + if db_data.collection is None: + cached_short = get_short_from_cache(original_url) + short_code = cached_short or generate_code() + set_cache_pair(short_code, original_url) + return { + "success": True, + "input_url": original_url, + "short_code": short_code, + "created_on": datetime.now(timezone.utc), + } + + try: + existing = db_data.collection.find_one({"original_url": original_url}) + except PyMongoError: + existing = None + + if existing: + return { + "success": True, + "input_url": original_url, + "short_code": existing["short_code"], + "created_on": existing["created_at"], + } + + short_code = generate_code() + try: + db_data.collection.insert_one( + { + "short_code": short_code, + "original_url": original_url, + "created_at": datetime.now(timezone.utc), + } + ) + except PyMongoError: + pass + + return { + "success": True, + "input_url": original_url, + "short_code": short_code, + "created_on": datetime.now(timezone.utc), + } + + +@app.get("/version") +def api_version(): + return {"version": __version__} + + +@api_v1.get("/help") +def get_help(): + return {"message": "Welcome to Tiny API. Visit /docs for API documentation."} + + +app.include_router(api_v1) diff --git a/app/db/__init__.py b/app/db/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/db/data.py b/app/db/data.py deleted file mode 100644 index 36e476c..0000000 --- a/app/db/data.py +++ /dev/null @@ -1,47 +0,0 @@ -import os -from typing import Any - -# --- DEFENSIVE IMPORT --- -try: - from pymongo import MongoClient - - MONGO_INSTALLED = True -except ImportError: - # This allows the app to start even if 'pip install pymongo' wasn't run - MONGO_INSTALLED = False - -client: Any = None -db: Any = None -urls: Any = None -url_stats: Any = None - - -def connect_db(): - global client, db, urls, url_stats - - # 1. Check if the library is even there - if not MONGO_INSTALLED: - print("⚠️ pymongo is not installed. Running in NO-DB mode.") - return False - - # 2. Check if the config is there - MONGO_URI = os.getenv("MONGO_URI") - DB_NAME = os.getenv("DATABASE_NAME", "tiny_url") - - if not MONGO_URI: - print("⚠️ MONGO_URI missing. Running in NO-DB mode.") - return False - - try: - client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=2000) - client.admin.command("ping") - - db = client[DB_NAME] - urls = db["urls"] - url_stats = db["url_stats"] - - print(f"✅ MongoDB connected: '{DB_NAME}'") - return True - except Exception as e: - print(f"⚠️ MongoDB connection failed: {e}. Running in NO-DB mode.") - return False diff --git a/app/main.py b/app/main.py index 15c0519..09bb540 100644 --- a/app/main.py +++ b/app/main.py @@ -1,235 +1,241 @@ -import datetime -import os -from contextlib import asynccontextmanager -from pathlib import Path -from typing import Any, Optional - -from fastapi import FastAPI, Form, Request -from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates -from starlette.middleware.sessions import SessionMiddleware - -from app.api.fast_api import app as api_app -from app.db import data as db_data -from app.utils.config import load_env -from app.utils.helper import ( - format_date, - generate_code, - is_valid_url, - sanitize_url, -) -from app.utils.qr import generate_qr_with_logo - -load_env() # ✅ load env ONCE - -RED = "\033[31m" -GREEN = "\033[32m" -BLUE = "\033[34m" -RESET = "\033[0m" - -app_name = os.getenv("APP_NAME", "TinyURL") -print(f"Environment loaded as {BLUE}{app_name}{RESET}") - -# 1. MongoDB error handling: Try to import the real exception class first -PyMongoError: Any -try: - from pymongo.errors import PyMongoError as _RealPyMongoError - - PyMongoError = _RealPyMongoError -except (ImportError, ModuleNotFoundError): - # 2. Fallback: Define our own only if the real one fails - class _FallbackPyMongoError(Exception): - pass - - # Assign our fallback to the same local name - PyMongoError = _FallbackPyMongoError - - -# ----------------------------- -# Lifespan: env + DB connect ONCE -# ----------------------------- -@asynccontextmanager -async def lifespan(app: FastAPI): - connected = db_data.connect_db() # ✅ connect DB ONCE - app.state.db_available = connected - yield - - -app = FastAPI(title="TinyURL", lifespan=lifespan) - - -app.add_middleware(SessionMiddleware, secret_key="super-secret-key") - - -BASE_DIR = Path(__file__).resolve().parent -STATIC_DIR = BASE_DIR / "static" - -app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") -templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) - - -def db_available(request: Request) -> bool: - return getattr(request.app.state, "db_available", False) - - -def build_short_url(short_code: str, request_host_url: str) -> str: - base_url = os.getenv("DOMAIN", request_host_url).rstrip("/") - return f"{base_url}/{short_code}" - - -@app.get("/", response_class=HTMLResponse) -async def index(request: Request): - session = request.session - - new_short_url = session.pop("new_short_url", None) - qr_enabled = session.pop("qr_enabled", False) - qr_type = session.pop("qr_type", "short") - original_url = session.pop("original_url", None) - short_code = session.pop("short_code", None) - info_message = session.pop("info_message", None) - error = session.pop("error", None) - - qr_image = None - qr_data = None - - if qr_enabled and new_short_url and short_code: - qr_data = new_short_url if qr_type == "short" else original_url - qr_filename = f"{short_code}.png" - generate_qr_with_logo(qr_data, qr_filename) - qr_image = f"/static/qr/{qr_filename}" - - all_urls = [] - if db_available(request) and db_data.urls is not None: - try: - all_urls = list(db_data.urls.find().sort("created_at", -1)) - except PyMongoError: - all_urls = [] - - return templates.TemplateResponse( - "index.html", - { - "request": request, - "urls": all_urls, - "new_short_url": new_short_url, - "error": error, - "info_message": info_message, - "qr_data": qr_data, - "qr_enabled": qr_enabled, - "qr_type": qr_type, - "qr_image": qr_image, - "db_available": db_available(request), - }, - ) - - -@app.post("/", response_class=RedirectResponse) -async def create_short_url( - request: Request, - original_url: str = Form(""), - generate_qr: Optional[str] = Form(None), - qr_type: str = Form("short"), -): - session = request.session - qr_enabled = generate_qr == "on" - original_url = sanitize_url(original_url) - - if not original_url: - session["error"] = "URL cannot be empty." - return RedirectResponse("/", status_code=303) - - if not is_valid_url(original_url): - session["error"] = ( - "Please enter a valid URL (must start with http:// or https://)." - ) - return RedirectResponse("/", status_code=303) - - short_code = None - - if db_available(request) and db_data.urls is not None: - try: - existing = db_data.urls.find_one({"original_url": original_url}) - if existing: - short_code = existing["short_code"] - session["info_message"] = ( - "Already shortened before — using existing short URL." - ) - except PyMongoError: - pass - - if not short_code: - short_code = generate_code() - - if db_available(request) and db_data.urls is not None: - try: - db_data.urls.insert_one( - { - "short_code": short_code, - "original_url": original_url, - "created_at": datetime.datetime.utcnow(), - "visit_count": 0, - } - ) - except PyMongoError: - pass - - new_short_url = build_short_url(short_code, str(request.base_url)) - session.update( - { - "new_short_url": new_short_url, - "qr_enabled": qr_enabled, - "qr_type": qr_type, - "original_url": original_url, - "short_code": short_code, - } - ) - - return RedirectResponse("/", status_code=303) - - -@app.get("/recent", response_class=HTMLResponse) -async def recent_urls(request: Request): - recent_urls_list = [] - if db_available(request) and db_data.urls is not None: - try: - recent_urls_list = list(db_data.urls.find().sort("created_at", -1)) - except PyMongoError: - pass - - return templates.TemplateResponse( - "recent.html", - {"request": request, "urls": recent_urls_list, "format_date": format_date}, - ) - - -@app.post("/delete/{short_code}") -async def delete_url(request: Request, short_code: str): - if db_available(request) and db_data.urls is not None: - try: - db_data.urls.delete_one({"short_code": short_code}) - except PyMongoError: - return PlainTextResponse("Database connection lost.", status_code=503) - - return RedirectResponse("/recent", status_code=303) - - -@app.get("/{short_code}") -async def redirect_short(request: Request, short_code: str): - if not db_available(request) or db_data.urls is None: - return PlainTextResponse("Database is not connected.", status_code=503) - - try: - doc = db_data.urls.find_one_and_update( - {"short_code": short_code}, - {"$inc": {"visit_count": 1}}, - ) - except PyMongoError: - return PlainTextResponse("Database connection lost.", status_code=503) - - if not doc: - return PlainTextResponse("Invalid or expired short URL", status_code=404) - - return RedirectResponse(doc["original_url"]) - - -app.mount("/api", api_app) +from contextlib import asynccontextmanager +from pathlib import Path +from typing import Optional + +from fastapi import FastAPI, Form, Request +from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from starlette.middleware.sessions import SessionMiddleware + +from app.api.fast_api import app as api_app +from app.utils import data as db_data +from app.utils.cache import ( + get_from_cache, + get_recent_from_cache, + get_short_from_cache, + rev_cache, + set_cache_pair, + url_cache, +) +from app.utils.config import DOMAIN, MAX_RECENT_URLS, SESSION_SECRET +from app.utils.helper import ( + format_date, + generate_code, + is_valid_url, + sanitize_url, +) +from app.utils.qr import generate_qr_with_logo + + + +# ----------------------------- +# Lifespan: env + DB connect ONCE +# ----------------------------- +@asynccontextmanager +async def lifespan(app: FastAPI): + db_data.connect_db() + yield + + +app = FastAPI(title="TinyURL", lifespan=lifespan) +app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET) + +BASE_DIR = Path(__file__).resolve().parent +STATIC_DIR = BASE_DIR / "static" + +app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") +templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) + + +def build_short_url(short_code: str, request_host_url: str) -> str: + base_url = DOMAIN.rstrip("/") + return f"{base_url}/{short_code}" + + +@app.get("/", response_class=HTMLResponse) +async def index(request: Request): + session = request.session + + new_short_url = session.pop("new_short_url", None) + qr_enabled = session.pop("qr_enabled", False) + qr_type = session.pop("qr_type", "short") + original_url = session.pop("original_url", None) + short_code = session.pop("short_code", None) + info_message = session.pop("info_message", None) + error = session.pop("error", None) + + qr_image = None + qr_data = None + + if qr_enabled and new_short_url and short_code: + qr_data = new_short_url if qr_type == "short" else original_url + qr_filename = f"{short_code}.png" + qr_dir = STATIC_DIR / "qr" + qr_dir.mkdir(parents=True, exist_ok=True) + generate_qr_with_logo(qr_data, str(qr_dir / qr_filename)) + qr_image = f"/static/qr/{qr_filename}" + + all_urls = db_data.get_recent_urls(MAX_RECENT_URLS) or get_recent_from_cache( + MAX_RECENT_URLS + ) + + return templates.TemplateResponse( + "index.html", + { + "request": request, + "urls": all_urls, + "new_short_url": new_short_url, + "qr_image": qr_image, + "qr_data": qr_data, + "qr_enabled": qr_enabled, + "original_url": original_url, + "error": error, + "info_message": info_message, + "db_available": db_data.get_collection() is not None, + }, + ) + + +@app.post("/shorten", response_class=RedirectResponse) +async def create_short_url( + request: Request, + original_url: str = Form(""), + generate_qr: Optional[str] = Form(None), + qr_type: str = Form("short"), +): + session = request.session + qr_enabled = bool(generate_qr) + original_url = sanitize_url(original_url) + + if not original_url: + session["error"] = "URL cannot be empty." + return RedirectResponse("/", status_code=303) + + if not is_valid_url(original_url): + session["error"] = ( + "Please enter a valid URL (must start with http:// or https://)." + ) + return RedirectResponse("/", status_code=303) + + # 1. Try Cache First + short_code: Optional[str] = get_short_from_cache(original_url) + + if short_code: + session["info_message"] = "Already shortened before — fetched from cache." + else: + # 2. Try Database + existing = db_data.find_by_original_url(original_url) + # Pull the value and check it in one go + db_code = existing.get("short_code") if existing else None + if isinstance(db_code, str): + short_code = db_code + set_cache_pair(short_code, original_url) # Cache it for future + session["info_message"] = ( + "Already shortened before — fetched from database." + ) + + # 3. Generate New if still None + if not short_code: + short_code = generate_code() + set_cache_pair(short_code, original_url) + db_data.insert_url(short_code, original_url) + + # --- TYPE GUARD FOR MYPY --- + # At this point, short_code could still technically be Optional[str] + # if generate_code() wasn't strictly typed. We cast or assert. + if not isinstance(short_code, str): + # This acts as a final safety net for production + session["error"] = "Internal server error: Code generation failed." + return RedirectResponse("/", status_code=303) + + # Mypy now knows short_code is strictly 'str' + new_short_url = build_short_url(short_code, DOMAIN) + + session.update( + { + "new_short_url": new_short_url, + "qr_enabled": qr_enabled, + "qr_type": qr_type, + "original_url": original_url, + "short_code": short_code, + } + ) + + return RedirectResponse("/", status_code=303) + + +@app.get("/recent", response_class=HTMLResponse) +async def recent_urls(request: Request): + recent_urls_list = db_data.get_recent_urls( + MAX_RECENT_URLS + ) or get_recent_from_cache(MAX_RECENT_URLS) + + normalized = [] + for item in recent_urls_list: + normalized.append( + { + "short_code": item.get("short_code"), + "original_url": item.get("original_url"), + "created_at": item.get("created_at"), + "visit_count": item.get("visit_count", 0), + } + ) + + return templates.TemplateResponse( + "recent.html", + { + "request": request, + "urls": normalized, + "format_date": format_date, + }, + ) + + +@app.post("/delete/{short_code}") +async def delete_url(request: Request, short_code: str): + db_data.delete_by_short_code(short_code) + + cached = url_cache.pop(short_code, None) + if cached: + rev_cache.pop(cached.get("url"), None) + + return PlainTextResponse("", status_code=204) + + +@app.get("/{short_code}") +async def redirect_short(request: Request, short_code: str): + doc = db_data.increment_visit(short_code) + + cached_url = get_from_cache(short_code) + if cached_url: + return RedirectResponse(cached_url) + + if doc: + set_cache_pair(short_code, doc["original_url"]) + return RedirectResponse(doc["original_url"]) + if db_data.get_collection() is None: + return PlainTextResponse("Database is not connected.", status_code=503) + + return PlainTextResponse("Invalid or expired short URL", status_code=404) + + +@app.get("/coming-soon", response_class=HTMLResponse) +async def coming_soon(request: Request): + return templates.TemplateResponse("coming-soon.html", {"request": request}) + + +app.mount("/api", api_app) + + +@app.get("/_debug/cache") +async def debug_cache(): + return { + "url_cache": url_cache, + "rev_cache": rev_cache, + "recent_from_cache": get_recent_from_cache(MAX_RECENT_URLS), + "size": { + "url_cache": len(url_cache), + "rev_cache": len(rev_cache), + }, + } diff --git a/app/static/css/tiny.css b/app/static/css/tiny.css new file mode 100644 index 0000000..873f075 --- /dev/null +++ b/app/static/css/tiny.css @@ -0,0 +1,415 @@ +:root { + --bg: #0a0a0c; + --glass: rgba(255, 255, 255, 0.03); + --glass-border: rgba(255, 255, 255, 0.07); + --accent: #6366f1; + --text-primary: #ffffff; + --text-secondary: #a1a1aa; +} + +body { + background: var(--bg); + color: var(--text-primary); + font-family: "Inter", system-ui, sans-serif; + margin: 0; + overflow-x: hidden; + background-image: radial-gradient(circle at 50% -20%, #1e1e2e 0%, transparent 50%); +} + +body { + background: var(--bg); + color: var(--text-primary); + font-family: "Inter", system-ui, sans-serif; + margin: 0; + overflow-x: hidden; + background-image: radial-gradient(circle at 50% -20%, #1e1e2e 0%, transparent 50%); +} + +/* Light theme overrides */ +body.light-theme { + --bg: #f9fafb; + --glass: rgba(0, 0, 0, 0.03); + --glass-border: rgba(0, 0, 0, 0.07); + --accent: #2563eb; + --text-primary: #111827; + --text-secondary: #4b5563; + + /* Remove or soften the dark gradient */ + background-image: none; + /* clean white background */ + /* Or use a subtle light gradient if you prefer */ + /* background-image: radial-gradient(circle at 50% -20%, #e5e7eb 0%, transparent 50%); */ +} + +/* Layout */ +.main-layout { + max-width: 900px; + margin: 0 auto; + padding: 4rem 1rem; + display: flex; + flex-direction: column; + gap: 2rem; +} + +.site-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.5rem; + background: var(--glass); + border-bottom: 1px solid var(--glass-border); + backdrop-filter: blur(10px); +} + +.header-left, +.header-right { + display: flex; + gap: 1rem; + align-items: center; +} + +.header-center { + flex: 1; + text-align: center; +} + +.logo { + margin: 0; + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); +} + +.icon-btn { + background: none; + border: none; + color: var(--text-primary); + font-size: 1.2rem; + cursor: pointer; + transition: color 0.3s; +} + +.icon-btn:hover { + color: var(--accent); +} + +@media (max-width: 600px) { + .logo { + font-size: 1.2rem; + } +} + +/* Hero input */ +.hero-input-card { + width: 100%; + background: var(--glass); + backdrop-filter: blur(20px); + border: 1px solid var(--glass-border); + border-radius: 2rem; + padding: 2rem; +} + +.hero-input-card h1 { + font-size: 2.5rem; + font-weight: 800; + text-align: center; + margin-bottom: 1.5rem; + background: linear-gradient(to right, var(--text-primary), var(--text-secondary)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.input-wrapper { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.input-wrapper input { + flex: 1; + min-width: 200px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--glass-border); + border-radius: 1.5rem; + padding: 1rem; + font-size: 1rem; + color: var(--text-primary); +} + +.btn-primary { + padding: 0.8rem 2rem; + border-radius: 1.5rem; + background: var(--accent); + color: #fff; + font-weight: bold; + border: none; + cursor: pointer; + transition: background 0.3s; +} + +.btn-primary:hover { + background: #4f46e5; +} + +.options-row { + margin-top: 1rem; + display: flex; + flex-wrap: wrap; + gap: 1rem; + font-size: 0.85rem; + color: var(--text-secondary); +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; +} + +/* Result card */ +.result-card { + width: 100%; + background: linear-gradient(145deg, rgba(99, 102, 241, 0.1), rgba(0, 0, 0, 0)); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: 2rem; + padding: 2rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 2rem; +} + +.result-content { + display: flex; + align-items: center; + gap: 1.5rem; +} + +.qr-image { + width: 80px; + height: 80px; + border-radius: 1rem; + background: #fff; + padding: 0.5rem; +} + +.ready-label { + font-size: 0.8rem; + text-transform: uppercase; + color: var(--accent); + font-weight: 800; +} + +.short-url a { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); + text-decoration: none; +} + +.result-actions { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.5rem; +} + +.btn-copy { + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--glass-border); + color: var(--text-primary); + padding: 0.8rem 1.5rem; + border-radius: 1rem; + cursor: pointer; + transition: all 0.3s ease; +} + +.btn-copy:hover { + border-color: var(--accent); + color: var(--accent); +} + +.download-link { + font-size: 0.75rem; + color: var(--text-secondary); + text-decoration: none; +} + +.download-link:hover { + color: var(--accent); +} + +/* Recent tray */ +.recent-tray { + width: 100%; + margin-top: 2rem; +} + +.recent-header { + display: flex; + justify-content: space-between; + margin-bottom: 1rem; + padding: 0 0.5rem; +} + +.scroll-container { + display: flex; + gap: 1rem; + overflow-x: auto; + padding: 1rem 0; +} + +.scroll-container::-webkit-scrollbar { + display: none; +} + +.recent-item { + min-width: 220px; + background: var(--glass); + border: 1px solid var(--glass-border); + padding: 1rem; + border-radius: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + flex-shrink: 0; +} + +.short-code { + color: var(--accent); + font-weight: bold; +} + +.original-url { + color: var(--text-secondary); + font-size: 0.8rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Footer */ +.big-footer { + background: rgba(255, 255, 255, 0.01); + border-top: 1px solid var(--glass-border); + padding: 4rem 1rem 2rem; + margin-top: 4rem; +} + +.footer-grid { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr; + gap: 2rem; +} + +.footer-brand h3 { + font-size: 1.5rem; + margin-bottom: 1rem; +} + +.footer-brand p { + color: var(--text-secondary); + line-height: 1.6; + max-width: 320px; +} + +.footer-col h4 { + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.1em; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.footer-col ul { + list-style: none; + padding: 0; + margin: 0; +} + +.footer-col ul li { + margin-bottom: 0.8rem; +} + +.footer-col ul li a { + color: var(--text-secondary); + text-decoration: none; + font-size: 0.9rem; + transition: color 0.2s; +} + +.footer-col ul li a:hover { + color: var(--accent); +} + +.footer-bottom { + max-width: 1200px; + margin: 2rem auto 0; + padding-top: 1rem; + border-top: 1px solid var(--glass-border); + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + color: var(--text-secondary); + font-size: 0.8rem; +} + +.footer-meta { + display: flex; + gap: 1rem; +} + +.footer-meta a { + color: inherit; + text-decoration: none; +} + +.footer-meta a:hover { + color: var(--accent); +} + +/* Responsive adjustments */ +@media (max-width: 900px) { + .footer-grid { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 700px) { + .result-card { + flex-direction: column; + align-items: flex-start; + } + + .result-actions { + align-items: flex-start; + } + + .recent-item { + min-width: 180px; + } +} + +@media (max-width: 600px) { + .hero-input-card h1 { + font-size: 2rem; + } + + .short-url a { + font-size: 1.2rem; + } + + .footer-grid { + grid-template-columns: 1fr; + } + + .footer-bottom { + flex-direction: column; + gap: 1rem; + text-align: center; + } +} diff --git a/app/static/qr/zSDrr0.png b/app/static/qr/zSDrr0.png deleted file mode 100644 index 0b8b9fb..0000000 Binary files a/app/static/qr/zSDrr0.png and /dev/null differ diff --git a/app/static/style.css b/app/static/style.css index ff47cab..b754687 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -1,815 +1,815 @@ -html, -body { - height: 100%; - margin: 0; - font-family: Arial; - padding: 0; - font-family: "Poppins", system-ui, Arial, sans-serif; - background: var(--bg); - background-size: cover; - background-position: center; - background-size: cover; - background-position: center; -} -input { - width: 70%; - margin-top: 2px; - margin-bottom: 2px; - font-size: 16px; -} -.admin-box { - margin: 120px auto 60px; - /* space from header + footer */ -} -.app-layout { - min-height: 100vh; - display: flex; - flex-direction: column; - margin-top: var(--header-height); -} -button { - padding: 8px; - margin: 5px; -} -.error-box { - margin-bottom: 15px; - padding: 10px; - color: #ff4d4d; - border-radius: 8px; - font-weight: 600; -} - -.dark-theme h1 { - background: linear-gradient(90deg, #ffffff, #dddddd, #ffffff); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - text-shadow: 0px 0px 10px rgba(255, 255, 255, 0.4); -} - -.dark-theme p { - background: linear-gradient(90deg, #ffffff, #dddddd, #ffffff); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - text-shadow: 0px 0px 10px rgba(255, 255, 255, 0.4); -} -.dark-theme { - --bg-overlay: rgba(0, 0, 0, 0.75); - --glass-bg: rgba(0, 0, 0, 0.4); - --text-color: #fff; - --input-bg: rgba(50, 50, 50, 0.8); - --input-text-color: #fff; -} - -@keyframes pop { - 0% { - transform: scale(0.7); - opacity: 0; - } - 100% { - transform: scale(1); - opacity: 1; - } -} -/* INPUT CONTAINER */ -.input-field { - flex: 1 1 700px; - display: flex; - align-items: center; - gap: 12px; - border-radius: 12px; - border: 2px solid rgb(6, 0, 0); - background: transparent; /* IMPORTANT */ - padding: 12px 12px; -} -.dark-theme .input-field { - border-color: #ffffff; -} -/* INPUT ITSELF */ -.input-field input[type="text"] { - width: 100%; - border: none; - outline: none; - background-color: transparent !important; - background-image: none !important; - box-shadow: none !important; - font-size: 23px; -} - -.input-field input { - color: #000 !important; -} - -.dark-theme .input-field input { - color: #fff !important; -} - -.input-field input:-webkit-autofill, -.input-field input:-webkit-autofill:hover, -.input-field input:-webkit-autofill:focus, -.input-field input:-webkit-autofill:active { - -webkit-box-shadow: 0 0 0 1000px transparent inset !important; - box-shadow: 0 0 0 1000px transparent inset !important; - background-color: transparent !important; - background-image: none !important; - transition: background-color 9999s ease-in-out 0s; -} - -.input-field input:-webkit-autofill { - -webkit-text-fill-color: #000 !important; -} - -.dark-theme .input-field input:-webkit-autofill { - -webkit-text-fill-color: #fff !important; -} -.input-field input::selection, -.input-field input::-moz-selection { - background: transparent; - color: inherit; -} -.short-code { - color: #0a0000; /* blue like links */ - font-weight: 700; -} - -.app-header { - position: fixed; - top: 0; - left: 0; - width: 97%; - height: 55px; - background: white; - display: flex; - align-items: center; - padding: 0 28px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); - z-index: 1000; -} - -/* Dark mode */ -.dark-theme .app-header { - background: linear-gradient(180deg, #0b1220, #050b14); -} - -footer { - margin-top: 0; -} - -body.dark-theme, -body.dark-theme .page, -body.dark-theme main, -body.dark-theme section { - background: #0f1720 !important; -} - -.header-left { - display: flex; - align-items: center; - gap: 12px; -} - -.app-logo { - width: 36px; - height: 36px; - background: linear-gradient(135deg, #2563eb, #5ab9ff); - color: white; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - font-size: 18px; -} - -.app-name { - font-size: 20px; - font-weight: 700; - color: #111827; -} - -.dark-theme .app-name { - color: #f8fafc; -} - -.header-nav { - position: absolute; - left: 50%; - transform: translateX(-50%); - display: flex; - gap: 26px; -} - -.nav-link { - text-decoration: none; - color: #111827; - font-weight: 500; - position: relative; -} - -.dark-theme .nav-link { - color: #e5e7eb; -} - -.nav-link:hover { - color: #2563eb; -} - -.nav-link.active::after { - content: ""; - position: absolute; - bottom: -6px; - left: 0; - width: 100%; - height: 2px; - background: #111827; -} - -.dark-theme .nav-link.active::after { - background: #f8fafc; -} - -.header-right { - margin-left: auto; - display: flex; - align-items: center; -} - -:root { - --header-height: 55px; - --bg: #eefaf8; - --card: rgba(255, 255, 255, 0.95); - --muted: #7b8b8a; - --accent-1: #5ab9ff; - --accent-2: #4cb39f; - --accent-grad: linear-gradient(90deg, #4cb39f, #5ab9ff); - --success: #2fb06e; - --glass: rgba(255, 255, 255, 0.85); -} - -.dark-theme { - --bg-overlay: rgba(0, 0, 0, 0.75); - --glass-bg: rgba(0, 0, 0, 0.4); - --text-color: #f3f3f3; - --input-bg: rgba(11, 10, 10, 0.8); - --button-bg: linear-gradient(90deg, #4444ff, #2266ff); - --recent-bg: rgba(255, 255, 255, 0.1); -} - -/* Preserve your dark theme variables too */ -body.dark-theme { - --bg: #0f1720; - --card: rgba(10, 14, 18, 0.92); - --muted: #9aa7a6; -} - -.page { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - gap: 1rem; - padding: 2rem; - min-height: 80vh; -} - -.theme-toggle { - background: transparent; - border: none; - cursor: pointer; - padding: 8px; - border-radius: 8px; - font-weight: 700; - background: var(--card); -} - -/* Hero */ -.hero { - width: 100%; - max-width: 1100px; - background: transparent; - text-align: center; - padding: 10px; -} - -.hero h1 { - margin: 10px 0 14px; - font-size: 36px; - line-height: 1.05; - color: #000606; -} - -.hero p { - margin: var(--bg-overlay); - color: var(--muted); - max-width: 820px; - margin-left: auto; - margin-right: auto; - color: #000606; -} - -/* Main card & input */ -.card { - width: 100%; - max-width: 1100px; - background: var(--card); - border-radius: 14px; - padding: 15px; - box-shadow: 0 18px 50px rgba(8, 24, 24, 0.06); -} - -.cta { - min-width: 220px; - padding: 14px 22px; - border-radius: 12px; - border: none; - color: rgb(12, 1, 1); - font-weight: 700; - cursor: pointer; - background: var(--accent-grad); - box-shadow: 0 12px 28px rgba(77, 163, 185, 0.12); -} - -.small-action { - display: flex; - align-items: center; - gap: 8px; - color: var(--muted); - margin-top: 10px; -} - -.result { - margin-top: 26px; - background: white; - border-radius: 12px; - padding: 20px; - border: 1px solid rgba(22, 60, 55, 0.03); - box-shadow: 0 8px 28px rgba(7, 20, 20, 0.03); -} - -.result-header { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 12px; -} - -.result-header .dot { - width: 30px; - height: 30px; - background: var(--success); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - color: white; - font-weight: 700; -} - -.short-actions { - display: flex; - justify-content: center; - align-items: center; - gap: 8px; - padding: 10px 14px; - border-radius: 12px; - background: linear-gradient(180deg, rgba(75, 194, 176, 0.06), rgba(94, 207, 255, 0.04)); -} - -.short-box input { - align-items: center; - padding: 10px; - font-size: 15px; -} - -.btn-copy { - border: none; - padding: 10px 14px; - border-radius: 8px; - color: white; - font-weight: 700; - cursor: pointer; -} - -.btn-share { - background: #f2f5f5; - border: none; - padding: 10px 14px; - border-radius: 8px; - color: #0b2b2a; - font-weight: 700; - cursor: pointer; - margin-left: 6px; -} - -.meta-row { - align-items: center; - justify-content: center; - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2px; - padding: 16px; - margin-top: 1px; - align-items: top; - color: black; -} -.result-body { - margin-top: 30px; - - display: flex; - flex-direction: column; - align-items: center; - text-align: center; -} - -.qr-block { - text-align: center; - padding-top: 8px; -} - -.qr-block img { - height: 15rem; - align-items: center; - aspect-ratio: 1; - box-shadow: 0 10px 20px rgba(10, 20, 30, 0.06); - outline: 2px solid green; - outline-offset: 4px; -} - -.download-qr { - display: inline-block; - margin-top: 12px; - text-decoration: none; - color: var(--accent-1); - font-weight: 700; -} - -.action-row { - display: flex; - justify-content: right; - align-items: right; -} - -.action-secondary { - background: #f6fbfb; - border: 1px solid rgba(0, 0, 0, 0.03); - border-radius: 10px; - cursor: pointer; - font-weight: 700; -} - -/* Force Generate QR to stay on one line */ -.qr-inline { - display: inline-flex; - align-items: center; - gap: 8px; - white-space: nowrap; -} - -.qr-inline input { - margin: 0; -} - -/* Responsive */ -@media (max-width: 880px) { - .input-row { - flex-direction: column; - } - - .cta { - width: 100%; - } - - .meta-row { - grid-template-columns: 1fr; - } -} -.result-title { - font-weight: 700; - color: #0e34f6; -} - -.dark-theme .result-title { - color: #150cff; -} - -footer { - min-height: auto; -} - -.app-footer { - background: white; - color: #e5e7eb; - padding: 8px 10px; - margin-top: auto; - position: relative; -} -.dark-theme .app-footer { - background: linear-gradient(180deg, #0b1220, #050b14); -} - -.footer-container { - margin: auto; - display: flex; - gap: 60px; - justify-content: space-between; - flex-wrap: wrap; -} - -.footer-brand { - max-width: 420px; -} - -.footer-logo { - width: 42px; - height: 42px; - background: linear-gradient(135deg, #2563eb, #5ab9ff); - border-radius: 14px; - display: flex; - align-items: center; - justify-content: center; - font-size: 22px; -} -.dark-theme .footer-brand h3, -.dark-theme .footer-brand p, -.dark-theme .footer-col h4, -.dark-theme .app-footer a, -.dark-theme .footer-bottom { - color: #f8fafc; -} - -.footer-brand h3 { - margin: 0; - color: #000000; - font-size: 22px; - font-weight: 700; -} - -.footer-brand p { - margin-top: 8px; - color: #000000; - line-height: 1.6; - font-size: 14px; -} - -/* MAIN CONTENT */ - -/* FOOTER */ -.app-footer { - margin-top: auto; -} - -/* GitHub button */ -.github-btn { - display: inline-flex; - align-items: center; - gap: 2px; - margin-top: 1px; - padding: 10px 16px; - border-radius: 8px; - background: rgba(255, 255, 255, 0.06); - color: #000000; - text-decoration: none; - font-weight: 600; - transition: all 0.25s ease; -} - -.github-btn:hover { - background: black(11, 1, 1); - transform: translateY(-2px); -} - -.footer-links { - display: flex; - gap: 80px; - flex-wrap: wrap; -} - -.footer-col h4 { - margin-bottom: 14px; - font-size: 16px; - color: #000000; - font-weight: 700; -} - -.footer-col a { - display: block; - text-decoration: none; - color: #000000; - margin-bottom: 10px; - font-size: 14px; - transition: color 0.2s ease; -} - -.footer-col a:hover { - text-decoration: underline; -} - -/* Bottom */ -.footer-bottom { - margin-top: 10px; - border-top: 1px solid rgba(255, 255, 255, 0.153); - padding-top: 8px; - padding-bottom: 1px; - text-align: center; - font-size: 14px; - color: #080808; -} -.footer-bottom a { - color: #030000; - font-weight: 600; - text-decoration: none; -} - -.footer-bottom a:hover { - text-decoration: underline; -} - -/* Responsive */ -@media (max-width: 768px) { - .footer-container { - flex-direction: column; - gap: 40px; - } - - .footer-links { - gap: 40px; - } -} -/* REMOVE white line above footer in dark mode */ -footer { - margin-top: 0 !important; -} -.recent-table-wrapper { - margin-top: 20px; - width: 100%; - overflow-x: auto; -} - -.recent-table { - width: 100%; - border-collapse: collapse; - border-radius: 12px; - overflow: hidden; -} - -.recent-table thead { - background: rgb(0, 0, 0); -} -.recent-table th { - color: rgb(0, 0, 0); - padding: 8px 14px; - text-align: left; - font-size: 16px; -} -.short-code a { - color: #2563eb; - font-weight: 600; - text-decoration: none; -} - -.short-code a:hover { - color: #1d4ed8; - text-decoration: underline; -} -.recent-table td { - color: rgb(34, 48, 77); - padding: 10px 14px; - text-align: left; - font-size: 14px; -} - -.created-time { - font-size: 14px; - color: #374151; - white-space: nowrap; -} - -.time-ago { - color: #374151; - font-size: 13px; - margin-left: 2px; -} -.recent-table th { - font-weight: 700; -} - -.recent-table tbody tr, -th { - background: rgb(255, 255, 255); - border-bottom: 1px solid rgb(0, 0, 0); -} -.dark-theme.recent-table tbody tr, -td { - background: rgba(255, 255, 255, 0.04); - border-bottom: 1px solid rgb(0, 0, 0); -} -.recent-table tbody tr:hover { - background: rgb(196, 196, 196); -} - -/* Short code */ -.short-code { - font-weight: 700; -} - -.original-url { - color: #22c55e; - word-break: break-all; -} - -/* Action buttons */ -.action-col { - display: flex; - gap: 10px; -} - -.action-btn { - width: 36px; - height: 36px; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - text-decoration: none; - font-size: 16px; - transition: 0.2s ease; -} - -.open-btn { - background: #3b82f6; - color: #fff; -} - -.delete-btn { - background: #ef4444; - color: #fff; -} - -.recent-table-wrapper { - margin-bottom: 20px; -} -/* ========================= - Coming Soon Page -========================= */ - -.coming-soon-page { - display: flex; - align-items: center; - justify-content: center; - padding: 120px 20px 60px; -} - -.coming-soon-card { - max-width: 520px; - width: 100%; - background: var(--card); - border-radius: 16px; - padding: 50px 40px; - text-align: center; - box-shadow: 0 20px 50px rgba(0, 0, 0, 0.08); -} - -.coming-icon { - font-size: 48px; - margin-bottom: 18px; -} - -.coming-soon-card h1 { - font-size: 34px; - margin-bottom: 14px; - color: #000; -} - -.dark-theme .coming-soon-card h1 { - color: #fff; -} - -.coming-soon-card p { - font-size: 15px; - color: var(--muted); - line-height: 1.6; - margin-bottom: 28px; -} - -.coming-btn { - display: inline-block; - padding: 12px 22px; - border-radius: 10px; - background: var(--accent-grad); - color: #fff; - font-weight: 700; - text-decoration: none; - transition: 0.25s ease; -} - -.coming-btn:hover { - transform: scale(1.05); - box-shadow: 0 12px 28px rgba(77, 163, 185, 0.25); -} -.info-box { - margin-bottom: 15px; - padding: 10px; - color: #0e34f6; - border-radius: 8px; - font-weight: 700; -} +html, +body { + height: 100%; + margin: 0; + font-family: Arial; + padding: 0; + font-family: "Poppins", system-ui, Arial, sans-serif; + background: var(--bg); + background-size: cover; + background-position: center; + background-size: cover; + background-position: center; +} +input { + width: 70%; + margin-top: 2px; + margin-bottom: 2px; + font-size: 16px; +} +.admin-box { + margin: 120px auto 60px; + /* space from header + footer */ +} +.app-layout { + min-height: 100vh; + display: flex; + flex-direction: column; + margin-top: var(--header-height); +} +button { + padding: 8px; + margin: 5px; +} +.error-box { + margin-bottom: 15px; + padding: 10px; + color: #ff4d4d; + border-radius: 8px; + font-weight: 600; +} + +.dark-theme h1 { + background: linear-gradient(90deg, #ffffff, #dddddd, #ffffff); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + text-shadow: 0px 0px 10px rgba(255, 255, 255, 0.4); +} + +.dark-theme p { + background: linear-gradient(90deg, #ffffff, #dddddd, #ffffff); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + text-shadow: 0px 0px 10px rgba(255, 255, 255, 0.4); +} +.dark-theme { + --bg-overlay: rgba(0, 0, 0, 0.75); + --glass-bg: rgba(0, 0, 0, 0.4); + --text-color: #fff; + --input-bg: rgba(50, 50, 50, 0.8); + --input-text-color: #fff; +} + +@keyframes pop { + 0% { + transform: scale(0.7); + opacity: 0; + } + 100% { + transform: scale(1); + opacity: 1; + } +} +/* INPUT CONTAINER */ +.input-field { + flex: 1 1 700px; + display: flex; + align-items: center; + gap: 12px; + border-radius: 12px; + border: 2px solid rgb(6, 0, 0); + background: transparent; /* IMPORTANT */ + padding: 12px 12px; +} +.dark-theme .input-field { + border-color: #ffffff; +} +/* INPUT ITSELF */ +.input-field input[type="text"] { + width: 100%; + border: none; + outline: none; + background-color: transparent !important; + background-image: none !important; + box-shadow: none !important; + font-size: 23px; +} + +.input-field input { + color: #000 !important; +} + +.dark-theme .input-field input { + color: #fff !important; +} + +.input-field input:-webkit-autofill, +.input-field input:-webkit-autofill:hover, +.input-field input:-webkit-autofill:focus, +.input-field input:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 1000px transparent inset !important; + box-shadow: 0 0 0 1000px transparent inset !important; + background-color: transparent !important; + background-image: none !important; + transition: background-color 9999s ease-in-out 0s; +} + +.input-field input:-webkit-autofill { + -webkit-text-fill-color: #000 !important; +} + +.dark-theme .input-field input:-webkit-autofill { + -webkit-text-fill-color: #fff !important; +} +.input-field input::selection, +.input-field input::-moz-selection { + background: transparent; + color: inherit; +} +.short-code { + color: #0a0000; /* blue like links */ + font-weight: 700; +} + +.app-header { + position: fixed; + top: 0; + left: 0; + width: 97%; + height: 55px; + background: white; + display: flex; + align-items: center; + padding: 0 28px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); + z-index: 1000; +} + +/* Dark mode */ +.dark-theme .app-header { + background: linear-gradient(180deg, #0b1220, #050b14); +} + +footer { + margin-top: 0; +} + +body.dark-theme, +body.dark-theme .page, +body.dark-theme main, +body.dark-theme section { + background: #0f1720 !important; +} + +.header-left { + display: flex; + align-items: center; + gap: 12px; +} + +.app-logo { + width: 36px; + height: 36px; + background: linear-gradient(135deg, #2563eb, #5ab9ff); + color: white; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; +} + +.app-name { + font-size: 20px; + font-weight: 700; + color: #111827; +} + +.dark-theme .app-name { + color: #f8fafc; +} + +.header-nav { + position: absolute; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 26px; +} + +.nav-link { + text-decoration: none; + color: #111827; + font-weight: 500; + position: relative; +} + +.dark-theme .nav-link { + color: #e5e7eb; +} + +.nav-link:hover { + color: #2563eb; +} + +.nav-link.active::after { + content: ""; + position: absolute; + bottom: -6px; + left: 0; + width: 100%; + height: 2px; + background: #111827; +} + +.dark-theme .nav-link.active::after { + background: #f8fafc; +} + +.header-right { + margin-left: auto; + display: flex; + align-items: center; +} + +:root { + --header-height: 55px; + --bg: #eefaf8; + --card: rgba(255, 255, 255, 0.95); + --muted: #7b8b8a; + --accent-1: #5ab9ff; + --accent-2: #4cb39f; + --accent-grad: linear-gradient(90deg, #4cb39f, #5ab9ff); + --success: #2fb06e; + --glass: rgba(255, 255, 255, 0.85); +} + +.dark-theme { + --bg-overlay: rgba(0, 0, 0, 0.75); + --glass-bg: rgba(0, 0, 0, 0.4); + --text-color: #f3f3f3; + --input-bg: rgba(11, 10, 10, 0.8); + --button-bg: linear-gradient(90deg, #4444ff, #2266ff); + --recent-bg: rgba(255, 255, 255, 0.1); +} + +/* Preserve your dark theme variables too */ +body.dark-theme { + --bg: #0f1720; + --card: rgba(10, 14, 18, 0.92); + --muted: #9aa7a6; +} + +.page { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + padding: 2rem; + min-height: 80vh; +} + +.theme-toggle { + background: transparent; + border: none; + cursor: pointer; + padding: 8px; + border-radius: 8px; + font-weight: 700; + background: var(--card); +} + +/* Hero */ +.hero { + width: 100%; + max-width: 1100px; + background: transparent; + text-align: center; + padding: 10px; +} + +.hero h1 { + margin: 10px 0 14px; + font-size: 36px; + line-height: 1.05; + color: #000606; +} + +.hero p { + margin: var(--bg-overlay); + color: var(--muted); + max-width: 820px; + margin-left: auto; + margin-right: auto; + color: #000606; +} + +/* Main card & input */ +.card { + width: 100%; + max-width: 1100px; + background: var(--card); + border-radius: 14px; + padding: 15px; + box-shadow: 0 18px 50px rgba(8, 24, 24, 0.06); +} + +.cta { + min-width: 220px; + padding: 14px 22px; + border-radius: 12px; + border: none; + color: rgb(12, 1, 1); + font-weight: 700; + cursor: pointer; + background: var(--accent-grad); + box-shadow: 0 12px 28px rgba(77, 163, 185, 0.12); +} + +.small-action { + display: flex; + align-items: center; + gap: 8px; + color: var(--muted); + margin-top: 10px; +} + +.result { + margin-top: 26px; + background: white; + border-radius: 12px; + padding: 20px; + border: 1px solid rgba(22, 60, 55, 0.03); + box-shadow: 0 8px 28px rgba(7, 20, 20, 0.03); +} + +.result-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.result-header .dot { + width: 30px; + height: 30px; + background: var(--success); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: 700; +} + +.short-actions { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + padding: 10px 14px; + border-radius: 12px; + background: linear-gradient(180deg, rgba(75, 194, 176, 0.06), rgba(94, 207, 255, 0.04)); +} + +.short-box input { + align-items: center; + padding: 10px; + font-size: 15px; +} + +.btn-copy { + border: none; + padding: 10px 14px; + border-radius: 8px; + color: white; + font-weight: 700; + cursor: pointer; +} + +.btn-share { + background: #f2f5f5; + border: none; + padding: 10px 14px; + border-radius: 8px; + color: #0b2b2a; + font-weight: 700; + cursor: pointer; + margin-left: 6px; +} + +.meta-row { + align-items: center; + justify-content: center; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2px; + padding: 16px; + margin-top: 1px; + align-items: top; + color: black; +} +.result-body { + margin-top: 30px; + + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.qr-block { + text-align: center; + padding-top: 8px; +} + +.qr-block img { + height: 15rem; + align-items: center; + aspect-ratio: 1; + box-shadow: 0 10px 20px rgba(10, 20, 30, 0.06); + outline: 2px solid green; + outline-offset: 4px; +} + +.download-qr { + display: inline-block; + margin-top: 12px; + text-decoration: none; + color: var(--accent-1); + font-weight: 700; +} + +.action-row { + display: flex; + justify-content: right; + align-items: right; +} + +.action-secondary { + background: #f6fbfb; + border: 1px solid rgba(0, 0, 0, 0.03); + border-radius: 10px; + cursor: pointer; + font-weight: 700; +} + +/* Force Generate QR to stay on one line */ +.qr-inline { + display: inline-flex; + align-items: center; + gap: 8px; + white-space: nowrap; +} + +.qr-inline input { + margin: 0; +} + +/* Responsive */ +@media (max-width: 880px) { + .input-row { + flex-direction: column; + } + + .cta { + width: 100%; + } + + .meta-row { + grid-template-columns: 1fr; + } +} +.result-title { + font-weight: 700; + color: #0e34f6; +} + +.dark-theme .result-title { + color: #150cff; +} + +footer { + min-height: auto; +} + +.app-footer { + background: white; + color: #e5e7eb; + padding: 8px 10px; + margin-top: auto; + position: relative; +} +.dark-theme .app-footer { + background: linear-gradient(180deg, #0b1220, #050b14); +} + +.footer-container { + margin: auto; + display: flex; + gap: 60px; + justify-content: space-between; + flex-wrap: wrap; +} + +.footer-brand { + max-width: 420px; +} + +.footer-logo { + width: 42px; + height: 42px; + background: linear-gradient(135deg, #2563eb, #5ab9ff); + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + font-size: 22px; +} +.dark-theme .footer-brand h3, +.dark-theme .footer-brand p, +.dark-theme .footer-col h4, +.dark-theme .app-footer a, +.dark-theme .footer-bottom { + color: #f8fafc; +} + +.footer-brand h3 { + margin: 0; + color: #000000; + font-size: 22px; + font-weight: 700; +} + +.footer-brand p { + margin-top: 8px; + color: #000000; + line-height: 1.6; + font-size: 14px; +} + +/* MAIN CONTENT */ + +/* FOOTER */ +.app-footer { + margin-top: auto; +} + +/* GitHub button */ +.github-btn { + display: inline-flex; + align-items: center; + gap: 2px; + margin-top: 1px; + padding: 10px 16px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.06); + color: #000000; + text-decoration: none; + font-weight: 600; + transition: all 0.25s ease; +} + +.github-btn:hover { + background: black(11, 1, 1); + transform: translateY(-2px); +} + +.footer-links { + display: flex; + gap: 80px; + flex-wrap: wrap; +} + +.footer-col h4 { + margin-bottom: 14px; + font-size: 16px; + color: #000000; + font-weight: 700; +} + +.footer-col a { + display: block; + text-decoration: none; + color: #000000; + margin-bottom: 10px; + font-size: 14px; + transition: color 0.2s ease; +} + +.footer-col a:hover { + text-decoration: underline; +} + +/* Bottom */ +.footer-bottom { + margin-top: 10px; + border-top: 1px solid rgba(255, 255, 255, 0.153); + padding-top: 8px; + padding-bottom: 1px; + text-align: center; + font-size: 14px; + color: #080808; +} +.footer-bottom a { + color: #030000; + font-weight: 600; + text-decoration: none; +} + +.footer-bottom a:hover { + text-decoration: underline; +} + +/* Responsive */ +@media (max-width: 768px) { + .footer-container { + flex-direction: column; + gap: 40px; + } + + .footer-links { + gap: 40px; + } +} +/* REMOVE white line above footer in dark mode */ +footer { + margin-top: 0 !important; +} +.recent-table-wrapper { + margin-top: 20px; + width: 100%; + overflow-x: auto; +} + +.recent-table { + width: 100%; + border-collapse: collapse; + border-radius: 12px; + overflow: hidden; +} + +.recent-table thead { + background: rgb(0, 0, 0); +} +.recent-table th { + color: rgb(0, 0, 0); + padding: 8px 14px; + text-align: left; + font-size: 16px; +} +.short-code a { + color: #2563eb; + font-weight: 600; + text-decoration: none; +} + +.short-code a:hover { + color: #1d4ed8; + text-decoration: underline; +} +.recent-table td { + color: rgb(34, 48, 77); + padding: 10px 14px; + text-align: left; + font-size: 14px; +} + +.created-time { + font-size: 14px; + color: #374151; + white-space: nowrap; +} + +.time-ago { + color: #374151; + font-size: 13px; + margin-left: 2px; +} +.recent-table th { + font-weight: 700; +} + +.recent-table tbody tr, +th { + background: rgb(255, 255, 255); + border-bottom: 1px solid rgb(0, 0, 0); +} +.dark-theme.recent-table tbody tr, +td { + background: rgba(255, 255, 255, 0.04); + border-bottom: 1px solid rgb(0, 0, 0); +} +.recent-table tbody tr:hover { + background: rgb(196, 196, 196); +} + +/* Short code */ +.short-code { + font-weight: 700; +} + +.original-url { + color: #22c55e; + word-break: break-all; +} + +/* Action buttons */ +.action-col { + display: flex; + gap: 10px; +} + +.action-btn { + width: 36px; + height: 36px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + font-size: 16px; + transition: 0.2s ease; +} + +.open-btn { + background: #3b82f6; + color: #fff; +} + +.delete-btn { + background: #ef4444; + color: #fff; +} + +.recent-table-wrapper { + margin-bottom: 20px; +} +/* ========================= + Coming Soon Page +========================= */ + +.coming-soon-page { + display: flex; + align-items: center; + justify-content: center; + padding: 120px 20px 60px; +} + +.coming-soon-card { + max-width: 520px; + width: 100%; + background: var(--card); + border-radius: 16px; + padding: 50px 40px; + text-align: center; + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.08); +} + +.coming-icon { + font-size: 48px; + margin-bottom: 18px; +} + +.coming-soon-card h1 { + font-size: 34px; + margin-bottom: 14px; + color: #000; +} + +.dark-theme .coming-soon-card h1 { + color: #fff; +} + +.coming-soon-card p { + font-size: 15px; + color: var(--muted); + line-height: 1.6; + margin-bottom: 28px; +} + +.coming-btn { + display: inline-block; + padding: 12px 22px; + border-radius: 10px; + background: var(--accent-grad); + color: #fff; + font-weight: 700; + text-decoration: none; + transition: 0.25s ease; +} + +.coming-btn:hover { + transform: scale(1.05); + box-shadow: 0 12px 28px rgba(77, 163, 185, 0.25); +} +.info-box { + margin-bottom: 15px; + padding: 10px; + color: #0e34f6; + border-radius: 8px; + font-weight: 700; +} diff --git a/app/templates/coming-soon.html b/app/templates/coming-soon.html index 3f86d1d..1a5e5eb 100644 --- a/app/templates/coming-soon.html +++ b/app/templates/coming-soon.html @@ -1,120 +1,120 @@ - - - - - - - Coming-soon - - - - - - -
-
- - tiny URL -
- - -
- -
-
- - -
-
-
🚧
-

Coming Soon

-

- This feature is under active development. -

- - ← Back to Home -
-
- - - - + + + + + + + Coming-soon + + + + + + +
+
+ + tiny URL +
+ + +
+ +
+
+ + +
+
+
🚧
+

Coming Soon

+

+ This feature is under active development. +

+ + ← Back to Home +
+
+ + + + \ No newline at end of file diff --git a/app/templates/header.html b/app/templates/header.html new file mode 100644 index 0000000..c7b19b1 --- /dev/null +++ b/app/templates/header.html @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index 494fb2c..7a5ecd4 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,348 +1,132 @@ - - - - - - - tiny URL - - - - - - - - - -
-
- - tiny URL -
- - -
- -
-
- - - -
-
- -
- {% if not db_available %} -
- ⚠️ Running in Limited Mode (No Database) -
- {% endif %} - - -
-

Shorten Your URLs with Precision

-

Transform long links into short, memorable URLs.

-
- - -
- - -
-
- - -
-
- - - - - - - -
- - -
- {% if error %} -
{{ error }}
- {% endif %} - {% if info_message %} -
- {{ info_message }} -
- {% endif %} - - - -
- - - - -
- -
-
- - - {% if new_short_url %} -
-
-
-
-
Your shortened URL is ready!
- -
Copy or share your short link, download the - QR code, or view details.
-
-
-
- -
- - {{ new_short_url }} - - - - -
-
- - - -
- -
- {% if qr_data %} - QR Code - - {% else %} -
-
- {% endif %} - -
Original URL
- {% set display_url = original_url or (urls[0].original_url if urls|length > 0 else '') %} - - {% if display_url %} - - {% endif %} - - -
-
-
-
-
-
-
-
- - - - -
- - -
-
-
- {% endif %} - - - - - -
- - - - - - - - - - \ No newline at end of file +{% extends "layout.html" %} + +{% block content %} +
+
+

tiny URL

+
+
+ + +
+
+ + Analytics Enabled +
+
+
+ + {% if new_short_url %} +
+
+ {% if qr_image %} + + {% endif %} +
+ Ready + +
+
+
+ + {% if qr_image %} + Download QR + {% endif %} +
+
+ {% endif %} + +
+
+

Recently Shortened

+ View History → +
+
+ {% for url in urls %} +
+
/{{ url.short_code }}
+
{{ url.original_url }}
+
+ {% endfor %} +
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/layout.html b/app/templates/layout.html new file mode 100644 index 0000000..e96619f --- /dev/null +++ b/app/templates/layout.html @@ -0,0 +1,39 @@ + + + + + + + + {% block title %}tiny URL{% endblock %} + + + + + + + + + + + {% block head_extra %}{% endblock %} + + + +
+ + {% include "header.html" %} + + {% block content %}{% endblock %} + + + + + \ No newline at end of file diff --git a/app/templates/recent.html b/app/templates/recent.html index 0e44929..26c5d69 100644 --- a/app/templates/recent.html +++ b/app/templates/recent.html @@ -1,210 +1,210 @@ - - - - - - - Recent URLs - - - - - - -
-
- - tiny URL -
- - -
- -
-
- - - -
-
- -
- - - -
-

Recent Shortened URLs

-
- - -
- -
- - - - - - - - - - - - - - {% for url in urls %} - - - - - - - - - - - - - - - {% else %} - - - - {% endfor %} - -
#Short URLOriginal URLCreatedVisitsActions
{{ loop.index }} - - {{ request.host_url }}{{ url.short_code }} - - - - {{ url.original_url }} - - - {{ format_date(url.created_at) }} - {{ url.visit_count }} - - ↗ - - - - 🗑 - -
- No URLs found. -
-
- -
- -
- - - - - - + + + + + + + Recent URLs + + + + + + +
+
+ + tiny URL +
+ + +
+ +
+
+ + + +
+
+ +
+ + + +
+

Recent Shortened URLs

+
+ + +
+ +
+ + + + + + + + + + + + + + {% for url in urls %} + + + + + + + + + + + + + + + {% else %} + + + + {% endfor %} + +
#Short URLOriginal URLCreatedVisitsActions
{{ loop.index }} + + {{ request.host_url }}{{ url.short_code }} + + + + {{ url.original_url }} + + + {{ format_date(url.created_at) }} + {{ url.visit_count }} + + ↗ + + + + 🗑 + +
+ No URLs found. +
+
+ +
+ +
+ + + + + + \ No newline at end of file diff --git a/app/utils/__init__.py b/app/utils/__init__.py index 3dc1f76..3f5c4a7 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.0" diff --git a/app/utils/_version.py b/app/utils/_version.py index d6ba74d..71f139c 100644 --- a/app/utils/_version.py +++ b/app/utils/_version.py @@ -1,14 +1,14 @@ -# use this to update version in setup.py automatically -# when building the package, so that we don't have to maintain version in two places - -try: - from importlib.metadata import PackageNotFoundError, version -except ImportError: # Python <3.8 - from importlib.metadata import PackageNotFoundError, version - - -def get_version() -> str: - try: - return version("tiny") # must match pyproject.toml [project].name - except PackageNotFoundError: - return "0.0.1" +# use this to update version in setup.py automatically +# when building the package, so that we don't have to maintain version in two places + +try: + from importlib.metadata import PackageNotFoundError, version +except ImportError: # Python <3.8 + from importlib.metadata import PackageNotFoundError, version + + +def get_version() -> str: + try: + return version("tiny") # must match pyproject.toml [project].name + except PackageNotFoundError: + return "0.0.1" diff --git a/app/utils/cache.py b/app/utils/cache.py new file mode 100644 index 0000000..a6f1c6a --- /dev/null +++ b/app/utils/cache.py @@ -0,0 +1,127 @@ +import time +from typing import TypedDict + +from app.utils.config import CACHE_TTL, MAX_RECENT_URLS + + +class UrlCacheItem(TypedDict): + url: str + expires_at: float + + +class RevCacheItem(TypedDict): + short_code: str + expires_at: float + last_accessed: float + + +# short_code -> original_url +url_cache: dict[str, UrlCacheItem] = {} + +# original_url -> short_code (+ metadata for recent tracking) +rev_cache: dict[str, RevCacheItem] = {} + + +def _now() -> float: + return time.time() + + +def get_from_cache(short_code: str) -> str | None: + data = url_cache.get(short_code) + + if not data: + return None + + if data["expires_at"] < _now(): + url_cache.pop(short_code, None) + return None + + return data["url"] + + +def get_short_from_cache(original_url: str) -> str | None: + data = rev_cache.get(original_url) + + if not data: + return None + + if data["expires_at"] < _now(): + rev_cache.pop(original_url, None) + return None + + # Touch for recent tracking + data["last_accessed"] = _now() + + return data["short_code"] + + +def set_cache_pair(short_code: str, original_url: str) -> None: + now = _now() + expires_at = now + CACHE_TTL + + url_cache[short_code] = { + "url": original_url, + "expires_at": expires_at, + } + + rev_cache[original_url] = { + "short_code": short_code, + "expires_at": expires_at, + "last_accessed": now, + } + + +def clear_cache() -> None: + """ + Useful for tests or if DB goes down and you want to reset cache. + """ + url_cache.clear() + rev_cache.clear() + + +def cleanup_expired() -> None: + """ + Optional: Manually remove expired cache entries. + Can be called periodically (cron/background task). + """ + now = _now() + + expired_short_codes = [ + key for key, value in url_cache.items() if value["expires_at"] < now + ] + for key in expired_short_codes: + url_cache.pop(key, None) + + expired_urls = [ + key for key, value in rev_cache.items() if value["expires_at"] < now + ] + for key in expired_urls: + rev_cache.pop(key, None) + + +# ----------------------- +# Recent URLs (derived from rev_cache) +# ----------------------- + + +def get_recent_from_cache(limit: int = MAX_RECENT_URLS) -> list[dict]: + """ + Returns recent URLs based on cache activity (no duplicates, TTL-aware). + Shape matches DB docs. + """ + now = _now() + + items = [ + { + "short_code": data["short_code"], + "original_url": original_url, + } + for original_url, data in rev_cache.items() + if data["expires_at"] >= now + ] + + items.sort( + key=lambda x: rev_cache[x["original_url"]]["last_accessed"], reverse=True + ) + + return items[:limit] diff --git a/app/utils/config.py b/app/utils/config.py index c38c606..0b9d73f 100644 --- a/app/utils/config.py +++ b/app/utils/config.py @@ -1,15 +1,62 @@ + import os +# ------------------------- +# Helpers +# ------------------------- + + +from app.utils.config_env import load_env # noqa: F401 + +load_env() + +def _get_int(key: str, default: int) -> int: + try: + return int(os.getenv(key, default)) + except (TypeError, ValueError): + return default + + +# ------------------------- +# App (constants + light env override) +# ------------------------- +APP_NAME = "TinyURL" +MODE = os.getenv("MODE", "local") +DEBUG = MODE == "local" # auto-debug in local + +API_VERSION = os.getenv("API_VERSION", "v1") + +# ------------------------- +# Server +# ------------------------- +HOST = "127.0.0.1" +PORT = _get_int("PORT", 8000) + +# If DOMAIN not provided, derive from HOST + PORT +DOMAIN = os.getenv("DOMAIN", f"http://{HOST}:{PORT}") + +# ------------------------- +# Database +# ------------------------- +MONGO_URI = os.getenv("MONGO_URI") +MONGO_DB_NAME = "tiny_url" +MONGO_COLLECTION = os.getenv("MONGO_COLLECTION", "urls") + -from dotenv import load_dotenv +# ------------------------- +# Cache (constants) +# ------------------------- +USE_CACHE = True +CACHE_TTL = 900 # 15 minutes +MAX_CACHE_SIZE = 10_000 +MAX_RECENT_URLS = 20 +# ------------------------- +# Security / Sessions +# ------------------------- +SESSION_SECRET = os.getenv("SESSION_SECRET", "super-secret-key") -def load_env(): - env = os.getenv("ENV", "development") - file_map = { - "production": ".env", - "local": ".env.local", - "development": ".env.development", - } - load_dotenv(file_map.get(env, ".env.development"), override=True) - print(f"Environment selected: {env}") - print(f"MODE value: {os.getenv('MODE')}") +# ------------------------- +# Short URL (constants) +# ------------------------- +SHORT_CODE_LENGTH = 6 +MAX_URL_LENGTH = 2048 diff --git a/app/utils/config_env.py b/app/utils/config_env.py new file mode 100644 index 0000000..049232d --- /dev/null +++ b/app/utils/config_env.py @@ -0,0 +1,15 @@ +import os + +from dotenv import load_dotenv + + +def load_env(): + env = os.getenv("ENV", "development") + file_map = { + "production": ".env", + "local": ".env.local", + "development": ".env.development", + } + load_dotenv(file_map.get(env, ".env.development"), override=True) + print(f"Environment selected: {env}") + print(f"MODE value: {os.getenv('MODE')}") diff --git a/app/utils/data.py b/app/utils/data.py new file mode 100644 index 0000000..50c6f6e --- /dev/null +++ b/app/utils/data.py @@ -0,0 +1,107 @@ +from typing import Any, Optional + +try: + from pymongo import MongoClient + from pymongo.errors import PyMongoError + + MONGO_INSTALLED = True +except ImportError: + MongoClient: Any = None # type: ignore + Collection: Any # type: ignore + PyMongoError = Exception # type: ignore + MONGO_INSTALLED = False + +from app.utils.config import MONGO_COLLECTION, MONGO_DB_NAME, MONGO_URI + +client: Any = None +db: Any = None +collection: Any = None + + +def connect_db() -> bool: + global client, db, collection + + if not MONGO_INSTALLED: + return False + + try: + # Create instance + new_client: Any = MongoClient(MONGO_URI, serverSelectionTimeoutMS=2000) + new_client.admin.command("ping") + client = new_client + db = new_client[MONGO_DB_NAME] + collection = db[MONGO_COLLECTION] + return True + + except Exception: + client = db = collection = None + return False + + return False + + +def get_collection() -> Optional[dict[str, Any]]: + return collection + + +# ------------------------ +# DB Operations +# ------------------------ + + +def find_by_original_url(original_url: str) -> Optional[dict]: + if collection is None: + return None + try: + return collection.find_one({"original_url": original_url}) + except PyMongoError: + return None + + +def insert_url(short_code: str, original_url: str) -> bool: + if collection is None: + return False + try: + collection.insert_one( + { + "short_code": short_code, + "original_url": original_url, + "created_at": __import__("datetime").datetime.utcnow(), + "visit_count": 0, + } + ) + return True + except PyMongoError: + return False + + +def delete_by_short_code(short_code: str) -> bool: + if collection is None: + return False + try: + collection.delete_one({"short_code": short_code}) + return True + except PyMongoError: + return False + + +def get_recent_urls(limit: int = 10) -> list[dict]: + if collection is None: + return [] + try: + return list(collection.find().sort("created_at", -1).limit(limit)) + except PyMongoError: + return [] + + +def increment_visit(short_code: str) -> Optional[dict]: + if collection is None: + return None + try: + return collection.find_one_and_update( + {"short_code": short_code}, + {"$inc": {"visit_count": 1}}, + return_document=True, + ) + except PyMongoError: + return None diff --git a/app/utils/helper.py b/app/utils/helper.py index c0dad93..30be0b2 100644 --- a/app/utils/helper.py +++ b/app/utils/helper.py @@ -1,28 +1,33 @@ -import string -import random -from datetime import timezone -import validators - - -def is_valid_url(url: str) -> bool: - return bool(validators.url(url)) - - -def sanitize_url(url: str) -> str: - url = url.strip() - if not url: - return "" - if not url.startswith(("http://", "https://")): - url = "https://" + url - return url - - -def generate_code(length: int = 6) -> str: - chars = string.ascii_letters + string.digits - return "".join(random.choice(chars) for _ in range(length)) - - -def format_date(dt): - if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) - return dt.strftime("%d %b %Y") +import string +import random +from datetime import timezone +import validators +from app.utils.config import SHORT_CODE_LENGTH + + +def is_valid_url(url: str) -> bool: + return bool(validators.url(url)) + + +def sanitize_url(url: str) -> str: + url = url.strip() + if not url: + return "" + if not url.startswith(("http://", "https://")): + url = "https://" + url + return url + + +def generate_code(length: int = SHORT_CODE_LENGTH) -> str: + chars = string.ascii_letters + string.digits + return "".join(random.choice(chars) for _ in range(length)) + + +def format_date(dt): + if not dt: + return "Just now" + + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + + return dt.strftime("%d %b %Y, %I:%M %p") diff --git a/app/utils/lint.py b/app/utils/lint.py index 44ddcc2..3a88cb1 100644 --- a/app/utils/lint.py +++ b/app/utils/lint.py @@ -1,23 +1,23 @@ -import subprocess -import sys - -commands = [ - ["black", "."], - ["flake8", "."], - ["mypy", "-p", "app"], - ["ruff", "check", "."], -] - - -def main(): - for cmd in commands: - print(f"\n🚀 Running: {' '.join(cmd)}") - result = subprocess.run(["poetry", "run"] + cmd) - if result.returncode != 0: - print(f"❌ Command failed: {' '.join(cmd)}") - sys.exit(result.returncode) - print("\n✅ All linters passed successfully!") - - -if __name__ == "__main__": - main() +import subprocess +import sys + +commands = [ + ["black", "."], + ["flake8", "."], + ["mypy", "-p", "app"], + ["ruff", "check", "."], +] + + +def main(): + for cmd in commands: + print(f"\n🚀 Running: {' '.join(cmd)}") + result = subprocess.run(["poetry", "run"] + cmd) + if result.returncode != 0: + print(f"❌ Command failed: {' '.join(cmd)}") + sys.exit(result.returncode) + print("\n✅ All linters passed successfully!") + + +if __name__ == "__main__": + main() diff --git a/app/utils/qr.py b/app/utils/qr.py index fc99801..246f95f 100644 --- a/app/utils/qr.py +++ b/app/utils/qr.py @@ -1,44 +1,52 @@ -import qrcode -from PIL import Image -from pathlib import Path - - -def generate_qr_with_logo(data, filename): - qr = qrcode.QRCode( - error_correction=qrcode.constants.ERROR_CORRECT_H, - box_size=20, - border=1, - ) - qr.add_data(data) - qr.make(fit=True) - - qr_img = qr.make_image(fill_color="black", back_color="white").convert("RGB") - - # ✅ Resolve project app directory (app/) - APP_DIR = Path(__file__).resolve().parents[1] # goes up to /app - STATIC_DIR = APP_DIR / "static" - QR_DIR = STATIC_DIR / "qr" - IMAGES_DIR = STATIC_DIR / "images" - - # ✅ Ensure QR dir exists - QR_DIR.mkdir(parents=True, exist_ok=True) - - logo_path = IMAGES_DIR / "logo.png" - - if not logo_path.exists(): - raise FileNotFoundError(f"Logo not found at: {logo_path}") - - logo = Image.open(logo_path) - - qr_width, qr_height = qr_img.size - logo_size = qr_width // 3 - logo = logo.resize((logo_size, logo_size)) - - pos = ((qr_width - logo_size) // 2, (qr_height - logo_size) // 2) - - qr_img.paste(logo, pos, mask=logo if logo.mode == "RGBA" else None) - - save_path = QR_DIR / filename - qr_img.save(save_path) - - return str(save_path) +from pathlib import Path +from typing import cast + +import qrcode +from PIL import Image + + +def generate_qr_with_logo(data: str, filename: str) -> str: + # 1. Use direct attribute access or explicit import for constants + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_H, # type: ignore + box_size=20, + border=1, + ) + qr.add_data(data) + qr.make(fit=True) + + # 2. Cast the result to PIL.Image.Image so Pylance sees .convert() + # The default factory for qrcode is a PIL-based image + qr_img = cast( + Image.Image, qr.make_image(fill_color="black", back_color="white") + ).convert("RGB") + + APP_DIR = Path(__file__).resolve().parents[1] + STATIC_DIR = APP_DIR / "static" + QR_DIR = STATIC_DIR / "qr" + IMAGES_DIR = STATIC_DIR / "images" + + QR_DIR.mkdir(parents=True, exist_ok=True) + + logo_path = IMAGES_DIR / "logo.png" + + if not logo_path.exists(): + raise FileNotFoundError(f"Logo not found at: {logo_path}") + + logo = Image.open(logo_path) + + qr_width, qr_height = qr_img.size + logo_size = qr_width // 3 + logo = logo.resize((logo_size, logo_size)) + + pos = ((qr_width - logo_size) // 2, (qr_height - logo_size) // 2) + + # Use the logo itself as a mask if it has an alpha channel + mask = logo if logo.mode == "RGBA" else None + qr_img.paste(logo, pos, mask=mask) + + save_path = QR_DIR / filename + qr_img.save(save_path) + + return str(save_path) diff --git a/docs/build-test.md b/docs/build-test.md index 7418014..7f4c5fc 100644 --- a/docs/build-test.md +++ b/docs/build-test.md @@ -1,87 +1,87 @@ -# Build & Test Guide (Tiny URL Project) - -This document explains how to **build**, **install**, and **run tests locally** for the Tiny URL project using Poetry, FastAPI (API), and Flask (UI). - ---- - -## ✅ Prerequisites - -- Python 3.10+ -- Poetry installed -- MongoDB running locally -- `.env.local` file created (not committed to Git) - -Example `.env.local`: - -📦 1. Install Dependencies - -``` -poetry install -``` - -🏗️ 2. Build the Project - -This creates a source distribution and wheel package: - -``` -poetry build -``` - -Expected output: - -``` -Built tiny-0.0.1.tar.gz -Built tiny-0.0.1-py3-none-any.whl -``` - -🧪 3. Test the Built Wheel - -Create a clean virtual environment and install the wheel: - -``` -python3 -m venv /tmp/tiny-test -source /tmp/tiny-test/bin/activate - -pip install dist/tiny-0.0.1-py3-none-any.whl -``` - -Verify imports: - -``` -python -c "import app; print('app import OK')" -``` - -🚀 4. Run API & UI Together (Two Terminals) - -Run both services in separate terminals. - -Terminal 1 – FastAPI (API) - -``` -poetry run uvicorn app.api.fast_api:app --reload --port 8001 -``` - -API will run at: - -```json -http://127.0.0.1:8000 -``` - -Docs: - -```json -http://127.0.0.1:8000/docs -``` - -Terminal 2 – Fast (UI) - -``` -poetry run uvicorn app.main:app --reload - -``` - -UI will run at: - -```json -http://127.0.0.1:5000 -``` +# Build & Test Guide (Tiny URL Project) + +This document explains how to **build**, **install**, and **run tests locally** for the Tiny URL project using Poetry, FastAPI (API), and Fast(UI). + +--- + +## ✅ Prerequisites + +- Python 3.10+ +- Poetry installed +- MongoDB running locally +- `.env.local` file created (not committed to Git) + +Example `.env.local`: + +📦 1. Install Dependencies + +``` +poetry install +``` + +🏗️ 2. Build the Project + +This creates a source distribution and wheel package: + +``` +poetry build +``` + +Expected output: + +``` +Built tiny-0.0.1.tar.gz +Built tiny-0.0.1-py3-none-any.whl +``` + +🧪 3. Test the Built Wheel + +Create a clean virtual environment and install the wheel: + +``` +python3 -m venv /tmp/tiny-test +source /tmp/tiny-test/bin/activate + +pip install dist/tiny-0.0.1-py3-none-any.whl +``` + +Verify imports: + +``` +python -c "import app; print('app import OK')" +``` + +🚀 4. Run API & UI Together (Two Terminals) + +Run both services in separate terminals. + +Terminal 1 – FastAPI (API) + +``` +poetry run uvicorn app.api.fast_api:app --reload --port 8001 +``` + +API will run at: + +```json +http://127.0.0.1:8000 +``` + +Docs: + +```json +http://127.0.0.1:8000/docs +``` + +Terminal 2 – Fast (UI) + +``` +poetry run uvicorn app.main:app --reload + +``` + +UI will run at: + +```json +http://127.0.0.1:5000 +``` diff --git a/docs/cache.md b/docs/cache.md new file mode 100644 index 0000000..2f4e718 --- /dev/null +++ b/docs/cache.md @@ -0,0 +1,200 @@ +# 📘 Cache – Complete Guide (Concepts, How It Works, How to Use It) + +`1. What is Cache?` + +Cache is a temporary storage layer used to store frequently accessed data so that future requests can be served faster. + +Instead of fetching data repeatedly from a slow source (like a database, API, or disk), the application first checks the cache. +If the data exists in cache, it is returned immediately. + +If not, the application fetches it from the original source and stores it in the cache for next time. + +Simple definition: + +Cache is a fast memory layer that stores frequently used data to reduce latency and load on the main data source. + +# 2. Why Cache is Needed + +Without cache: + +- Every request hits the database or external service + +- Response time is slow + +- Database load increases + +- Application does not scale well + +With cache: + +- Most requests are served from fast memory + +- Database load is reduced + +- Response time improves significantly + +- Application handles more traffic with the same resources + +Key benefits: + +- Performance improvement + +- Reduced load on database + +- Better user experience + +- Lower infrastructure cost + +- Improved scalability + + # 3. How Cache Works (High-Level Flow) + +Basic flow: + +``` +User Request + ↓ +Application checks Cache + ↓ +Cache Hit? ── Yes ──> Return data from Cache + │ + No + ↓ +Fetch from Database / Source + ↓ +Store result in Cache + ↓ +Return data to User + +``` + +Cache Hit vs Cache Miss + +- Cache Hit: + Data is found in cache → fast response + +- Cache Miss: + Data is not in cache → fetch from main source → save to cache → return response + +# 5. Cache Lifetime (TTL – Time To Live) + +Cached data should not live forever. +A TTL (Time To Live) defines how long data remains in cache before it expires. + +`Why TTL is important:` + +- Prevents stale data + +- Frees memory + +- Keeps cache fresh + +- Avoids serving outdated values + +`Examples:` + +- Short TTL → fast-changing data + +- Long TTL → stable data + +`Once TTL expires:` + +- Data is removed from cache automatically + +- Next request becomes a cache miss + +# 6. Cache Invalidation (Keeping Cache Correct) + +Cache invalidation means removing or updating cached data when the original data changes. + +`When to invalidate cache:` + +- Data is updated + +- Data is deleted + +- Data becomes invalid + +- TTL expires + +`Why invalidation is important:` + +- Prevents serving outdated or incorrect data + +- Keeps cache consistent with main data source + Cache invalidation is one of the hardest problems in system design because it requires keeping cache and source data in sync. + +# 7. Common Cache Strategies (Conceptual) + +`1️⃣ Read-Through Cache` + +- Application reads from cache first + +- On miss, fetches from source and stores in cache + +`2️⃣ Write-Through Cache` + +- Data is written to cache and main storage together + +`3️⃣ Write-Behind Cache` + +- Data is written to cache first + +- Main storage is updated later asynchronously + +`Each strategy has trade-offs between:` + +- Speed + +- Consistency + +- Reliability + +# 1. Cache Consistency vs Performance + +Cache improves performance but can introduce stale data. + +There is always a trade-off between: + +- Strong consistency → always fresh data + +- High performance → faster responses + +`Design choice depends on:` + +- How critical correctness is + +- How often data changes + +- How much stale data is acceptable + +# 9. Cache Failure Handling + +Cache should never be a single point of failure. + +`Good design principles:` + +- Application should still work if cache is unavailable + +- Cache is an optimization, not the source of truth + +- Main data source remains authoritative + +`If cache fails:` + +- Application should fallback to main data source + +- Cache can be repopulated later + +# 10. Cache in Application Architecture + +`Cache usually sits between application and main storage:` + +```Client + ↓ +Application + ↓ +Cache Layer + ↓ +Database / External Service +``` diff --git a/docs/tree.md b/docs/tree.md new file mode 100644 index 0000000..ad9374d --- /dev/null +++ b/docs/tree.md @@ -0,0 +1,47 @@ +## Project Folder Structure + +```text +Directory structure: +tiny/ +├── CHANGELOG.md +├── LICENSE +├── README.md +├── app/ +│ ├──__init__.py +│ ├── main.py +│ ├── cli.py +│ ├── api/ +│ │ └── fast_api.py +| ├──assets/images +│ ├── db/ +| | └──__init__.py +| | └──data.py +│ ├── static/ +| | └── images +| | └── qr +| | └──style.css +| └── templates/ +| | └── index.html +| | └── recent.html +| | └── coming-soon.html +│ ├── utils/ +| | └──__init__.py +| | └──_version.py +| | └── cache.py +| | └── config.py +| | └── helper.py +| | └── lint.py +| | └── qr.py +├── docs/ +| └── build-test.md +| └── cache.md +| └── run_with_curl.md +├── pyproject.toml +| └── poetry.lock +├──README.md +| └── CHANGELOG.md +| └── requirements.txt +├── tiny.code-workspace +└── .gitignore + +``` diff --git a/mypy.ini b/mypy.ini index cbe57d5..b8d8384 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,11 +1,11 @@ -[mypy-untyped_package.*] -follow_untyped_imports = True - -[mypy] -ignore_missing_imports = True -disable_error_code = import-untyped,attr-defined - - -[mypy-win32com.*] -ignore_missing_imports = True - +[mypy-untyped_package.*] +follow_untyped_imports = True + +[mypy] +ignore_missing_imports = True +disable_error_code = import-untyped,attr-defined + + +[mypy-win32com.*] +ignore_missing_imports = True + diff --git a/poetry.lock b/poetry.lock index 1a589f1..0d9aade 100644 --- a/poetry.lock +++ b/poetry.lock @@ -430,62 +430,62 @@ markers = {main = "sys_platform == \"win32\" or platform_system == \"Windows\"", [[package]] name = "cryptography" -version = "46.0.4" +version = "46.0.5" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.8" groups = ["dev"] markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" files = [ - {file = "cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616"}, - {file = "cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0"}, - {file = "cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0"}, - {file = "cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5"}, - {file = "cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b"}, - {file = "cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f"}, - {file = "cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82"}, - {file = "cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c"}, - {file = "cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061"}, - {file = "cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7"}, - {file = "cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019"}, - {file = "cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4"}, - {file = "cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b"}, - {file = "cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc"}, - {file = "cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3"}, - {file = "cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59"}, + {file = "cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731"}, + {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82"}, + {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1"}, + {file = "cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48"}, + {file = "cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4"}, + {file = "cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663"}, + {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826"}, + {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d"}, + {file = "cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a"}, + {file = "cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4"}, + {file = "cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c"}, + {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4"}, + {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9"}, + {file = "cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72"}, + {file = "cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7"}, + {file = "cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d"}, ] [package.dependencies] @@ -499,7 +499,7 @@ nox = ["nox[uv] (>=2024.4.15)"] pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==46.0.4)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.5)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -592,14 +592,14 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.128.7" +version = "0.128.8" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "fastapi-0.128.7-py3-none-any.whl", hash = "sha256:6bd9bd31cb7047465f2d3fa3ba3f33b0870b17d4eaf7cdb36d1576ab060ad662"}, - {file = "fastapi-0.128.7.tar.gz", hash = "sha256:783c273416995486c155ad2c0e2b45905dedfaf20b9ef8d9f6a9124670639a24"}, + {file = "fastapi-0.128.8-py3-none-any.whl", hash = "sha256:5618f492d0fe973a778f8fec97723f598aa9deee495040a8d51aaf3cf123ecf1"}, + {file = "fastapi-0.128.8.tar.gz", hash = "sha256:3171f9f328c4a218f0a8d2ba8310ac3a55d1ee12c28c949650288aee25966007"}, ] [package.dependencies] @@ -616,14 +616,14 @@ standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[stand [[package]] name = "filelock" -version = "3.20.3" +version = "3.21.2" description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"}, - {file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"}, + {file = "filelock-3.21.2-py3-none-any.whl", hash = "sha256:d6cd4dbef3e1bb63bc16500fc5aa100f16e405bbff3fb4231711851be50c1560"}, + {file = "filelock-3.21.2.tar.gz", hash = "sha256:cfd218cfccf8b947fce7837da312ec3359d10ef2a47c8602edd59e0bacffb708"}, ] [[package]] @@ -812,6 +812,24 @@ files = [ test = ["async-timeout ; python_version < \"3.11\"", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] trio = ["trio"] +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + [[package]] name = "keyring" version = "25.7.0" @@ -845,89 +863,103 @@ type = ["pygobject-stubs", "pytest-mypy (>=1.0.1)", "shtab", "types-pywin32"] [[package]] name = "librt" -version = "0.7.8" +version = "0.8.0" description = "Mypyc runtime library" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "platform_python_implementation != \"PyPy\"" files = [ - {file = "librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d"}, - {file = "librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b"}, - {file = "librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d"}, - {file = "librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d"}, - {file = "librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c"}, - {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c"}, - {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d"}, - {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0"}, - {file = "librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85"}, - {file = "librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c"}, - {file = "librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f"}, - {file = "librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac"}, - {file = "librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c"}, - {file = "librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8"}, - {file = "librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff"}, - {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3"}, - {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75"}, - {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873"}, - {file = "librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7"}, - {file = "librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c"}, - {file = "librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232"}, - {file = "librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63"}, - {file = "librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93"}, - {file = "librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592"}, - {file = "librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850"}, - {file = "librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62"}, - {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b"}, - {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714"}, - {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449"}, - {file = "librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac"}, - {file = "librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708"}, - {file = "librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0"}, - {file = "librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc"}, - {file = "librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2"}, - {file = "librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3"}, - {file = "librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6"}, - {file = "librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d"}, - {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e"}, - {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca"}, - {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93"}, - {file = "librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951"}, - {file = "librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34"}, - {file = "librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09"}, - {file = "librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418"}, - {file = "librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611"}, - {file = "librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758"}, - {file = "librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea"}, - {file = "librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac"}, - {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398"}, - {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81"}, - {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83"}, - {file = "librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d"}, - {file = "librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44"}, - {file = "librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce"}, - {file = "librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f"}, - {file = "librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde"}, - {file = "librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e"}, - {file = "librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b"}, - {file = "librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666"}, - {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581"}, - {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a"}, - {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca"}, - {file = "librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365"}, - {file = "librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32"}, - {file = "librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06"}, - {file = "librt-0.7.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c7e8f88f79308d86d8f39c491773cbb533d6cb7fa6476f35d711076ee04fceb6"}, - {file = "librt-0.7.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:389bd25a0db916e1d6bcb014f11aa9676cedaa485e9ec3752dfe19f196fd377b"}, - {file = "librt-0.7.8-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73fd300f501a052f2ba52ede721232212f3b06503fa12665408ecfc9d8fd149c"}, - {file = "librt-0.7.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d772edc6a5f7835635c7562f6688e031f0b97e31d538412a852c49c9a6c92d5"}, - {file = "librt-0.7.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde8a130bd0f239e45503ab39fab239ace094d63ee1d6b67c25a63d741c0f71"}, - {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fdec6e2368ae4f796fc72fad7fd4bd1753715187e6d870932b0904609e7c878e"}, - {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:00105e7d541a8f2ee5be52caacea98a005e0478cfe78c8080fbb7b5d2b340c63"}, - {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c6f8947d3dfd7f91066c5b4385812c18be26c9d5a99ca56667547f2c39149d94"}, - {file = "librt-0.7.8-cp39-cp39-win32.whl", hash = "sha256:41d7bb1e07916aeb12ae4a44e3025db3691c4149ab788d0315781b4d29b86afb"}, - {file = "librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be"}, - {file = "librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862"}, + {file = "librt-0.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db63cf3586a24241e89ca1ce0b56baaec9d371a328bd186c529b27c914c9a1ef"}, + {file = "librt-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba9d9e60651615bc614be5e21a82cdb7b1769a029369cf4b4d861e4f19686fb6"}, + {file = "librt-0.8.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb4b3ad543084ed79f186741470b251b9d269cd8b03556f15a8d1a99a64b7de5"}, + {file = "librt-0.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d2720335020219197380ccfa5c895f079ac364b4c429e96952cd6509934d8eb"}, + {file = "librt-0.8.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726305d3e53419d27fc8cdfcd3f9571f0ceae22fa6b5ea1b3662c2e538f833e"}, + {file = "librt-0.8.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3d107f603b5ee7a79b6aa6f166551b99b32fb4a5303c4dfcb4222fc6a0335e"}, + {file = "librt-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41064a0c07b4cc7a81355ccc305cb097d6027002209ffca51306e65ee8293630"}, + {file = "librt-0.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c6e4c10761ddbc0d67d2f6e2753daf99908db85d8b901729bf2bf5eaa60e0567"}, + {file = "librt-0.8.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ba581acad5ac8f33e2ff1746e8a57e001b47c6721873121bf8bbcf7ba8bd3aa4"}, + {file = "librt-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bdab762e2c0b48bab76f1a08acb3f4c77afd2123bedac59446aeaaeed3d086cf"}, + {file = "librt-0.8.0-cp310-cp310-win32.whl", hash = "sha256:6a3146c63220d814c4a2c7d6a1eacc8d5c14aed0ff85115c1dfea868080cd18f"}, + {file = "librt-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:bbebd2bba5c6ae02907df49150e55870fdd7440d727b6192c46b6f754723dde9"}, + {file = "librt-0.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ce33a9778e294507f3a0e3468eccb6a698b5166df7db85661543eca1cfc5369"}, + {file = "librt-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8070aa3368559de81061ef752770d03ca1f5fc9467d4d512d405bd0483bfffe6"}, + {file = "librt-0.8.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:20f73d4fecba969efc15cdefd030e382502d56bb6f1fc66b580cce582836c9fa"}, + {file = "librt-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a512c88900bdb1d448882f5623a0b1ad27ba81a9bd75dacfe17080b72272ca1f"}, + {file = "librt-0.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:015e2dde6e096d27c10238bf9f6492ba6c65822dfb69d2bf74c41a8e88b7ddef"}, + {file = "librt-0.8.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c25a131013eadd3c600686a0c0333eb2896483cbc7f65baa6a7ee761017aef9"}, + {file = "librt-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:21b14464bee0b604d80a638cf1ee3148d84ca4cc163dcdcecb46060c1b3605e4"}, + {file = "librt-0.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:05a3dd3f116747f7e1a2b475ccdc6fb637fd4987126d109e03013a79d40bf9e6"}, + {file = "librt-0.8.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fa37f99bff354ff191c6bcdffbc9d7cdd4fc37faccfc9be0ef3a4fd5613977da"}, + {file = "librt-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1566dbb9d1eb0987264c9b9460d212e809ba908d2f4a3999383a84d765f2f3f1"}, + {file = "librt-0.8.0-cp311-cp311-win32.whl", hash = "sha256:70defb797c4d5402166787a6b3c66dfb3fa7f93d118c0509ffafa35a392f4258"}, + {file = "librt-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:db953b675079884ffda33d1dca7189fb961b6d372153750beb81880384300817"}, + {file = "librt-0.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:75d1a8cab20b2043f03f7aab730551e9e440adc034d776f15f6f8d582b0a5ad4"}, + {file = "librt-0.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:17269dd2745dbe8e42475acb28e419ad92dfa38214224b1b01020b8cac70b645"}, + {file = "librt-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4617cef654fca552f00ce5ffdf4f4b68770f18950e4246ce94629b789b92467"}, + {file = "librt-0.8.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5cb11061a736a9db45e3c1293cfcb1e3caf205912dfa085734ba750f2197ff9a"}, + {file = "librt-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb00bd71b448f16749909b08a0ff16f58b079e2261c2e1000f2bbb2a4f0a45"}, + {file = "librt-0.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95a719a049f0eefaf1952673223cf00d442952273cbd20cf2ed7ec423a0ef58d"}, + {file = "librt-0.8.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bd32add59b58fba3439d48d6f36ac695830388e3da3e92e4fc26d2d02670d19c"}, + {file = "librt-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f764b2424cb04524ff7a486b9c391e93f93dc1bd8305b2136d25e582e99aa2f"}, + {file = "librt-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f04ca50e847abc486fa8f4107250566441e693779a5374ba211e96e238f298b9"}, + {file = "librt-0.8.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9ab3a3475a55b89b87ffd7e6665838e8458e0b596c22e0177e0f961434ec474a"}, + {file = "librt-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e36a8da17134ffc29373775d88c04832f9ecfab1880470661813e6c7991ef79"}, + {file = "librt-0.8.0-cp312-cp312-win32.whl", hash = "sha256:4eb5e06ebcc668677ed6389164f52f13f71737fc8be471101fa8b4ce77baeb0c"}, + {file = "librt-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:0a33335eb59921e77c9acc05d0e654e4e32e45b014a4d61517897c11591094f8"}, + {file = "librt-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:24a01c13a2a9bdad20997a4443ebe6e329df063d1978bbe2ebbf637878a46d1e"}, + {file = "librt-0.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7f820210e21e3a8bf8fde2ae3c3d10106d4de9ead28cbfdf6d0f0f41f5b12fa1"}, + {file = "librt-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4831c44b8919e75ca0dfb52052897c1ef59fdae19d3589893fbd068f1e41afbf"}, + {file = "librt-0.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:88c6e75540f1f10f5e0fc5e87b4b6c290f0e90d1db8c6734f670840494764af8"}, + {file = "librt-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9646178cd794704d722306c2c920c221abbf080fede3ba539d5afdec16c46dad"}, + {file = "librt-0.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e1af31a710e17891d9adf0dbd9a5fcd94901a3922a96499abdbf7ce658f4e01"}, + {file = "librt-0.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:507e94f4bec00b2f590fbe55f48cd518a208e2474a3b90a60aa8f29136ddbada"}, + {file = "librt-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f1178e0de0c271231a660fbef9be6acdfa1d596803464706862bef6644cc1cae"}, + {file = "librt-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:71fc517efc14f75c2f74b1f0a5d5eb4a8e06aa135c34d18eaf3522f4a53cd62d"}, + {file = "librt-0.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0583aef7e9a720dd40f26a2ad5a1bf2ccbb90059dac2b32ac516df232c701db3"}, + {file = "librt-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d0f76fc73480d42285c609c0ea74d79856c160fa828ff9aceab574ea4ecfd7b"}, + {file = "librt-0.8.0-cp313-cp313-win32.whl", hash = "sha256:e79dbc8f57de360f0ed987dc7de7be814b4803ef0e8fc6d3ff86e16798c99935"}, + {file = "librt-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:25b3e667cbfc9000c4740b282df599ebd91dbdcc1aa6785050e4c1d6be5329ab"}, + {file = "librt-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:e9a3a38eb4134ad33122a6d575e6324831f930a771d951a15ce232e0237412c2"}, + {file = "librt-0.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:421765e8c6b18e64d21c8ead315708a56fc24f44075059702e421d164575fdda"}, + {file = "librt-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:48f84830a8f8ad7918afd743fd7c4eb558728bceab7b0e38fd5a5cf78206a556"}, + {file = "librt-0.8.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f09d4884f882baa39a7e36bbf3eae124c4ca2a223efb91e567381d1c55c6b06"}, + {file = "librt-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:693697133c3b32aa9b27f040e3691be210e9ac4d905061859a9ed519b1d5a376"}, + {file = "librt-0.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5512aae4648152abaf4d48b59890503fcbe86e85abc12fb9b096fe948bdd816"}, + {file = "librt-0.8.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:995d24caa6bbb34bcdd4a41df98ac6d1af637cfa8975cb0790e47d6623e70e3e"}, + {file = "librt-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b9aef96d7593584e31ef6ac1eb9775355b0099fee7651fae3a15bc8657b67b52"}, + {file = "librt-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4f6e975377fbc4c9567cb33ea9ab826031b6c7ec0515bfae66a4fb110d40d6da"}, + {file = "librt-0.8.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:daae5e955764be8fd70a93e9e5133c75297f8bce1e802e1d3683b98f77e1c5ab"}, + {file = "librt-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7bd68cebf3131bb920d5984f75fe302d758db33264e44b45ad139385662d7bc3"}, + {file = "librt-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1e6811cac1dcb27ca4c74e0ca4a5917a8e06db0d8408d30daee3a41724bfde7a"}, + {file = "librt-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:178707cda89d910c3b28bf5aa5f69d3d4734e0f6ae102f753ad79edef83a83c7"}, + {file = "librt-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3e8b77b5f54d0937b26512774916041756c9eb3e66f1031971e626eea49d0bf4"}, + {file = "librt-0.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:789911e8fa40a2e82f41120c936b1965f3213c67f5a483fc5a41f5839a05dcbb"}, + {file = "librt-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2b37437e7e4ef5e15a297b36ba9e577f73e29564131d86dd75875705e97402b5"}, + {file = "librt-0.8.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:671a6152edf3b924d98a5ed5e6982ec9cb30894085482acadce0975f031d4c5c"}, + {file = "librt-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8992ca186a1678107b0af3d0c9303d8c7305981b9914989b9788319ed4d89546"}, + {file = "librt-0.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:001e5330093d887b8b9165823eca6c5c4db183fe4edea4fdc0680bbac5f46944"}, + {file = "librt-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d920789eca7ef71df7f31fd547ec0d3002e04d77f30ba6881e08a630e7b2c30e"}, + {file = "librt-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:82fb4602d1b3e303a58bfe6165992b5a78d823ec646445356c332cd5f5bbaa61"}, + {file = "librt-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4d3e38797eb482485b486898f89415a6ab163bc291476bd95712e42cf4383c05"}, + {file = "librt-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a905091a13e0884701226860836d0386b88c72ce5c2fdfba6618e14c72be9f25"}, + {file = "librt-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:375eda7acfce1f15f5ed56cfc960669eefa1ec8732e3e9087c3c4c3f2066759c"}, + {file = "librt-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:2ccdd20d9a72c562ffb73098ac411de351b53a6fbb3390903b2d33078ef90447"}, + {file = "librt-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:25e82d920d4d62ad741592fcf8d0f3bda0e3fc388a184cb7d2f566c681c5f7b9"}, + {file = "librt-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:92249938ab744a5890580d3cb2b22042f0dce71cdaa7c1369823df62bedf7cbc"}, + {file = "librt-0.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4b705f85311ee76acec5ee70806990a51f0deb519ea0c29c1d1652d79127604d"}, + {file = "librt-0.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7ce0a8cb67e702dcb06342b2aaaa3da9fb0ddc670417879adfa088b44cf7b3b6"}, + {file = "librt-0.8.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:aaadec87f45a3612b6818d1db5fbfe93630669b7ee5d6bdb6427ae08a1aa2141"}, + {file = "librt-0.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56901f1eec031396f230db71c59a01d450715cbbef9856bf636726994331195d"}, + {file = "librt-0.8.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b055bb3abaf69abed25743d8fc1ab691e4f51a912ee0a6f9a6c84f4bbddb283d"}, + {file = "librt-0.8.0-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ef3bd856373cf8e7382402731f43bfe978a8613b4039e49e166e1e0dc590216"}, + {file = "librt-0.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2e0ffe88ebb5962f8fb0ddcbaaff30f1ea06a79501069310e1e030eafb1ad787"}, + {file = "librt-0.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82e61cd1c563745ad495387c3b65806bfd453badb4adbc019df3389dddee1bf6"}, + {file = "librt-0.8.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:667e2513cf69bfd1e1ed9a00d6c736d5108714ec071192afb737987955888a25"}, + {file = "librt-0.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6b6caff69e25d80c269b1952be8493b4d94ef745f438fa619d7931066bdd26de"}, + {file = "librt-0.8.0-cp39-cp39-win32.whl", hash = "sha256:02a9fe85410cc9bef045e7cb7fd26fdde6669e6d173f99df659aa7f6335961e9"}, + {file = "librt-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:de076eaba208d16efb5962f99539867f8e2c73480988cb513fcf1b5dbb0c9dcf"}, + {file = "librt-0.8.0.tar.gz", hash = "sha256:cb74cdcbc0103fc988e04e5c58b0b31e8e5dd2babb9182b6f9490488eb36324b"}, ] [[package]] @@ -972,6 +1004,105 @@ profiling = ["gprof2dot"] rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + [[package]] name = "mccabe" version = "0.7.0" @@ -1241,103 +1372,103 @@ tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] [[package]] name = "pillow" -version = "12.1.0" +version = "12.1.1" description = "Python Imaging Library (fork)" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd"}, - {file = "pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0"}, - {file = "pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8"}, - {file = "pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1"}, - {file = "pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda"}, - {file = "pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7"}, - {file = "pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a"}, - {file = "pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef"}, - {file = "pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09"}, - {file = "pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91"}, - {file = "pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea"}, - {file = "pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3"}, - {file = "pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0"}, - {file = "pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451"}, - {file = "pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e"}, - {file = "pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84"}, - {file = "pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0"}, - {file = "pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b"}, - {file = "pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18"}, - {file = "pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64"}, - {file = "pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75"}, - {file = "pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304"}, - {file = "pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b"}, - {file = "pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551"}, - {file = "pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208"}, - {file = "pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5"}, - {file = "pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661"}, - {file = "pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17"}, - {file = "pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670"}, - {file = "pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616"}, - {file = "pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7"}, - {file = "pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d"}, - {file = "pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c"}, - {file = "pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1"}, - {file = "pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179"}, - {file = "pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0"}, - {file = "pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587"}, - {file = "pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac"}, - {file = "pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b"}, - {file = "pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea"}, - {file = "pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c"}, - {file = "pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc"}, - {file = "pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644"}, - {file = "pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c"}, - {file = "pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171"}, - {file = "pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a"}, - {file = "pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45"}, - {file = "pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d"}, - {file = "pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0"}, - {file = "pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554"}, - {file = "pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e"}, - {file = "pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82"}, - {file = "pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4"}, - {file = "pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0"}, - {file = "pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b"}, - {file = "pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65"}, - {file = "pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0"}, - {file = "pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8"}, - {file = "pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91"}, - {file = "pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796"}, - {file = "pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd"}, - {file = "pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13"}, - {file = "pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e"}, - {file = "pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643"}, - {file = "pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5"}, - {file = "pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de"}, - {file = "pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9"}, - {file = "pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a"}, - {file = "pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a"}, - {file = "pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030"}, - {file = "pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94"}, - {file = "pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4"}, - {file = "pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2"}, - {file = "pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61"}, - {file = "pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51"}, - {file = "pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc"}, - {file = "pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14"}, - {file = "pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8"}, - {file = "pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924"}, - {file = "pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef"}, - {file = "pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988"}, - {file = "pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6"}, - {file = "pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19"}, - {file = "pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9"}, + {file = "pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0"}, + {file = "pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4"}, + {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e"}, + {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff"}, + {file = "pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40"}, + {file = "pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23"}, + {file = "pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9"}, + {file = "pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32"}, + {file = "pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b"}, + {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5"}, + {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d"}, + {file = "pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c"}, + {file = "pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563"}, + {file = "pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80"}, + {file = "pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052"}, + {file = "pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0"}, + {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3"}, + {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35"}, + {file = "pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a"}, + {file = "pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6"}, + {file = "pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6"}, + {file = "pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60"}, + {file = "pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717"}, + {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a"}, + {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029"}, + {file = "pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b"}, + {file = "pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1"}, + {file = "pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a"}, + {file = "pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da"}, + {file = "pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13"}, + {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf"}, + {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524"}, + {file = "pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986"}, + {file = "pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c"}, + {file = "pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642"}, + {file = "pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd"}, + {file = "pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e"}, + {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0"}, + {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb"}, + {file = "pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f"}, + {file = "pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15"}, + {file = "pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f"}, + {file = "pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8"}, + {file = "pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586"}, + {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce"}, + {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8"}, + {file = "pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36"}, + {file = "pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b"}, + {file = "pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e"}, + {file = "pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4"}, ] [package.extras] @@ -1428,21 +1559,16 @@ testing = ["aboutcode-toolkit (>=6.0.0)", "black", "pytest (>=6,!=7.0.0)", "pyte [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.7.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, - {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, + {file = "platformdirs-4.7.0-py3-none-any.whl", hash = "sha256:1ed8db354e344c5bb6039cd727f096af975194b508e37177719d562b2b540ee6"}, + {file = "platformdirs-4.7.0.tar.gz", hash = "sha256:fd1a5f8599c85d49b9ac7d6e450bc2f1aaf4a23f1fe86d09952fe20ad365cf36"}, ] -[package.extras] -docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] -type = ["mypy (>=1.18.2)"] - [[package]] name = "py-serializable" version = "2.1.0" @@ -2004,30 +2130,30 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.15.0" +version = "0.15.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455"}, - {file = "ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d"}, - {file = "ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce"}, - {file = "ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621"}, - {file = "ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9"}, - {file = "ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179"}, - {file = "ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d"}, - {file = "ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78"}, - {file = "ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4"}, - {file = "ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e"}, - {file = "ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662"}, - {file = "ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1"}, - {file = "ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16"}, - {file = "ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3"}, - {file = "ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3"}, - {file = "ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18"}, - {file = "ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a"}, - {file = "ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a"}, + {file = "ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a"}, + {file = "ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602"}, + {file = "ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899"}, + {file = "ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16"}, + {file = "ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc"}, + {file = "ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779"}, + {file = "ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb"}, + {file = "ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83"}, + {file = "ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2"}, + {file = "ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454"}, + {file = "ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c"}, + {file = "ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330"}, + {file = "ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61"}, + {file = "ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f"}, + {file = "ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098"}, + {file = "ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336"}, + {file = "ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416"}, + {file = "ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f"}, ] [[package]] @@ -2319,4 +2445,4 @@ mongodb = ["pymongo"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.13" -content-hash = "c1db37cabcddd05d318573d632529be139b1ce1430e7eda7c915bb9d171c7a77" +content-hash = "349ea21b64217dca053564867ff9f7cf5375fe7c8dee163a5689d9f3a25fd371" diff --git a/pyproject.toml b/pyproject.toml index 054fc24..413922a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,74 +1,74 @@ -[project] -name = "tiny" -version = "1.0.0" -description = "A package for URL Shortener with QR Code generation" -authors = [ - { name = "recursivezero", email = "152776938+recursivezero@users.noreply.github.com" }, -] -maintainers = [ - { name = "Harsh Mishra", email = "187759129+harshmishra2701@users.noreply.github.com" }, -] -license = { text = "MIT" } -readme = "README.md" -requires-python = ">=3.10,<3.13" -keywords = ["url-shortener", "qr-code", "fastapi", "qrcode"] -classifiers = [ - "Programming Language :: Python :: 3", - "Topic :: Software Development :: Libraries", -] - -# Use PEP 508 syntax for production dependencies -dependencies = [ - "python-dotenv>=1.0.1", - "qrcode>=8.2", - "pillow>=10.4.0", - "redis>=7.1.0,<8.0.0", - "uvicorn>=0.40.0,<0.41.0", - "fastapi>=0.128.0,<0.129.0", - "pydantic>=2.12.5,<3.0.0", - "validators>=0.35.0,<0.36.0", - "itsdangerous>=2.2.0,<3.0.0", - "python-multipart>=0.0.22,<0.0.23", - "jinja2>=3.1.2", -] -[project.optional-dependencies] -mongodb = ["pymongo>=4.16.0,<5.0.0"] - -[project.scripts] -tiny = "app.cli:main" -lint = "app.utils.lint:main" - -# MODERN: Use dependency-groups (PEP 735) instead of tool.poetry.group -[dependency-groups] -dev = [ - "black>=24.10.0", - "flake8>=7.1.1", - "mypy>=1.14.1", - "pip-audit>=2.9.0", - "ruff>=0.5.3", - "twine>=6.2.0", - "vulture>=2.3", - "pymongo>=4.16.0", - "types-pillow>=10.2.0.20240822", - "types-pywin32>=308.0.0.20250128", -] - -[build-system] -requires = ["poetry-core>=2.0.0"] -build-backend = "poetry.core.masonry.api" - -[tool.poetry] -# We keep this for Poetry-specific settings like the package location -packages = [{ include = "app" }] -requires-poetry = ">=2.0.0" - -[tool.poetry.requires-plugins] -poetry-plugin-export = "^1.9" - -[tool.poetry-auto-export] -output = "requirements.txt" -without_hashes = true -without = ["dev"] - -[tool.flake8] -max_line_length = 190 +[project] +name = "tiny" +version = "1.0.1" +description = "A package for URL Shortener with QR Code generation" +authors = [ + { name = "recursivezero", email = "152776938+recursivezero@users.noreply.github.com" }, +] +maintainers = [ + { name = "Harsh Mishra", email = "187759129+harshmishra2701@users.noreply.github.com" }, +] +license = { text = "MIT" } +readme = "README.md" +requires-python = ">=3.10,<3.13" +keywords = ["url-shortener", "qr-code", "fastapi", "qrcode"] +classifiers = [ + "Programming Language :: Python :: 3", + "Topic :: Software Development :: Libraries", +] + +# Use PEP 508 syntax for production dependencies +dependencies = [ + "python-dotenv>=1.0.1", + "qrcode>=8.2", + "pillow>=10.4.0", + "redis>=7.1.0,<8.0.0", + "uvicorn>=0.40.0,<0.41.0", + "fastapi>=0.128.0,<0.129.0", + "pydantic>=2.12.5,<3.0.0", + "validators>=0.35.0,<0.36.0", + "itsdangerous>=2.2.0,<3.0.0", + "python-multipart>=0.0.22,<0.0.23", + "jinja2>=3.1.2", +] +[project.optional-dependencies] +mongodb = ["pymongo>=4.16.0,<5.0.0"] + +[project.scripts] +tiny = "app.cli:main" +lint = "app.utils.lint:main" + +# MODERN: Use dependency-groups (PEP 735) instead of tool.poetry.group +[dependency-groups] +dev = [ + "black>=24.10.0", + "flake8>=7.1.1", + "mypy>=1.14.1", + "pip-audit>=2.9.0", + "ruff>=0.5.3", + "twine>=6.2.0", + "vulture>=2.3", + "pymongo>=4.16.0", + "types-pillow>=10.2.0.20240822", + "types-pywin32>=308.0.0.20250128", +] + +[build-system] +requires = ["poetry-core>=2.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +# We keep this for Poetry-specific settings like the package location +packages = [{ include = "app" }] +requires-poetry = ">=2.0.0" + +[tool.poetry.requires-plugins] +poetry-plugin-export = "^1.9" + +[tool.poetry-auto-export] +output = "requirements.txt" +without_hashes = true +without = ["dev"] + +[tool.flake8] +max_line_length = 190 diff --git a/request/urls.json b/request/urls.json new file mode 100644 index 0000000..7f48e57 --- /dev/null +++ b/request/urls.json @@ -0,0 +1,32 @@ +[ + { + "url": "https://example.com" + }, + { + "url": "https://google.com" + }, + { + "url": "htt//github.com" + }, + { + "url": "https://stackoverflow.com" + }, + { + "url": "https://microsoft.com" + }, + { + "url": "https://apple.com" + }, + { + "url": "https:openai.com" + }, + { + "url": "https://python.org" + }, + { + "url": "https://fastapi.tiangolo.com" + }, + { + "url": "https://docs.github.com" + } +] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7857e7c..bf81eea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,23 +1,27 @@ -annotated-doc==0.0.4 ; python_version >= "3.10" and python_version < "3.13" -annotated-types==0.7.0 ; python_version >= "3.10" and python_version < "3.13" -anyio==4.12.1 ; python_version >= "3.10" and python_version < "3.13" -async-timeout==5.0.1 ; python_version >= "3.10" and python_full_version < "3.11.3" -click==8.3.1 ; python_version >= "3.10" and python_version < "3.13" -colorama==0.4.6 ; python_version >= "3.10" and python_version < "3.13" and (sys_platform == "win32" or platform_system == "Windows") -exceptiongroup==1.3.1 ; python_version == "3.10" -fastapi==0.128.7 ; python_version >= "3.10" and python_version < "3.13" -h11==0.16.0 ; python_version >= "3.10" and python_version < "3.13" -idna==3.11 ; python_version >= "3.10" and python_version < "3.13" -itsdangerous==2.2.0 ; python_version >= "3.10" and python_version < "3.13" -pillow==10.4.0 ; python_version >= "3.10" and python_version < "3.13" -pydantic-core==2.41.5 ; python_version >= "3.10" and python_version < "3.13" -pydantic==2.12.5 ; python_version >= "3.10" and python_version < "3.13" -python-dotenv==1.0.1 ; python_version >= "3.10" and python_version < "3.13" -python-multipart==0.0.22 ; python_version >= "3.10" and python_version < "3.13" -qrcode==8.2 ; python_version >= "3.10" and python_version < "3.13" -redis==7.1.1 ; python_version >= "3.10" and python_version < "3.13" -starlette==0.52.1 ; python_version >= "3.10" and python_version < "3.13" -typing-extensions==4.15.0 ; python_version >= "3.10" and python_version < "3.13" -typing-inspection==0.4.2 ; python_version >= "3.10" and python_version < "3.13" -uvicorn==0.40.0 ; python_version >= "3.10" and python_version < "3.13" -validators==0.35.0 ; python_version >= "3.10" and python_version < "3.13" +annotated-doc==0.0.4 ; python_version >= "3.10" and python_version < "3.13" +annotated-types==0.7.0 ; python_version >= "3.10" and python_version < "3.13" +anyio==4.12.1 ; python_version >= "3.10" and python_version < "3.13" +async-timeout==5.0.1 ; python_version >= "3.10" and python_full_version < "3.11.3" +click==8.3.1 ; python_version >= "3.10" and python_version < "3.13" +colorama==0.4.6 ; python_version >= "3.10" and python_version < "3.13" and (sys_platform == "win32" or platform_system == "Windows") +dnspython==2.8.0 ; python_version >= "3.10" and python_version < "3.13" +exceptiongroup==1.3.1 ; python_version == "3.10" +fastapi==0.128.8 ; python_version >= "3.10" and python_version < "3.13" +h11==0.16.0 ; python_version >= "3.10" and python_version < "3.13" +idna==3.11 ; python_version >= "3.10" and python_version < "3.13" +itsdangerous==2.2.0 ; python_version >= "3.10" and python_version < "3.13" +jinja2==3.1.6 ; python_version >= "3.10" and python_version < "3.13" +markupsafe==3.0.3 ; python_version >= "3.10" and python_version < "3.13" +pillow==12.1.1 ; python_version >= "3.10" and python_version < "3.13" +pydantic-core==2.41.5 ; python_version >= "3.10" and python_version < "3.13" +pydantic==2.12.5 ; python_version >= "3.10" and python_version < "3.13" +pymongo==4.16.0 ; python_version >= "3.10" and python_version < "3.13" +python-dotenv==1.2.1 ; python_version >= "3.10" and python_version < "3.13" +python-multipart==0.0.22 ; python_version >= "3.10" and python_version < "3.13" +qrcode==8.2 ; python_version >= "3.10" and python_version < "3.13" +redis==7.1.1 ; python_version >= "3.10" and python_version < "3.13" +starlette==0.52.1 ; python_version >= "3.10" and python_version < "3.13" +typing-extensions==4.15.0 ; python_version >= "3.10" and python_version < "3.13" +typing-inspection==0.4.2 ; python_version >= "3.10" and python_version < "3.13" +uvicorn==0.40.0 ; python_version >= "3.10" and python_version < "3.13" +validators==0.35.0 ; python_version >= "3.10" and python_version < "3.13"