diff --git a/.coverage b/.coverage new file mode 100644 index 000000000..44fe1534a Binary files /dev/null and b/.coverage differ diff --git a/.gitignore b/.gitignore index 2cba99d87..e7e28eb67 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ bin include lib .Python -tests/ .envrc -__pycache__ \ No newline at end of file +__pycache__ +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 61307d2cd..8d3def852 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,256 @@ -# gudlift-registration +# Flask App - OpenClassrooms Project 11 +**Improve a web Flask app by testing and debugging** -1. Why +--- +## DESCRIPTION - This is a proof of concept (POC) project to show a light-weight version of our competition booking platform. The aim is the keep things as light as possible, and use feedback from the users to iterate. +This project was completed as part of the "Python Developer" path at OpenClassrooms. -2. Getting Started +The goal was to improve a web application, through testing and debugging, capable of: - This project uses the following technologies: +- managing lifting competitions +- allowing clubs to authenticate themselves within the application +- allowing clubs to reserve places in several lifting competitions - * Python v3.x+ +The application must: - * [Flask](https://flask.palletsprojects.com/en/1.1.x/) +- allow the club secretary to log in. +- allow the club secretary to view competitions and other clubs' points. +- allow the club secretary to use their points to purchase competition tickets. +- display error messages in case of incorrect action, according to the application specifications. - Whereas Django does a lot of things for us out of the box, Flask allows us to add only what we need. - +--- - * [Virtual environment](https://virtualenv.pypa.io/en/stable/installation.html) +## PROJECT STRUCTURE +

+ +

- This ensures you'll be able to install the correct packages without interfering with Python on your machine. +--- - Before you begin, please ensure you have this installed globally. +## INSTALLATION +- ### Clone the repository : -3. Installation +``` +git clone https://github.com/Tit-Co/OpenClassrooms_Project_11.git +``` - - After cloning, change into the directory and type virtualenv .. This will then set up a a virtual python environment within that directory. +- ### Navigate into the project directory : + `cd OpenClassrooms_Project_11` - - Next, type source bin/activate. You should see that your command prompt has changed to the name of the folder. This means that you can install packages in here without affecting affecting files outside. To deactivate, type deactivate +- ### Create a virtual environment and dependencies : - - Rather than hunting around for the packages you need, you can install in one step. Type pip install -r requirements.txt. This will install all the packages listed in the respective file. If you install a package, make sure others know by updating the requirements.txt file. An easy way to do this is pip freeze > requirements.txt +1. #### With [uv](https://docs.astral.sh/uv/) - - Flask requires that you set an environmental variable to the python file. However you do that, you'll want to set the file to be server.py. Check [here](https://flask.palletsprojects.com/en/1.1.x/quickstart/#a-minimal-application) for more details + `uv` is an environment and dependencies manager. + + - #### Install environment and dependencies + + `uv sync` - - You should now be ready to test the application. In the directory, type either flask run or python -m flask run. The app should respond with an address you should be able to go to using your browser. +2. #### With pip -4. Current Setup + - #### Install the virtual env : - The app is powered by [JSON files](https://www.tutorialspoint.com/json/json_quick_guide.htm). This is to get around having a DB until we actually need one. The main ones are: - - * competitions.json - list of competitions - * clubs.json - list of clubs with relevant information. You can look here to see what email addresses the app will accept for login. + `python -m venv env` -5. Testing + - #### Activate the virtual env : + `source env/bin/activate` + Or + `env\Scripts\activate` on Windows - You are free to use whatever testing framework you like-the main thing is that you can show what tests you are using. +3. #### With [Poetry](https://python-poetry.org/docs/) - We also like to show how well we're testing, so there's a module called - [coverage](https://coverage.readthedocs.io/en/coverage-5.1/) you should add to your project. + `Poetry` is a tool for dependency management and packaging in Python. + + - #### Install the virtual env : + `py -3.13 -m venv env` + + - #### Activate the virtual env : + `poetry env activate` +- ### Install dependencies + 1. #### With [uv](https://docs.astral.sh/uv/) + `uv sync` or `uv pip install -r requirements.txt` + + 2. #### With pip + `pip install -r requirements.txt` + + 3. #### With [Poetry](https://python-poetry.org/docs/) + `poetry install` + + (NB : Poetry and uv will read the `pyproject.toml` file to know which dependencies to install) + +--- + +## USAGE + +### Launching server +- Open a terminal +- Go to project folder - example : `cd gudlft` +- Activate the virtual environment as described previously +- Create flask environment : + - With PowerShell : + ``` + $env:FLASK_APP = "server" + ``` + Plus, in option, the 2 lines below for the good execution of performance tests + ``` + $env:CLUBS_JSON="D:\\clubs_tmp.json" + $env:COMPETITIONS_JSON="D:\\competitions_tmp.json" + ``` + + - With Git Bash (with the temp JSON env variables) : + ``` + export FLASK_APP=server + ``` + Plus, in option, the line below for the good execution of performance tests + ``` + export CLUBS_JSON="/d//clubs_tmp.json" && export COMPETITIONS_JSON="/d//competitions_tmp.json" + ``` + +- Finally, launch the Django server : `flask run` + +### Launching the APP +- Open a web browser +- And type the URL : `http://127.0.0.1:5000/` + +--- + +## ISSUES THAT HAS BEEN FIXED + +- A user types in an email not found in the system +- The club should not be able to redeem more points than available +- The club should be able to book no more than 12 places +- The club should not be able to book a place on a post-dated competition (but past competitions should be visible). +- The amount of points used should be deducted from the club's balance +- The club should be able to see the list of clubs and their associated current points balance +- The club should not be able to book more than the competition places available + +--- + +## EXPLANATIONS OF WHAT THE APP DOES + +### Registration +- The user can sign in the application by typing the club name, email and password (two times). + +### Authentication +- The user can log in the application by typing the club email and password. + +### Profile page +- The user can see the club profile by clicking on "go to profile" link in welcome page. + +### Welcome page +- The user can view all past and upcoming competitions, and access the booking page by clicking on the competition they +wish to participate in. + +### Booking page +- The user can book places in the competition by typing the number of places. The UI will display a message according +to the validation or rejection of the entry. + +### Log out +- The user can log out by clicking the appropriate link. + +### Points board +- The user can view the points board with all the clubs and their points. +- No need to be authenticated to see this board. + +--- + +## TEMPLATES EXAMPLES + +- Registration +

+ +

+ +- Authentication +

+ +

+ +- Profile page +

+ +

+ +- Change password +

+ +

+ +- Points board +

+ +

+ +- Welcome page +

+ +

+ +- Booking page +

+ +

+ +- Log out +

+ +

+ +--- + +## PEP 8 CONVENTIONS + +- Flake 8 report +

+ +

+ +**Type the line below in the terminal to generate another report with [flake8-html](https://pypi.org/project/flake8-html/) tool :** + +` flake8 --format=html --htmldir=flake8-report --max-line-length=119 --extend-exclude=env/, cov_html/ --ignore=E402` +(E402 error ignored because we need to import some libraries after some variables' declaration and assignation in tests) + +--- + +## TESTS COVERAGE WITH PYTEST + +- Coverage report +

+ + +

+ +- **Type the line below in the terminal to generate another coverage report with pytest** + + `pytest --cov=server --cov-report=html:` + +--- + +## PERFORMANCE TESTS WITH LOCUST + +- Locust report example +

+ +

+ +- **Launch the application as described previously (and do not forget the environment variables for temp JSON files)** + +- **Open another terminal, activate the virtual environment, go into the locust test folder, + and type the `locust` command to launch the performance tests** + +--- + +![Python](https://img.shields.io/badge/python-3.13-blue.svg) +![License](https://img.shields.io/badge/license-MIT-green.svg) + +--- + +## AUTHOR +**Name**: Nicolas MARIE +**Track**: Python Developer – OpenClassrooms +**Project 11 – Improve a Flask web app by testing and debugging – March 2025** diff --git a/clubs.json b/clubs.json index 1d7ad1ffe..84d26e892 100644 --- a/clubs.json +++ b/clubs.json @@ -1,16 +1,76 @@ -{"clubs":[ - { - "name":"Simply Lift", - "email":"john@simplylift.co", - "points":"13" - }, - { - "name":"Iron Temple", - "email": "admin@irontemple.com", - "points":"4" - }, - { "name":"She Lifts", - "email": "kate@shelifts.co.uk", - "points":"12" - } -]} \ No newline at end of file +{ + "clubs": [ + { + "name": "Power Lift", + "email": "admin@powerlift.com", + "password": "scrypt:32768:8:1$c0ePLkJjwFO0xhpP$a9c637393de25e3f6b121ab38d3d9c3589d66879618157ea3382ae9ee2310c28265be341d4af02c4c1d1f81427ccdbc96464b0bb19697a819f5ad987764c17ad", + "points": "5" + }, + { + "name": "Titanium Tribe", + "email": "info@titaniumtribe.com", + "password": "scrypt:32768:8:1$4bM8hSwBdKH0Dj2n$d6d64d3a635cc260803451f8b0c9acc1e50bf3a48ab071e9389d9e492cce5131d6c011f841d92b7422bbfdfa6609e198ec529fc3394cb27788a1a696baba767e", + "points": "15" + }, + { + "name": "Steel Strength", + "email": "hello@steelstrength.org", + "password": "scrypt:32768:8:1$uGiMzIndzui0kLQp$50ef79e3db9134208a46072a6297637a5636d81ca10b9a42d69883c8a12ee021fdab58df6019339ebc8924312598c4d37a6d71ce8647177241ddf012c69acafc", + "points": "15" + }, + { + "name": "Heavy Hitters", + "email": "admin@heavyhitters.net", + "password": "scrypt:32768:8:1$U4IUVIbiypkLsiwd$b57afe44db3af13b7e9772560a228c1100f41c3222b0cbbaa5c8855632fbca004c7d2ca36d7bef52624ee028373a0cd90df65fb9b73d4eb119ad1edf3a741bf9", + "points": "15" + }, + { + "name": "Power Lifts Club", + "email": "admin@powerlifts.com", + "password": "scrypt:32768:8:1$zdBGCvw6xXxHzAGl$c4534f2b7b2089aeca6f50bf3221f86f885aa4139692844149becf3c56b37a3c8f6a5f7c47e8c4f60405d591f58cf8f9567d1e383396a5f4de87073d441e5fe3", + "points": "15" + }, + { + "name": "Olympic Lifters", + "email": "contact@olympiclifters.com", + "password": "scrypt:32768:8:1$CO1XjG2GYak7bGad$906886f7afc2f66ac120f12b263e195fd44283a3cf0a39f963722ca25fc62e0909629165ff184ab67e43dfb44b4ca87ec7448ce95bcd896af2d2c32a31ffa604", + "points": "15" + }, + { + "name": "Barbell Warriors", + "email": "info@barbellwarriors.com", + "password": "scrypt:32768:8:1$ZmtTQFYQVRwhl8cK$d4abb1c57ba950bb082a6c963605af00a0e120f7b6d7346d2a9419efad6d7d5a78c7d15a32b57d4fc2eab10aa864977be0a3734c24f69bc6694cd9b224b57545", + "points": "15" + }, + { + "name": "Simply Lift", + "email": "john@simplylift.co", + "password": "scrypt:32768:8:1$cYYbKbko75gPN3N3$4883a2360abf24fcc9f8329aedfca168994222748933eff4d89c78e5764f0972fce4c478b5737ece1bd31a3d2a0f1a00586096ffbddb79809b9685eabc5e5f4e", + "points": "13" + }, + { + "name": "She Lifts", + "email": "kate@shelifts.co.uk", + "password": "scrypt:32768:8:1$E4fcXT3MQ7oVfc0w$cf31c4a6aec02d4532ffcae03670b12f20265036a3ae5ee941fae6d135ca2db5e406fe9e6c6eded7776e0c256f6d8970f2cbfb74ee50a1c7f9b49b1ad6819b40", + "points": "12" + }, + { + "name": "Iron Titans", + "email": "contact@irontitans.com", + "password": "scrypt:32768:8:1$kRW7krsT6zGr4iLk$6a24553114aa57f8cf4297786bf6136fa1d6f5f9832c6157fb406eb4459fcde2a82ed2a64d890ebe5e9a6efe73376239c59a124c77b6bd79b371c84095496b78", + "points": "15" + }, + { + "name": "The Weightroom", + "email": "team@weightroom.com", + "password": "scrypt:32768:8:1$38I8jEaWVYS5jgcA$0b29c9aa0e7f52b2634516d46b312ba429d2748fd4d72973f83275d4ada39907a6bae0389d31a868acdd4b5b43363515186f86e5f7419f9253d092c94d2a9185", + "points": "15" + }, + { + "name": "Iron Temple", + "email": "admin@irontemple.com", + "password": "scrypt:32768:8:1$rHMWqkkiOSz7QiYU$e1a798fc6d464425359fb999db786ffbe39f6059e195d6d22a15e3ed70be3b5630e1d828399ca241f9d94e109ee5b350067f14a8054079344f01495c73594021", + "points": "4" + } + ] +} \ No newline at end of file diff --git a/competitions.json b/competitions.json index 039fc61bd..b7301d946 100644 --- a/competitions.json +++ b/competitions.json @@ -1,14 +1,76 @@ { "competitions": [ + { + "name": "Fall Classic", + "date": "2020-10-22 13:30:00", + "number_of_places": "13", + "is_past": true + }, + { + "name": "Barbell Classic", + "date": "2025-04-15 10:00:00", + "number_of_places": "0", + "is_past": true + }, + { + "name": "Olympic Meet", + "date": "2025-05-20 10:30:00", + "number_of_places": "25", + "is_past": true + }, + { + "name": "Iron Challenge", + "date": "2025-10-22 09:30:00", + "number_of_places": "15", + "is_past": true + }, + { + "name": "Summer Stronger", + "date": "2026-05-30 18:23:40", + "number_of_places": "0", + "is_past": false + }, + { + "name": "Winter Power", + "date": "2026-06-26 12:16:00", + "number_of_places": "4", + "is_past": false + }, + { + "name": "Strength Showdown", + "date": "2026-07-10 14:00:00", + "number_of_places": "0", + "is_past": false + }, { "name": "Spring Festival", - "date": "2020-03-27 10:00:00", - "numberOfPlaces": "25" + "date": "2026-07-27 10:00:00", + "number_of_places": "25", + "is_past": false }, { - "name": "Fall Classic", - "date": "2020-10-22 13:30:00", - "numberOfPlaces": "13" + "name": "Regional Strength", + "date": "2026-09-12 15:00:00", + "number_of_places": "45", + "is_past": false + }, + { + "name": "Heavyweight First", + "date": "2026-11-08 13:00:00", + "number_of_places": "30", + "is_past": false + }, + { + "name": "Powerlifting Open", + "date": "2026-12-05 11:00:00", + "number_of_places": "60", + "is_past": false + }, + { + "name": "Lifting Championship", + "date": "2027-06-18 12:00:00", + "number_of_places": "0", + "is_past": false } ] } \ No newline at end of file diff --git a/cov_html/.gitignore b/cov_html/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/cov_html/class_index.html b/cov_html/class_index.html new file mode 100644 index 000000000..1bc7dc280 --- /dev/null +++ b/cov_html/class_index.html @@ -0,0 +1,121 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 84% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.13.4, + created at 2026-04-07 14:49 +0200 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Fileclass statementsmissingexcluded coverage
server.py(no class) 151240 84%
Total  151240 84%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/cov_html/coverage_html_cb_188fc9a4.js b/cov_html/coverage_html_cb_188fc9a4.js new file mode 100644 index 000000000..6f871742c --- /dev/null +++ b/cov_html/coverage_html_cb_188fc9a4.js @@ -0,0 +1,735 @@ +// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +// For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt + +// Coverage.py HTML report browser code. +/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ +/*global coverage: true, document, window, $ */ + +coverage = {}; + +// General helpers +function debounce(callback, wait) { + let timeoutId = null; + return function(...args) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + callback.apply(this, args); + }, wait); + }; +}; + +function checkVisible(element) { + const rect = element.getBoundingClientRect(); + const viewBottom = Math.max(document.documentElement.clientHeight, window.innerHeight); + const viewTop = 30; + return !(rect.bottom < viewTop || rect.top >= viewBottom); +} + +function on_click(sel, fn) { + const elt = document.querySelector(sel); + if (elt) { + elt.addEventListener("click", fn); + } +} + +// Helpers for table sorting +function getCellValue(row, column = 0) { + const cell = row.cells[column] // nosemgrep: eslint.detect-object-injection + if (cell.childElementCount == 1) { + var child = cell.firstElementChild; + if (child.tagName === "A") { + child = child.firstElementChild; + } + if (child instanceof HTMLDataElement && child.value) { + return child.value; + } + } + return cell.innerText || cell.textContent; +} + +function rowComparator(rowA, rowB, column = 0) { + let valueA = getCellValue(rowA, column); + let valueB = getCellValue(rowB, column); + if (!isNaN(valueA) && !isNaN(valueB)) { + return valueA - valueB; + } + return valueA.localeCompare(valueB, undefined, {numeric: true}); +} + +function sortColumn(th) { + // Get the current sorting direction of the selected header, + // clear state on other headers and then set the new sorting direction. + const currentSortOrder = th.getAttribute("aria-sort"); + [...th.parentElement.cells].forEach(header => header.setAttribute("aria-sort", "none")); + var direction; + if (currentSortOrder === "none") { + direction = th.dataset.defaultSortOrder || "ascending"; + } + else if (currentSortOrder === "ascending") { + direction = "descending"; + } + else { + direction = "ascending"; + } + th.setAttribute("aria-sort", direction); + + const column = [...th.parentElement.cells].indexOf(th) + + // Sort all rows and afterwards append them in order to move them in the DOM. + Array.from(th.closest("table").querySelectorAll("tbody tr")) + .sort((rowA, rowB) => rowComparator(rowA, rowB, column) * (direction === "ascending" ? 1 : -1)) + .forEach(tr => tr.parentElement.appendChild(tr)); + + // Save the sort order for next time. + if (th.id !== "region") { + let th_id = "file"; // Sort by file if we don't have a column id + let current_direction = direction; + const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE); + if (stored_list) { + ({th_id, direction} = JSON.parse(stored_list)) + } + localStorage.setItem(coverage.INDEX_SORT_STORAGE, JSON.stringify({ + "th_id": th.id, + "direction": current_direction + })); + if (th.id !== th_id || document.getElementById("region")) { + // Sort column has changed, unset sorting by function or class. + localStorage.setItem(coverage.SORTED_BY_REGION, JSON.stringify({ + "by_region": false, + "region_direction": current_direction + })); + } + } + else { + // Sort column has changed to by function or class, remember that. + localStorage.setItem(coverage.SORTED_BY_REGION, JSON.stringify({ + "by_region": true, + "region_direction": direction + })); + } +} + +// Find all the elements with data-shortcut attribute, and use them to assign a shortcut key. +coverage.assign_shortkeys = function () { + document.querySelectorAll("[data-shortcut]").forEach(element => { + document.addEventListener("keypress", event => { + if (event.target.tagName.toLowerCase() === "input") { + return; // ignore keypress from search filter + } + if (event.key === element.dataset.shortcut) { + element.click(); + } + }); + }); +}; + +// Create the events for the filter box. +coverage.wire_up_filter = function () { + // Populate the filter and hide100 inputs if there are saved values for them. + const saved_filter_value = localStorage.getItem(coverage.FILTER_STORAGE); + if (saved_filter_value) { + document.getElementById("filter").value = saved_filter_value; + } + const saved_hide100_value = localStorage.getItem(coverage.HIDE100_STORAGE); + if (saved_hide100_value) { + document.getElementById("hide100").checked = JSON.parse(saved_hide100_value); + } + + // Cache elements. + const table = document.querySelector("table.index"); + const table_body_rows = table.querySelectorAll("tbody tr"); + const no_rows = document.getElementById("no_rows"); + + const footer = table.tFoot.rows[0]; + const ratio_columns = Array.from(footer.cells).map(cell => Boolean(cell.dataset.ratio)); + + // Observe filter keyevents. + const filter_handler = (event => { + // Keep running total of each metric, first index contains number of shown rows + const totals = ratio_columns.map( + is_ratio => is_ratio ? {"numer": 0, "denom": 0} : 0 + ); + + var text = document.getElementById("filter").value; + // Store filter value + localStorage.setItem(coverage.FILTER_STORAGE, text); + const casefold = (text === text.toLowerCase()); + const hide100 = document.getElementById("hide100").checked; + // Store hide value. + localStorage.setItem(coverage.HIDE100_STORAGE, JSON.stringify(hide100)); + + // Hide / show elements. + table_body_rows.forEach(row => { + var show = false; + // Check the text filter. + for (let column = 0; column < totals.length; column++) { + cell = row.cells[column]; + if (cell.classList.contains("name")) { + var celltext = cell.textContent; + if (casefold) { + celltext = celltext.toLowerCase(); + } + if (celltext.includes(text)) { + show = true; + } + } + } + + // Check the "hide covered" filter. + if (show && hide100) { + const [numer, denom] = row.cells[row.cells.length - 1].dataset.ratio.split(" "); + show = (numer !== denom); + } + + if (!show) { + // hide + row.classList.add("hidden"); + return; + } + + // show + row.classList.remove("hidden"); + totals[0]++; + + for (let column = 0; column < totals.length; column++) { + // Accumulate dynamic totals + cell = row.cells[column] // nosemgrep: eslint.detect-object-injection + if (cell.matches(".name, .spacer")) { + continue; + } + if (ratio_columns[column] && cell.dataset.ratio) { + // Column stores a ratio + const [numer, denom] = cell.dataset.ratio.split(" "); + totals[column]["numer"] += parseInt(numer, 10); // nosemgrep: eslint.detect-object-injection + totals[column]["denom"] += parseInt(denom, 10); // nosemgrep: eslint.detect-object-injection + } + else { + totals[column] += parseInt(cell.textContent, 10); // nosemgrep: eslint.detect-object-injection + } + } + }); + + // Show placeholder if no rows will be displayed. + if (!totals[0]) { + // Show placeholder, hide table. + no_rows.style.display = "block"; + table.style.display = "none"; + return; + } + + // Hide placeholder, show table. + no_rows.style.display = null; + table.style.display = null; + + // Calculate new dynamic sum values based on visible rows. + for (let column = 0; column < totals.length; column++) { + // Get footer cell element. + const cell = footer.cells[column]; // nosemgrep: eslint.detect-object-injection + if (cell.matches(".name, .spacer")) { + continue; + } + + // Set value into dynamic footer cell element. + if (ratio_columns[column]) { + // Percentage column uses the numerator and denominator, + // and adapts to the number of decimal places. + const match = /\.([0-9]+)/.exec(cell.textContent); + const places = match ? match[1].length : 0; + const { numer, denom } = totals[column]; // nosemgrep: eslint.detect-object-injection + cell.dataset.ratio = `${numer} ${denom}`; + // Check denom to prevent NaN if filtered files contain no statements + cell.textContent = denom + ? `${(numer * 100 / denom).toFixed(places)}%` + : `${(100).toFixed(places)}%`; + } + else { + cell.textContent = totals[column]; // nosemgrep: eslint.detect-object-injection + } + } + }); + + document.getElementById("filter").addEventListener("input", debounce(filter_handler)); + document.getElementById("hide100").addEventListener("input", debounce(filter_handler)); + + // Trigger change event on setup, to force filter on page refresh + // (filter value may still be present). + document.getElementById("filter").dispatchEvent(new Event("input")); + document.getElementById("hide100").dispatchEvent(new Event("input")); +}; +coverage.FILTER_STORAGE = "COVERAGE_FILTER_VALUE"; +coverage.HIDE100_STORAGE = "COVERAGE_HIDE100_VALUE"; + +// Set up the click-to-sort columns. +coverage.wire_up_sorting = function () { + document.querySelectorAll("[data-sortable] th[aria-sort]").forEach( + th => th.addEventListener("click", e => sortColumn(e.target)) + ); + + // Look for a localStorage item containing previous sort settings: + let th_id = "file", direction = "ascending"; + const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE); + if (stored_list) { + ({th_id, direction} = JSON.parse(stored_list)); + } + let by_region = false, region_direction = "ascending"; + const sorted_by_region = localStorage.getItem(coverage.SORTED_BY_REGION); + if (sorted_by_region) { + ({ + by_region, + region_direction + } = JSON.parse(sorted_by_region)); + } + + const region_id = "region"; + if (by_region && document.getElementById(region_id)) { + direction = region_direction; + } + // If we are in a page that has a column with id of "region", sort on + // it if the last sort was by function or class. + let th; + if (document.getElementById(region_id)) { + th = document.getElementById(by_region ? region_id : th_id); + } + else { + th = document.getElementById(th_id); + } + th.setAttribute("aria-sort", direction === "ascending" ? "descending" : "ascending"); + th.click() +}; + +coverage.INDEX_SORT_STORAGE = "COVERAGE_INDEX_SORT_2"; +coverage.SORTED_BY_REGION = "COVERAGE_SORT_REGION"; + +// Loaded on index.html +coverage.index_ready = function () { + coverage.assign_shortkeys(); + coverage.wire_up_filter(); + coverage.wire_up_sorting(); + + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + + on_click(".button_show_hide_help", coverage.show_hide_help); +}; + +// -- pyfile stuff -- + +coverage.LINE_FILTERS_STORAGE = "COVERAGE_LINE_FILTERS"; + +coverage.pyfile_ready = function () { + // If we're directed to a particular line number, highlight the line. + var frag = location.hash; + if (frag.length > 2 && frag[1] === "t") { + document.querySelector(frag).closest(".n").classList.add("highlight"); + coverage.set_sel(parseInt(frag.substr(2), 10)); + } + else { + coverage.set_sel(0); + } + + on_click(".button_toggle_run", coverage.toggle_lines); + on_click(".button_toggle_mis", coverage.toggle_lines); + on_click(".button_toggle_exc", coverage.toggle_lines); + on_click(".button_toggle_par", coverage.toggle_lines); + + on_click(".button_next_chunk", coverage.to_next_chunk_nicely); + on_click(".button_prev_chunk", coverage.to_prev_chunk_nicely); + on_click(".button_top_of_page", coverage.to_top); + on_click(".button_first_chunk", coverage.to_first_chunk); + + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + on_click(".button_to_index", coverage.to_index); + + on_click(".button_show_hide_help", coverage.show_hide_help); + + coverage.filters = undefined; + try { + coverage.filters = localStorage.getItem(coverage.LINE_FILTERS_STORAGE); + } catch(err) {} + + if (coverage.filters) { + coverage.filters = JSON.parse(coverage.filters); + } + else { + coverage.filters = {run: false, exc: true, mis: true, par: true}; + } + + for (cls in coverage.filters) { + coverage.set_line_visibilty(cls, coverage.filters[cls]); // nosemgrep: eslint.detect-object-injection + } + + coverage.assign_shortkeys(); + coverage.init_scroll_markers(); + coverage.wire_up_sticky_header(); + + document.querySelectorAll("[id^=ctxs]").forEach( + cbox => cbox.addEventListener("click", coverage.expand_contexts) + ); + + // Rebuild scroll markers when the window height changes. + window.addEventListener("resize", coverage.build_scroll_markers); +}; + +coverage.toggle_lines = function (event) { + const btn = event.target.closest("button"); + const category = btn.value + const show = !btn.classList.contains("show_" + category); + coverage.set_line_visibilty(category, show); + coverage.build_scroll_markers(); + coverage.filters[category] = show; + try { + localStorage.setItem(coverage.LINE_FILTERS_STORAGE, JSON.stringify(coverage.filters)); + } catch(err) {} +}; + +coverage.set_line_visibilty = function (category, should_show) { + const cls = "show_" + category; + const btn = document.querySelector(".button_toggle_" + category); + if (btn) { + if (should_show) { + document.querySelectorAll("#source ." + category).forEach(e => e.classList.add(cls)); + btn.classList.add(cls); + } + else { + document.querySelectorAll("#source ." + category).forEach(e => e.classList.remove(cls)); + btn.classList.remove(cls); + } + } +}; + +// Return the nth line div. +coverage.line_elt = function (n) { + return document.getElementById("t" + n)?.closest("p"); +}; + +// Set the selection. b and e are line numbers. +coverage.set_sel = function (b, e) { + // The first line selected. + coverage.sel_begin = b; + // The next line not selected. + coverage.sel_end = (e === undefined) ? b+1 : e; +}; + +coverage.to_top = function () { + coverage.set_sel(0, 1); + coverage.scroll_window(0); +}; + +coverage.to_first_chunk = function () { + coverage.set_sel(0, 1); + coverage.to_next_chunk(); +}; + +coverage.to_prev_file = function () { + window.location = document.getElementById("prevFileLink").href; +} + +coverage.to_next_file = function () { + window.location = document.getElementById("nextFileLink").href; +} + +coverage.to_index = function () { + location.href = document.getElementById("indexLink").href; +} + +coverage.show_hide_help = function () { + const helpCheck = document.getElementById("help_panel_state") + helpCheck.checked = !helpCheck.checked; +} + +// Return a string indicating what kind of chunk this line belongs to, +// or null if not a chunk. +coverage.chunk_indicator = function (line_elt) { + const classes = line_elt?.className; + if (!classes) { + return null; + } + const match = classes.match(/\bshow_\w+\b/); + if (!match) { + return null; + } + return match[0]; +}; + +coverage.to_next_chunk = function () { + const c = coverage; + + // Find the start of the next colored chunk. + var probe = c.sel_end; + var chunk_indicator, probe_line; + while (true) { + probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + if (chunk_indicator) { + break; + } + probe++; + } + + // There's a next chunk, `probe` points to it. + var begin = probe; + + // Find the end of this chunk. + var next_indicator = chunk_indicator; + while (next_indicator === chunk_indicator) { + probe++; + probe_line = c.line_elt(probe); + next_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(begin, probe); + c.show_selection(); +}; + +coverage.to_prev_chunk = function () { + const c = coverage; + + // Find the end of the prev colored chunk. + var probe = c.sel_begin-1; + var probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + var chunk_indicator = c.chunk_indicator(probe_line); + while (probe > 1 && !chunk_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + } + + // There's a prev chunk, `probe` points to its last line. + var end = probe+1; + + // Find the beginning of this chunk. + var prev_indicator = chunk_indicator; + while (prev_indicator === chunk_indicator) { + probe--; + if (probe <= 0) { + return; + } + probe_line = c.line_elt(probe); + prev_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(probe+1, end); + c.show_selection(); +}; + +// Returns 0, 1, or 2: how many of the two ends of the selection are on +// the screen right now? +coverage.selection_ends_on_screen = function () { + if (coverage.sel_begin === 0) { + return 0; + } + + const begin = coverage.line_elt(coverage.sel_begin); + const end = coverage.line_elt(coverage.sel_end-1); + + return ( + (checkVisible(begin) ? 1 : 0) + + (checkVisible(end) ? 1 : 0) + ); +}; + +coverage.to_next_chunk_nicely = function () { + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: + // Set the top line on the screen as selection. + + // This will select the top-left of the viewport + // As this is most likely the span with the line number we take the parent + const line = document.elementFromPoint(0, 0).parentElement; + if (line.parentElement !== document.getElementById("source")) { + // The element is not a source line but the header or similar + coverage.select_line_or_chunk(1); + } + else { + // We extract the line number from the id + coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); + } + } + coverage.to_next_chunk(); +}; + +coverage.to_prev_chunk_nicely = function () { + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: + // Set the lowest line on the screen as selection. + + // This will select the bottom-left of the viewport + // As this is most likely the span with the line number we take the parent + const line = document.elementFromPoint(document.documentElement.clientHeight-1, 0).parentElement; + if (line.parentElement !== document.getElementById("source")) { + // The element is not a source line but the header or similar + coverage.select_line_or_chunk(coverage.lines_len); + } + else { + // We extract the line number from the id + coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); + } + } + coverage.to_prev_chunk(); +}; + +// Select line number lineno, or if it is in a colored chunk, select the +// entire chunk +coverage.select_line_or_chunk = function (lineno) { + var c = coverage; + var probe_line = c.line_elt(lineno); + if (!probe_line) { + return; + } + var the_indicator = c.chunk_indicator(probe_line); + if (the_indicator) { + // The line is in a highlighted chunk. + // Search backward for the first line. + var probe = lineno; + var indicator = the_indicator; + while (probe > 0 && indicator === the_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (!probe_line) { + break; + } + indicator = c.chunk_indicator(probe_line); + } + var begin = probe + 1; + + // Search forward for the last line. + probe = lineno; + indicator = the_indicator; + while (indicator === the_indicator) { + probe++; + probe_line = c.line_elt(probe); + indicator = c.chunk_indicator(probe_line); + } + + coverage.set_sel(begin, probe); + } + else { + coverage.set_sel(lineno); + } +}; + +coverage.show_selection = function () { + // Highlight the lines in the chunk + document.querySelectorAll("#source .highlight").forEach(e => e.classList.remove("highlight")); + for (let probe = coverage.sel_begin; probe < coverage.sel_end; probe++) { + coverage.line_elt(probe).querySelector(".n").classList.add("highlight"); + } + + coverage.scroll_to_selection(); +}; + +coverage.scroll_to_selection = function () { + // Scroll the page if the chunk isn't fully visible. + if (coverage.selection_ends_on_screen() < 2) { + const element = coverage.line_elt(coverage.sel_begin); + coverage.scroll_window(element.offsetTop - 60); + } +}; + +coverage.scroll_window = function (to_pos) { + window.scroll({top: to_pos, behavior: "smooth"}); +}; + +coverage.init_scroll_markers = function () { + // Init some variables + coverage.lines_len = document.querySelectorAll("#source > p").length; + + // Build html + coverage.build_scroll_markers(); +}; + +coverage.build_scroll_markers = function () { + const temp_scroll_marker = document.getElementById("scroll_marker") + if (temp_scroll_marker) temp_scroll_marker.remove(); + // Don't build markers if the window has no scroll bar. + if (document.body.scrollHeight <= window.innerHeight) { + return; + } + + const marker_scale = window.innerHeight / document.body.scrollHeight; + const line_height = Math.min(Math.max(3, window.innerHeight / coverage.lines_len), 10); + + let previous_line = -99, last_mark, last_top; + + const scroll_marker = document.createElement("div"); + scroll_marker.id = "scroll_marker"; + document.getElementById("source").querySelectorAll( + "p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par" + ).forEach(element => { + const line_top = Math.floor(element.offsetTop * marker_scale); + const line_number = parseInt(element.querySelector(".n a").id.substr(1)); + + if (line_number === previous_line + 1) { + // If this solid missed block just make previous mark higher. + last_mark.style.height = `${line_top + line_height - last_top}px`; + } + else { + // Add colored line in scroll_marker block. + last_mark = document.createElement("div"); + last_mark.id = `m${line_number}`; + last_mark.classList.add("marker"); + last_mark.style.height = `${line_height}px`; + last_mark.style.top = `${line_top}px`; + scroll_marker.append(last_mark); + last_top = line_top; + } + + previous_line = line_number; + }); + + // Append last to prevent layout calculation + document.body.append(scroll_marker); +}; + +coverage.wire_up_sticky_header = function () { + const header = document.querySelector("header"); + const header_bottom = ( + header.querySelector(".content h2").getBoundingClientRect().top - + header.getBoundingClientRect().top + ); + + function updateHeader() { + if (window.scrollY > header_bottom) { + header.classList.add("sticky"); + } + else { + header.classList.remove("sticky"); + } + } + + window.addEventListener("scroll", updateHeader); + updateHeader(); +}; + +coverage.expand_contexts = function (e) { + var ctxs = e.target.parentNode.querySelector(".ctxs"); + + if (!ctxs.classList.contains("expanded")) { + var ctxs_text = ctxs.textContent; + var width = Number(ctxs_text[0]); + ctxs.textContent = ""; + for (var i = 1; i < ctxs_text.length; i += width) { + key = ctxs_text.substring(i, i + width).trim(); + ctxs.appendChild(document.createTextNode(contexts[key])); + ctxs.appendChild(document.createElement("br")); + } + ctxs.classList.add("expanded"); + } +}; + +document.addEventListener("DOMContentLoaded", () => { + if (document.body.classList.contains("indexfile")) { + coverage.index_ready(); + } + else { + coverage.pyfile_ready(); + } +}); diff --git a/cov_html/favicon_32_cb_c827f16f.png b/cov_html/favicon_32_cb_c827f16f.png new file mode 100644 index 000000000..8649f0475 Binary files /dev/null and b/cov_html/favicon_32_cb_c827f16f.png differ diff --git a/cov_html/function_index.html b/cov_html/function_index.html new file mode 100644 index 000000000..e348d68bb --- /dev/null +++ b/cov_html/function_index.html @@ -0,0 +1,231 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 84% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.13.4, + created at 2026-04-07 14:49 +0200 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Filefunction statementsmissingexcluded coverage
server.pyindex 100 100%
server.pysign_up 100 100%
server.pyprofile 820 75%
server.pyprofile_post 3090 70%
server.pychange_password 2360 74%
server.pyshow_summary 520 60%
server.pyshow_summary_post 1530 80%
server.pybook 1520 87%
server.pypurchase_places 1200 100%
server.pypoints_board 300 100%
server.pylogout 400 100%
server.py(no function) 3400 100%
Total  151240 84%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/cov_html/index.html b/cov_html/index.html new file mode 100644 index 000000000..b363ea196 --- /dev/null +++ b/cov_html/index.html @@ -0,0 +1,117 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 84% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.13.4, + created at 2026-04-07 14:49 +0200 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
File statementsmissingexcluded coverage
server.py 151240 84%
Total 151240 84%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/cov_html/keybd_closed_cb_900cfef5.png b/cov_html/keybd_closed_cb_900cfef5.png new file mode 100644 index 000000000..ba119c47d Binary files /dev/null and b/cov_html/keybd_closed_cb_900cfef5.png differ diff --git a/cov_html/server_py.html b/cov_html/server_py.html new file mode 100644 index 000000000..f60239da3 --- /dev/null +++ b/cov_html/server_py.html @@ -0,0 +1,423 @@ + + + + + Coverage for server.py: 84% + + + + + +
+
+

+ Coverage for server.py: + 84% +

+ +

+ 151 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.4, + created at 2026-04-07 14:49 +0200 +

+ +
+
+
+

1import os 

+

2 

+

3from flask import Flask, render_template, request, redirect, flash, url_for, session 

+

4from werkzeug.security import generate_password_hash, check_password_hash 

+

5 

+

6 

+

7app = Flask(__name__) 

+

8app.secret_key = 'something_special' 

+

9 

+

10app.config["CLUBS_JSON"] = os.environ.get("CLUBS_JSON") 

+

11app.config["COMPETITIONS_JSON"] = os.environ.get("COMPETITIONS_JSON") 

+

12 

+

13CLUB_POINTS = 15 

+

14 

+

15import utils 

+

16import exceptions 

+

17 

+

18utils.clubs = utils.load_clubs() 

+

19utils.competitions = utils.load_competitions() 

+

20 

+

21 

+

22@app.route('/') 

+

23def index(): 

+

24 """ 

+

25 Route to home page 

+

26 Returns: 

+

27 The template for home page 

+

28 """ 

+

29 return render_template(template_name_or_list='index.html') 

+

30 

+

31 

+

32@app.route('/signUp') 

+

33def sign_up(): 

+

34 """ 

+

35 Route to sign up page 

+

36 Returns: 

+

37 The template for sign up page 

+

38 """ 

+

39 return render_template(template_name_or_list='sign_up.html') 

+

40 

+

41 

+

42@app.route('/profile/<club>', methods=['GET']) 

+

43def profile(club): 

+

44 """ 

+

45 Route to club profile page 

+

46 Args: 

+

47 club (str): The club name 

+

48 

+

49 Returns: 

+

50 The template for club profile page. The template may display a message to the user. 

+

51 """ 

+

52 if "club" in session and session['club'] == club: 

+

53 the_club = next((c for c in utils.clubs if c['name'] == club), None) 

+

54 

+

55 if the_club is None: 

+

56 flash(message="Sorry, that club was not found.") 

+

57 return render_template(template_name_or_list="index.html", error="Club not found"), 404 

+

58 

+

59 return render_template(template_name_or_list='profile.html', club=the_club) 

+

60 

+

61 flash(message="Sorry, you are not allow to see that profile.") 

+

62 return render_template(template_name_or_list='index.html', error="Not allow"), 403 

+

63 

+

64 

+

65@app.route('/profile', methods=['POST']) 

+

66def profile_post(): 

+

67 """ 

+

68 Route for signing up. 

+

69 Returns: 

+

70 The template to profile page if correctly signed up or sign up page otherwise. 

+

71 """ 

+

72 club_name = request.form['name'] 

+

73 club_email = request.form['email'] 

+

74 club_password = request.form['password'] 

+

75 club_password_confirmation = request.form['confirm_password'] 

+

76 

+

77 try: 

+

78 utils.validate_profile_fields(club_name, club_email, club_password, club_password_confirmation) 

+

79 

+

80 except exceptions.ValidationError as e: 

+

81 flash(message=e.message) 

+

82 return redirect(location=url_for('sign_up')) 

+

83 

+

84 try: 

+

85 utils.validate_email_format(club_email) 

+

86 

+

87 except exceptions.ValidationError as e: 

+

88 flash(message=e.message) 

+

89 return redirect(location=url_for('sign_up')) 

+

90 

+

91 found_clubs = [c for c in utils.clubs if c['email'] == club_email or c['name'] == club_name] 

+

92 

+

93 if not found_clubs: 

+

94 if club_password != club_password_confirmation: 

+

95 flash(message='Sorry, passwords do not match') 

+

96 return redirect(location=url_for('sign_up')) 

+

97 

+

98 hashed_password = generate_password_hash(club_password) 

+

99 utils.add_club(name=club_name, 

+

100 email=club_email, 

+

101 password=hashed_password, 

+

102 points=str(CLUB_POINTS)) 

+

103 

+

104 the_club = next((c for c in utils.clubs if c['email'] == club_email), None) 

+

105 

+

106 if the_club is None: 

+

107 flash(message="Sorry, something went wrong. Please try again.") 

+

108 return render_template(template_name_or_list='sign_up.html') 

+

109 

+

110 flash(message="Great! You have successfully signed up.") 

+

111 session['club'] = the_club['name'] 

+

112 return render_template(template_name_or_list='profile.html', club=the_club) 

+

113 

+

114 else: 

+

115 flash(message="Sorry, the club already exists.") 

+

116 return render_template(template_name_or_list='sign_up.html') 

+

117 

+

118 

+

119@app.route('/changePassword/<club>', methods=['GET', 'POST']) 

+

120def change_password(club): 

+

121 """ 

+

122 Route to change password. 

+

123 Args: 

+

124 club (str): The club name 

+

125 

+

126 Returns: 

+

127 The template for club profile page. The index template otherwise. 

+

128 """ 

+

129 if "club" in session and session['club'] == club: 

+

130 if request.method == 'GET': 

+

131 the_club = next((c for c in utils.clubs if c['name'] == club), None) 

+

132 

+

133 if the_club is None: 

+

134 flash(message="Sorry, that club was not found.") 

+

135 return render_template(template_name_or_list="index.html", error="Email not found"), 404 

+

136 

+

137 return render_template(template_name_or_list='change_password.html', club=the_club) 

+

138 else: 

+

139 club_password = request.form['password'] 

+

140 club_password_confirmation = request.form['confirm_password'] 

+

141 

+

142 the_club = next((c for c in utils.clubs if c['name'] == club), None) 

+

143 

+

144 try: 

+

145 utils.validate_password(password=club_password, 

+

146 password2=club_password_confirmation, 

+

147 club=the_club) 

+

148 except exceptions.ValidationError as e: 

+

149 flash(e.message) 

+

150 return render_template(template_name_or_list='change_password.html', 

+

151 club=the_club, 

+

152 error=e.tag), 200 

+

153 

+

154 the_club = utils.update_club_password(the_club, club_password) 

+

155 

+

156 if the_club: 

+

157 flash(message="Great! You have successfully changed your password.") 

+

158 return render_template(template_name_or_list='profile.html', club=the_club) 

+

159 

+

160 flash(message="Sorry, something went wrong. Please try again.") 

+

161 return render_template(template_name_or_list='index.html') 

+

162 

+

163 flash(message="Sorry, you are not allow to do this action.") 

+

164 return render_template(template_name_or_list='index.html', error="Not allow"), 403 

+

165 

+

166 

+

167@app.route('/showSummary/<club>', methods=['GET']) 

+

168def show_summary(club): 

+

169 """ 

+

170 Route to club summary page 

+

171 Args: 

+

172 club (str): The club name 

+

173 

+

174 Returns: 

+

175 The welcome template if authorized. The index template otherwise. 

+

176 """ 

+

177 if "club" in session and session['club'] == club: 

+

178 the_club = next((c for c in utils.clubs if c['name'] == club), None) 

+

179 

+

180 return render_template(template_name_or_list='welcome.html', 

+

181 club=the_club, 

+

182 competitions=utils.competitions) 

+

183 

+

184 flash(message="Sorry, you are not allow to do this action.") 

+

185 return render_template(template_name_or_list='index.html', error="Not allow"), 403 

+

186 

+

187 

+

188@app.route('/showSummary', methods=['POST']) 

+

189def show_summary_post(): 

+

190 """ 

+

191 Route to log in. 

+

192 Returns: 

+

193 The welcome template if success. The index template otherwise. 

+

194 """ 

+

195 try: 

+

196 utils.validate_login_fields(request.form['email'], request.form['password']) 

+

197 except exceptions.ValidationError as e: 

+

198 flash(message=e.message) 

+

199 return render_template(template_name_or_list="index.html", error=e.tag), 404 

+

200 

+

201 the_club = next((c for c in utils.clubs if c['email'] == request.form['email']), None) 

+

202 

+

203 if the_club is None: 

+

204 flash(message="Sorry, that email was not found.") 

+

205 return render_template(template_name_or_list="index.html", error="Email not found"), 404 

+

206 

+

207 if not check_password_hash(the_club['password'], request.form['password']): 

+

208 flash(message="Sorry, the password is incorrect.") 

+

209 return render_template(template_name_or_list="index.html", error="Incorrect password"), 403 

+

210 

+

211 session["club"] = the_club["name"] 

+

212 

+

213 flash("Great! You are successfully logged in.") 

+

214 return render_template(template_name_or_list='welcome.html', 

+

215 club=the_club, 

+

216 competitions=utils.competitions) 

+

217 

+

218 

+

219@app.route('/book/<competition>/<club>') 

+

220def book(competition, club): 

+

221 """ 

+

222 Route to book page 

+

223 Args: 

+

224 competition (str): The competition name 

+

225 club (str): The club name 

+

226 

+

227 Returns: 

+

228 The booking template if success. The welcome template if error. The index template otherwise. 

+

229 """ 

+

230 if "club" in session and session['club'] == club: 

+

231 found_club = [c for c in utils.clubs if c['name'] == club][0] 

+

232 found_competition = [c for c in utils.competitions if c['name'] == competition][0] 

+

233 

+

234 try: 

+

235 utils.validate_competition(the_competition=found_competition) 

+

236 

+

237 except exceptions.ValidationError as e: 

+

238 flash(message=e.message) 

+

239 

+

240 the_club = next((a_club for a_club in utils.clubs if a_club['name'] == club), None) 

+

241 

+

242 return render_template(template_name_or_list='welcome.html', 

+

243 club=the_club, 

+

244 competitions=utils.competitions, 

+

245 error=e.tag), 200 

+

246 

+

247 if found_club and found_competition: 

+

248 return render_template(template_name_or_list='booking.html', 

+

249 club=found_club, 

+

250 competition=found_competition), 200 

+

251 else: 

+

252 flash(message="Sorry, something went wrong. Please try again.") 

+

253 return render_template(template_name_or_list='welcome.html', 

+

254 club=club, 

+

255 competitions=utils.competitions) 

+

256 

+

257 flash(message="Sorry, you are not allow to do this action.") 

+

258 return render_template(template_name_or_list='index.html', error="Not allow"), 403 

+

259 

+

260 

+

261@app.route('/purchasePlaces', methods=['POST']) 

+

262def purchase_places(): 

+

263 """ 

+

264 Route to purchase places page 

+

265 Returns: 

+

266 The welcome template if success. The welcome template with error message otherwise. 

+

267 """ 

+

268 competition = [c for c in utils.competitions if c['name'] == request.form['competition']][0] 

+

269 club = [c for c in utils.clubs if c['name'] == request.form['club']][0] 

+

270 

+

271 places_required = int(request.form['places']) if request.form['places'] else 0 

+

272 

+

273 try: 

+

274 utils.validate_places(places_required=places_required, 

+

275 club=club, 

+

276 the_competition=competition) 

+

277 

+

278 except exceptions.ValidationError as e: 

+

279 flash(message=e.message) 

+

280 return render_template(template_name_or_list='welcome.html', 

+

281 club=club, 

+

282 competitions=utils.competitions, 

+

283 error=e.tag), 200 

+

284 

+

285 utils.update_club_booked_places(club=club, 

+

286 places=places_required, 

+

287 competition_name=competition["name"]) 

+

288 

+

289 utils.update_competition_available_places(the_competition=competition, places=places_required) 

+

290 

+

291 flash(message=f""" 

+

292 Great! Booking of {places_required} place(s) for {competition['name']} competition complete! 

+

293 """) 

+

294 

+

295 return render_template(template_name_or_list='welcome.html', 

+

296 club=club, 

+

297 competitions=utils.competitions) 

+

298 

+

299 

+

300@app.route('/pointsBoard') 

+

301def points_board(): 

+

302 """ 

+

303 Route to points board page 

+

304 Returns: 

+

305 The points board template. 

+

306 """ 

+

307 clubs_for_board = utils.copy_clubs_for_board() 

+

308 

+

309 club = session.get('club') 

+

310 

+

311 return render_template(template_name_or_list='points_board.html', 

+

312 clubs=clubs_for_board, 

+

313 club=club) 

+

314 

+

315 

+

316@app.route('/logout') 

+

317def logout(): 

+

318 """ 

+

319 Route for logging out 

+

320 Returns: 

+

321 The index template. 

+

322 """ 

+

323 flash(message="Great! You are successfully logged out.") 

+

324 if 'club' in session: 

+

325 session.pop('club') 

+

326 return redirect(location=url_for('index')) 

+
+ + + diff --git a/cov_html/status.json b/cov_html/status.json new file mode 100644 index 000000000..920e4e8bd --- /dev/null +++ b/cov_html/status.json @@ -0,0 +1 @@ +{"note":"This file is an internal implementation detail to speed up HTML report generation. Its format can change at any time. You might be looking for the JSON report: https://coverage.rtfd.io/cmd.html#cmd-json","format":5,"version":"7.13.4","globals":"dea14ccd98f0f9e164a14411b74eed10","files":{"server_py":{"hash":"bebe0f21eaf0050b1497427e05200b3f","index":{"url":"server_py.html","file":"server.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":151,"n_excluded":0,"n_missing":24,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}}}} \ No newline at end of file diff --git a/cov_html/style_cb_5c747636.css b/cov_html/style_cb_5c747636.css new file mode 100644 index 000000000..5e304ce5f --- /dev/null +++ b/cov_html/style_cb_5c747636.css @@ -0,0 +1,389 @@ +@charset "UTF-8"; +/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ +/* For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt */ +/* Don't edit this .css file. Edit the .scss file instead! */ +html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } + +body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { body { color: #eee; } } + +html > body { font-size: 16px; } + +a:active, a:focus { outline: 2px dashed #007acc; } + +p { font-size: .875em; line-height: 1.4em; } + +table { border-collapse: collapse; } + +td { vertical-align: top; } + +table tr.hidden { display: none !important; } + +p#no_rows { display: none; font-size: 1.15em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +a.nav { text-decoration: none; color: inherit; } + +a.nav:hover { text-decoration: underline; color: inherit; } + +.hidden { display: none; } + +header { background: #f8f8f8; width: 100%; z-index: 2; border-bottom: 1px solid #ccc; } + +@media (prefers-color-scheme: dark) { header { background: black; } } + +@media (prefers-color-scheme: dark) { header { border-color: #333; } } + +header .content { padding: 1rem 3.5rem; } + +header h2 { margin-top: .5em; font-size: 1em; } + +header h2 a.button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; background: #eee; color: inherit; text-decoration: none; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { header h2 a.button { background: #333; } } + +@media (prefers-color-scheme: dark) { header h2 a.button { border-color: #444; } } + +header h2 a.button.current { border: 2px solid; background: #fff; border-color: #999; cursor: default; } + +@media (prefers-color-scheme: dark) { header h2 a.button.current { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { header h2 a.button.current { border-color: #777; } } + +header p.text { margin: .5em 0 -.5em; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { header p.text { color: #aaa; } } + +header.sticky { position: fixed; left: 0; right: 0; height: 2.5em; } + +header.sticky .text { display: none; } + +header.sticky h1, header.sticky h2 { font-size: 1em; margin-top: 0; display: inline-block; } + +header.sticky .content { padding: 0.5rem 3.5rem; } + +header.sticky .content p { font-size: 1em; } + +header.sticky ~ #source { padding-top: 6.5em; } + +main { position: relative; z-index: 1; } + +footer { margin: 1rem 3.5rem; } + +footer .content { padding: 0; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { footer .content { color: #aaa; } } + +#index { margin: 1rem 0 0 3.5rem; } + +h1 { font-size: 1.25em; display: inline-block; } + +#filter_container { float: right; margin: 0 2em 0 0; line-height: 1.66em; } + +#filter_container #filter { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { #filter_container #filter { border-color: #444; } } + +@media (prefers-color-scheme: dark) { #filter_container #filter { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #filter_container #filter { color: #eee; } } + +#filter_container #filter:focus { border-color: #007acc; } + +#filter_container :disabled ~ label { color: #ccc; } + +@media (prefers-color-scheme: dark) { #filter_container :disabled ~ label { color: #444; } } + +#filter_container label { font-size: .875em; color: #666; } + +@media (prefers-color-scheme: dark) { #filter_container label { color: #aaa; } } + +header button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; background: #eee; color: inherit; text-decoration: none; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { header button { background: #333; } } + +@media (prefers-color-scheme: dark) { header button { border-color: #444; } } + +header button:active, header button:focus { outline: 2px dashed #007acc; } + +header button.run { background: #eeffee; } + +@media (prefers-color-scheme: dark) { header button.run { background: #373d29; } } + +header button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.run.show_run { background: #373d29; } } + +header button.mis { background: #ffeeee; } + +@media (prefers-color-scheme: dark) { header button.mis { background: #4b1818; } } + +header button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.mis.show_mis { background: #4b1818; } } + +header button.exc { background: #f7f7f7; } + +@media (prefers-color-scheme: dark) { header button.exc { background: #333; } } + +header button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.exc.show_exc { background: #333; } } + +header button.par { background: #ffffd5; } + +@media (prefers-color-scheme: dark) { header button.par { background: #650; } } + +header button.par.show_par { background: #ffa; border: 2px solid #bbbb00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.par.show_par { background: #650; } } + +#help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; } + +#source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; } + +#help_panel_wrapper { float: right; position: relative; } + +#keyboard_icon { margin: 5px; } + +#help_panel_state { display: none; } + +#help_panel { top: 25px; right: 0; padding: .75em; border: 1px solid #883; color: #333; } + +#help_panel .keyhelp p { margin-top: .75em; } + +#help_panel .legend { font-style: italic; margin-bottom: 1em; } + +.indexfile #help_panel { width: 25em; } + +.pyfile #help_panel { width: 18em; } + +#help_panel_state:checked ~ #help_panel { display: block; } + +kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; border-radius: 3px; } + +#source { padding: 1em 0 1em 3.5rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } + +#source p { position: relative; white-space: pre; } + +#source p * { box-sizing: border-box; } + +#source p .n { float: left; text-align: right; width: 3.5rem; box-sizing: border-box; margin-left: -3.5rem; padding-right: 1em; color: #999; user-select: none; } + +@media (prefers-color-scheme: dark) { #source p .n { color: #777; } } + +#source p .n.highlight { background: #ffdd00; } + +#source p .n a { scroll-margin-top: 6em; text-decoration: none; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } } + +#source p .n a:hover { text-decoration: underline; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } } + +#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; } + +@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } } + +#source p .t:hover { background: #f2f2f2; } + +@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } } + +#source p .t:hover ~ .r .annotate.long { display: block; } + +#source p .t .com { color: #008000; font-style: italic; line-height: 1px; } + +@media (prefers-color-scheme: dark) { #source p .t .com { color: #6a9955; } } + +#source p .t .key { font-weight: bold; line-height: 1px; } + +#source p .t .str, #source p .t .fst { color: #0451a5; } + +@media (prefers-color-scheme: dark) { #source p .t .str, #source p .t .fst { color: #9cdcfe; } } + +#source p.mis .t { border-left: 0.2em solid #ff0000; } + +#source p.mis.show_mis .t { background: #fdd; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } } + +#source p.mis.show_mis .t:hover { background: #f2d2d2; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } } + +#source p.mis.mis2 .t { border-left: 0.2em dotted #ff0000; } + +#source p.mis.mis2.show_mis .t { background: #ffeeee; } + +@media (prefers-color-scheme: dark) { #source p.mis.mis2.show_mis .t { background: #351b1b; } } + +#source p.mis.mis2.show_mis .t:hover { background: #f2d2d2; } + +@media (prefers-color-scheme: dark) { #source p.mis.mis2.show_mis .t:hover { background: #532323; } } + +#source p.run .t { border-left: 0.2em solid #00dd00; } + +#source p.run.show_run .t { background: #dfd; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } } + +#source p.run.show_run .t:hover { background: #d2f2d2; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } } + +#source p.run.run2 .t { border-left: 0.2em dotted #00dd00; } + +#source p.run.run2.show_run .t { background: #eeffee; } + +@media (prefers-color-scheme: dark) { #source p.run.run2.show_run .t { background: #2b2e24; } } + +#source p.run.run2.show_run .t:hover { background: #d2f2d2; } + +@media (prefers-color-scheme: dark) { #source p.run.run2.show_run .t:hover { background: #404633; } } + +#source p.exc .t { border-left: 0.2em solid #808080; } + +#source p.exc.show_exc .t { background: #eee; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } } + +#source p.exc.show_exc .t:hover { background: #e2e2e2; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } } + +#source p.exc.exc2 .t { border-left: 0.2em dotted #808080; } + +#source p.exc.exc2.show_exc .t { background: #f7f7f7; } + +@media (prefers-color-scheme: dark) { #source p.exc.exc2.show_exc .t { background: #292929; } } + +#source p.exc.exc2.show_exc .t:hover { background: #e2e2e2; } + +@media (prefers-color-scheme: dark) { #source p.exc.exc2.show_exc .t:hover { background: #3c3c3c; } } + +#source p.par .t { border-left: 0.2em solid #bbbb00; } + +#source p.par.show_par .t { background: #ffa; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } } + +#source p.par.show_par .t:hover { background: #f2f2a2; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } } + +#source p.par.par2 .t { border-left: 0.2em dotted #bbbb00; } + +#source p.par.par2.show_par .t { background: #ffffd5; } + +@media (prefers-color-scheme: dark) { #source p.par.par2.show_par .t { background: #423a0f; } } + +#source p.par.par2.show_par .t:hover { background: #f2f2a2; } + +@media (prefers-color-scheme: dark) { #source p.par.par2.show_par .t:hover { background: #6d5d0c; } } + +#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; } + +@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } } + +#source p .annotate.short:hover ~ .long { display: block; } + +#source p .annotate.long { width: 30em; right: 2.5em; } + +#source p input { display: none; } + +#source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; } + +#source p input ~ .r label.ctx::before { content: "▶ "; } + +#source p input ~ .r label.ctx:hover { background: #e8f4ff; color: #666; } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } } + +#source p input:checked ~ .r label.ctx { background: #d0e8ff; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } } + +#source p input:checked ~ .r label.ctx::before { content: "▼ "; } + +#source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; } + +#source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; } + +@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } + +#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; text-align: right; } + +@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } + +#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } + +#index table.index { margin-left: -.5em; } + +#index td, #index th { text-align: right; vertical-align: baseline; padding: .25em .5em; border-bottom: 1px solid #eee; } + +@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } } + +#index td.name, #index th.name { text-align: left; width: auto; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; min-width: 15em; } + +#index td.left, #index th.left { text-align: left; } + +#index td.spacer, #index th.spacer { border: none; padding: 0; } + +#index td.spacer:hover, #index th.spacer:hover { background: inherit; } + +#index th { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-style: italic; color: #333; border-color: #ccc; cursor: pointer; } + +@media (prefers-color-scheme: dark) { #index th { color: #ddd; } } + +@media (prefers-color-scheme: dark) { #index th { border-color: #444; } } + +#index th:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } } + +#index th .arrows { color: #666; font-size: 85%; font-family: sans-serif; font-style: normal; pointer-events: none; } + +#index th[aria-sort="ascending"], #index th[aria-sort="descending"] { white-space: nowrap; background: #eee; padding-left: .5em; } + +@media (prefers-color-scheme: dark) { #index th[aria-sort="ascending"], #index th[aria-sort="descending"] { background: #333; } } + +#index th[aria-sort="ascending"] .arrows::after { content: " ▲"; } + +#index th[aria-sort="descending"] .arrows::after { content: " ▼"; } + +#index tr.grouphead th { cursor: default; font-style: normal; border-color: #999; } + +@media (prefers-color-scheme: dark) { #index tr.grouphead th { border-color: #777; } } + +#index td.name { font-size: 1.15em; } + +#index td.name a { text-decoration: none; color: inherit; } + +#index td.name .no-noun { font-style: italic; } + +#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-bottom: none; } + +#index tr.region:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index tr.region:hover { background: #333; } } + +#index tr.region:hover td.name { text-decoration: underline; color: inherit; } + +#scroll_marker { position: fixed; z-index: 3; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; } + +@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } } + +#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; } + +@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } } diff --git a/docs/cov_report.png b/docs/cov_report.png new file mode 100644 index 000000000..b18d86454 Binary files /dev/null and b/docs/cov_report.png differ diff --git a/docs/cov_report_functions.png b/docs/cov_report_functions.png new file mode 100644 index 000000000..dba485b56 Binary files /dev/null and b/docs/cov_report_functions.png differ diff --git a/docs/flake8_report.png b/docs/flake8_report.png new file mode 100644 index 000000000..7051dc6ab Binary files /dev/null and b/docs/flake8_report.png differ diff --git a/docs/locust_report.png b/docs/locust_report.png new file mode 100644 index 000000000..a4cb80a9b Binary files /dev/null and b/docs/locust_report.png differ diff --git a/docs/screenshots/authentication_screenshot.png b/docs/screenshots/authentication_screenshot.png new file mode 100644 index 000000000..d81791386 Binary files /dev/null and b/docs/screenshots/authentication_screenshot.png differ diff --git a/docs/screenshots/booking_screenshot.png b/docs/screenshots/booking_screenshot.png new file mode 100644 index 000000000..04df9e44e Binary files /dev/null and b/docs/screenshots/booking_screenshot.png differ diff --git a/docs/screenshots/logout_screenshot.png b/docs/screenshots/logout_screenshot.png new file mode 100644 index 000000000..7d1735405 Binary files /dev/null and b/docs/screenshots/logout_screenshot.png differ diff --git a/docs/screenshots/password_screenshot.png b/docs/screenshots/password_screenshot.png new file mode 100644 index 000000000..e9719c8e3 Binary files /dev/null and b/docs/screenshots/password_screenshot.png differ diff --git a/docs/screenshots/points_board_screenshot.png b/docs/screenshots/points_board_screenshot.png new file mode 100644 index 000000000..e6e27390e Binary files /dev/null and b/docs/screenshots/points_board_screenshot.png differ diff --git a/docs/screenshots/profile_screenshot.png b/docs/screenshots/profile_screenshot.png new file mode 100644 index 000000000..3b50db692 Binary files /dev/null and b/docs/screenshots/profile_screenshot.png differ diff --git a/docs/screenshots/registration_screenshot.png b/docs/screenshots/registration_screenshot.png new file mode 100644 index 000000000..7d6e7a55c Binary files /dev/null and b/docs/screenshots/registration_screenshot.png differ diff --git a/docs/screenshots/welcome_page_screenshot.png b/docs/screenshots/welcome_page_screenshot.png new file mode 100644 index 000000000..ea7bd1ab0 Binary files /dev/null and b/docs/screenshots/welcome_page_screenshot.png differ diff --git a/docs/structure.png b/docs/structure.png new file mode 100644 index 000000000..6a5b2b1fe Binary files /dev/null and b/docs/structure.png differ diff --git a/exceptions.py b/exceptions.py new file mode 100644 index 000000000..90f5667c1 --- /dev/null +++ b/exceptions.py @@ -0,0 +1,85 @@ +class ValidationError(Exception): + def __init__(self, message, tag): + self.message = message + self.tag = tag + super().__init__(message) + + +class NegativePlacesError(ValidationError): + message = "Sorry, you should type a positive number." + tag = "Negative number" + + def __init__(self): + super().__init__(self.message, self.tag) + + +class Over12PlacesError(ValidationError): + message = "Sorry, you are not allow to purchase more than 12 places for this competition." + tag = "Over 12 places" + + def __init__(self): + super().__init__(self.message, self.tag) + + +class NotEnoughPlacesError(ValidationError): + message = "Sorry, there are not enough places available for this competition." + tag = "Not enough places" + + def __init__(self): + super().__init__(self.message, self.tag) + + +class NotEnoughPointsError(ValidationError): + message = "Sorry, you do not have enough points to purchase." + tag = "Not enough points" + + def __init__(self): + super().__init__(self.message, self.tag) + + +class OutdatedCompetitionError(ValidationError): + message = "Sorry, this competition is outdated. Booking not possible." + tag = "Outdated" + + def __init__(self): + super().__init__(self.message, self.tag) + + +class CompetitionNullPlacesError(ValidationError): + message = "Sorry, this competition is sold out. Booking not possible." + tag = "Sold out" + + def __init__(self): + super().__init__(self.message, self.tag) + + +class EmptyFieldError(ValidationError): + message = "Sorry, please fill all fields." + tag = "Empty field(s)" + + def __init__(self): + super().__init__(self.message, self.tag) + + +class PasswordsNotMatchError(ValidationError): + message = "Sorry, passwords do not match." + tag = "Passwords not match" + + def __init__(self): + super().__init__(self.message, self.tag) + + +class PasswordNotDifferentError(ValidationError): + message = "Sorry, you have to type a new different password." + tag = "Identical password" + + def __init__(self): + super().__init__(self.message, self.tag) + + +class MailAddressInvalidFormatError(ValidationError): + message = "Sorry, the e-mail address you entered has invalid format." + tag = "Invalid email format" + + def __init__(self): + super().__init__(self.message, self.tag) diff --git a/flake8-report/back.svg b/flake8-report/back.svg new file mode 100644 index 000000000..ce80d2e6d --- /dev/null +++ b/flake8-report/back.svg @@ -0,0 +1,73 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/flake8-report/file.svg b/flake8-report/file.svg new file mode 100644 index 000000000..98706cfe5 --- /dev/null +++ b/flake8-report/file.svg @@ -0,0 +1,64 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/flake8-report/index.html b/flake8-report/index.html new file mode 100644 index 000000000..f80b372c4 --- /dev/null +++ b/flake8-report/index.html @@ -0,0 +1,30 @@ + + + + flake8 violations + + + + +
+
+

flake8 violations

+

Generated on 2026-04-07 14:08 + with Installed plugins: flake8-html: 0.4.3, mccabe: 0.7.0, pycodestyle: 2.14.0, pyflakes: 3.4.0 +

+
    + +
  • +
    + + + +

    All good!

    +

    No flake8 errors found in 14 files scanned.

    +
    +
  • + +
+
+ + \ No newline at end of file diff --git a/flake8-report/styles.css b/flake8-report/styles.css new file mode 100644 index 000000000..6e0e447a6 --- /dev/null +++ b/flake8-report/styles.css @@ -0,0 +1,327 @@ +html { + font-family: sans-serif; + font-size: 90%; +} + +#masthead { + position: fixed; + left: 0; + top: 0; + right: 0; + height: 40%; +} + +h1, h2 { + font-family: sans-serif; + font-weight: normal; +} + +h1 { + color: white; + font-size: 36px; + margin-top: 1em; +} + +h1 img { + margin-right: 0.3em; +} + +h2 { + margin-top: 0; +} + +h1 a { + color: white; +} + +#versions { + color: rgba(255, 255, 255, 0.7); +} + +#page { + position: relative; + max-width: 960px; + margin: 0 auto; +} + +#index { + background-color: white; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.8); + padding: 0; + margin: 0; +} + +#index li { + list-style: none; + margin: 0; + padding: 1px 0; +} + +#index li + li { + border-top: solid silver 1px; +} + +.details p { + margin-left: 3em; + color: #888; +} + +#index a { + display: block; + padding: 0.8em 1em; + cursor: pointer; +} + +#index #all-good { + padding: 1.4em 1em 0.8em; +} + +#all-good .count .tick { + font-size: 2em; +} + +#all-good .count { + float: left; +} + +#all-good h2, +#all-good p { + margin-left: 50px; +} + +#index a:hover { + background-color: #eee; +} + +.count { + display: inline-block; + border-radius: 50%; + text-align: center; + width: 2.5em; + line-height: 2.5em; + height: 2.5em; + color: white; + margin-right: 1em; +} + +.sev-1 { + background-color: #a00; +} +.sev-2 { + background-color: #b80; +} +.sev-3 { + background-color: #28c; +} +.sev-4 { + background-color: #383; +} + +a { + text-decoration: none; +} + +#doc { + background-color: white; + margin: 1em 0; + padding: 1em; + padding-left: 1.2em; + position: relative; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.8); +} + +#doc pre { + margin: 0; + padding: 0.07em; +} + +.violations { + position: absolute; + margin: 1.2em 0 0 3em; + padding: 0.5em 1em; + font-size: 14px; + background-color: white; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.4); + display: none; +} + +.violations .count { + font-size: 70%; +} + +.violations li { + padding: 0.1em 0.3em; + list-style: none; +} + +.line-violations::before { + display: block; + content: ""; + position: absolute; + left: -1em; + width: 14px; + height: 14px; + border-radius: 50%; + background-color: red; +} + +.code:hover .violations { + display: block; +} + +tt { + white-space: pre-wrap; + font-family: Consolas, monospace; + font-size: 10pt; +} + +tt i { + color: silver; + display: inline-block; + text-align: right; + width: 3em; + box-sizing: border-box; + height: 100%; + border-right: solid #eee 1px; + padding-right: 0.2em; +} + +.le { + background-color: #ffe8e8; + cursor: pointer; +} + +.le:hover { + background-color: #fcc; +} + +.details { + clear: both; +} + +#index .details { + border-top-style: none; + margin: 1em; +} + +ul.details { + margin-left: 0; + padding-left: 0; +} + +#index .details li { + list-style: none; + border-top-style: none; + margin: 0.3em 0; + padding: 0; +} + +#srclink { + float: right; + font-size: 36px; + margin: 0; +} + +#srclink a { + color: white; +} + +#index .details a { + padding: 0; + color: inherit; +} + +.le { + background-color: #ffe8e8; + cursor: pointer; +} + +.le.sev-1 { + background-color: #f88; +} +.le.sev-2 { + background-color: #fda; +} +.le.sev-3 { + background-color: #adf; +} + +img { + height: 1.2em; + vertical-align: -0.35em; +} + +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.hll { background-color: #ffffcc } +.c { color: #3D7B7B; font-style: italic } /* Comment */ +.err { border: 1px solid #F00 } /* Error */ +.k { color: #008000; font-weight: bold } /* Keyword */ +.o { color: #666 } /* Operator */ +.ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ +.cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ +.cp { color: #9C6500 } /* Comment.Preproc */ +.cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ +.c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ +.cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ +.gd { color: #A00000 } /* Generic.Deleted */ +.ge { font-style: italic } /* Generic.Emph */ +.ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +.gr { color: #E40000 } /* Generic.Error */ +.gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.gi { color: #008400 } /* Generic.Inserted */ +.go { color: #717171 } /* Generic.Output */ +.gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.gs { font-weight: bold } /* Generic.Strong */ +.gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.gt { color: #04D } /* Generic.Traceback */ +.kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +.kp { color: #008000 } /* Keyword.Pseudo */ +.kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.kt { color: #B00040 } /* Keyword.Type */ +.m { color: #666 } /* Literal.Number */ +.s { color: #BA2121 } /* Literal.String */ +.na { color: #687822 } /* Name.Attribute */ +.nb { color: #008000 } /* Name.Builtin */ +.nc { color: #00F; font-weight: bold } /* Name.Class */ +.no { color: #800 } /* Name.Constant */ +.nd { color: #A2F } /* Name.Decorator */ +.ni { color: #717171; font-weight: bold } /* Name.Entity */ +.ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ +.nf { color: #00F } /* Name.Function */ +.nl { color: #767600 } /* Name.Label */ +.nn { color: #00F; font-weight: bold } /* Name.Namespace */ +.nt { color: #008000; font-weight: bold } /* Name.Tag */ +.nv { color: #19177C } /* Name.Variable */ +.ow { color: #A2F; font-weight: bold } /* Operator.Word */ +.w { color: #BBB } /* Text.Whitespace */ +.mb { color: #666 } /* Literal.Number.Bin */ +.mf { color: #666 } /* Literal.Number.Float */ +.mh { color: #666 } /* Literal.Number.Hex */ +.mi { color: #666 } /* Literal.Number.Integer */ +.mo { color: #666 } /* Literal.Number.Oct */ +.sa { color: #BA2121 } /* Literal.String.Affix */ +.sb { color: #BA2121 } /* Literal.String.Backtick */ +.sc { color: #BA2121 } /* Literal.String.Char */ +.dl { color: #BA2121 } /* Literal.String.Delimiter */ +.sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.s2 { color: #BA2121 } /* Literal.String.Double */ +.se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ +.sh { color: #BA2121 } /* Literal.String.Heredoc */ +.si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ +.sx { color: #008000 } /* Literal.String.Other */ +.sr { color: #A45A77 } /* Literal.String.Regex */ +.s1 { color: #BA2121 } /* Literal.String.Single */ +.ss { color: #19177C } /* Literal.String.Symbol */ +.bp { color: #008000 } /* Name.Builtin.Pseudo */ +.fm { color: #00F } /* Name.Function.Magic */ +.vc { color: #19177C } /* Name.Variable.Class */ +.vg { color: #19177C } /* Name.Variable.Global */ +.vi { color: #19177C } /* Name.Variable.Instance */ +.vm { color: #19177C } /* Name.Variable.Magic */ +.il { color: #666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 000000000..2ec008810 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1736 @@ +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. + +[[package]] +name = "attrs" +version = "25.4.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, + {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.7.0" +groups = ["main"] +files = [ + {file = "beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb"}, + {file = "beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86"}, +] + +[package.dependencies] +soupsieve = ">=1.6.1" +typing-extensions = ">=4.0.0" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "bidict" +version = "0.23.1" +description = "The bidirectional mapping library for Python." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5"}, + {file = "bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71"}, +] + +[[package]] +name = "blinker" +version = "1.9.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + +[[package]] +name = "brotli" +version = "1.2.0" +description = "Python bindings for the Brotli compression library" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "brotli-1.2.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:99cfa69813d79492f0e5d52a20fd18395bc82e671d5d40bd5a91d13e75e468e8"}, + {file = "brotli-1.2.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3ebe801e0f4e56d17cd386ca6600573e3706ce1845376307f5d2cbd32149b69a"}, + {file = "brotli-1.2.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:a387225a67f619bf16bd504c37655930f910eb03675730fc2ad69d3d8b5e7e92"}, + {file = "brotli-1.2.0-cp27-cp27m-win32.whl", hash = "sha256:b908d1a7b28bc72dfb743be0d4d3f8931f8309f810af66c906ae6cd4127c93cb"}, + {file = "brotli-1.2.0-cp27-cp27m-win_amd64.whl", hash = "sha256:d206a36b4140fbb5373bf1eb73fb9de589bb06afd0d22376de23c5e91d0ab35f"}, + {file = "brotli-1.2.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7e9053f5fb4e0dfab89243079b3e217f2aea4085e4d58c5c06115fc34823707f"}, + {file = "brotli-1.2.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4735a10f738cb5516905a121f32b24ce196ab82cfc1e4ba2e3ad1b371085fd46"}, + {file = "brotli-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b90b767916ac44e93a8e28ce6adf8d551e43affb512f2377c732d486ac6514e"}, + {file = "brotli-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6be67c19e0b0c56365c6a76e393b932fb0e78b3b56b711d180dd7013cb1fd984"}, + {file = "brotli-1.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bbd5b5ccd157ae7913750476d48099aaf507a79841c0d04a9db4415b14842de"}, + {file = "brotli-1.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f3c908bcc404c90c77d5a073e55271a0a498f4e0756e48127c35d91cf155947"}, + {file = "brotli-1.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b557b29782a643420e08d75aea889462a4a8796e9a6cf5621ab05a3f7da8ef2"}, + {file = "brotli-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81da1b229b1889f25adadc929aeb9dbc4e922bd18561b65b08dd9343cfccca84"}, + {file = "brotli-1.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d"}, + {file = "brotli-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a1778532b978d2536e79c05dac2d8cd857f6c55cd0c95ace5b03740824e0e2f1"}, + {file = "brotli-1.2.0-cp310-cp310-win32.whl", hash = "sha256:b232029d100d393ae3c603c8ffd7e3fe6f798c5e28ddca5feabb8e8fdb732997"}, + {file = "brotli-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef87b8ab2704da227e83a246356a2b179ef826f550f794b2c52cddb4efbd0196"}, + {file = "brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744"}, + {file = "brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f"}, + {file = "brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd"}, + {file = "brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe"}, + {file = "brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a"}, + {file = "brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b"}, + {file = "brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3"}, + {file = "brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae"}, + {file = "brotli-1.2.0-cp311-cp311-win32.whl", hash = "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03"}, + {file = "brotli-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24"}, + {file = "brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84"}, + {file = "brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b"}, + {file = "brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d"}, + {file = "brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca"}, + {file = "brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f"}, + {file = "brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28"}, + {file = "brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7"}, + {file = "brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036"}, + {file = "brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161"}, + {file = "brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44"}, + {file = "brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab"}, + {file = "brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c"}, + {file = "brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f"}, + {file = "brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6"}, + {file = "brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c"}, + {file = "brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48"}, + {file = "brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18"}, + {file = "brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5"}, + {file = "brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a"}, + {file = "brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8"}, + {file = "brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21"}, + {file = "brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac"}, + {file = "brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e"}, + {file = "brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7"}, + {file = "brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63"}, + {file = "brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b"}, + {file = "brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361"}, + {file = "brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888"}, + {file = "brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d"}, + {file = "brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3"}, + {file = "brotli-1.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:82676c2781ecf0ab23833796062786db04648b7aae8be139f6b8065e5e7b1518"}, + {file = "brotli-1.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c16ab1ef7bb55651f5836e8e62db1f711d55b82ea08c3b8083ff037157171a69"}, + {file = "brotli-1.2.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e85190da223337a6b7431d92c799fca3e2982abd44e7b8dec69938dcc81c8e9e"}, + {file = "brotli-1.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d8c05b1dfb61af28ef37624385b0029df902ca896a639881f594060b30ffc9a7"}, + {file = "brotli-1.2.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:465a0d012b3d3e4f1d6146ea019b5c11e3e87f03d1676da1cc3833462e672fb0"}, + {file = "brotli-1.2.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:96fbe82a58cdb2f872fa5d87dedc8477a12993626c446de794ea025bbda625ea"}, + {file = "brotli-1.2.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:1b71754d5b6eda54d16fbbed7fce2d8bc6c052a1b91a35c320247946ee103502"}, + {file = "brotli-1.2.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:66c02c187ad250513c2f4fce973ef402d22f80e0adce734ee4e4efd657b6cb64"}, + {file = "brotli-1.2.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:ba76177fd318ab7b3b9bf6522be5e84c2ae798754b6cc028665490f6e66b5533"}, + {file = "brotli-1.2.0-cp36-cp36m-win32.whl", hash = "sha256:c1702888c9f3383cc2f09eb3e88b8babf5965a54afb79649458ec7c3c7a63e96"}, + {file = "brotli-1.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f8d635cafbbb0c61327f942df2e3f474dde1cff16c3cd0580564774eaba1ee13"}, + {file = "brotli-1.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e80a28f2b150774844c8b454dd288be90d76ba6109670fe33d7ff54d96eb5cb8"}, + {file = "brotli-1.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b1b799f45da91292ffaa21a473ab3a3054fa78560e8ff67082a185274431c8"}, + {file = "brotli-1.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29b7e6716ee4ea0c59e3b241f682204105f7da084d6254ec61886508efeb43bc"}, + {file = "brotli-1.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:640fe199048f24c474ec6f3eae67c48d286de12911110437a36a87d7c89573a6"}, + {file = "brotli-1.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:92edab1e2fd6cd5ca605f57d4545b6599ced5dea0fd90b2bcdf8b247a12bd190"}, + {file = "brotli-1.2.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7274942e69b17f9cef76691bcf38f2b2d4c8a5f5dba6ec10958363dcb3308a0a"}, + {file = "brotli-1.2.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:a56ef534b66a749759ebd091c19c03ef81eb8cd96f0d1d16b59127eaf1b97a12"}, + {file = "brotli-1.2.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5732eff8973dd995549a18ecbd8acd692ac611c5c0bb3f59fa3541ae27b33be3"}, + {file = "brotli-1.2.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:598e88c736f63a0efec8363f9eb34e5b5536b7b6b1821e401afcb501d881f59a"}, + {file = "brotli-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:7ad8cec81f34edf44a1c6a7edf28e7b7806dfb8886e371d95dcf789ccd4e4982"}, + {file = "brotli-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:865cedc7c7c303df5fad14a57bc5db1d4f4f9b2b4d0a7523ddd206f00c121a16"}, + {file = "brotli-1.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ac27a70bda257ae3f380ec8310b0a06680236bea547756c277b5dfe55a2452a8"}, + {file = "brotli-1.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e813da3d2d865e9793ef681d3a6b66fa4b7c19244a45b817d0cceda67e615990"}, + {file = "brotli-1.2.0-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fe11467c42c133f38d42289d0861b6b4f9da31e8087ca2c0d7ebb4543625526"}, + {file = "brotli-1.2.0-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c0d6770111d1879881432f81c369de5cde6e9467be7c682a983747ec800544e2"}, + {file = "brotli-1.2.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:eda5a6d042c698e28bda2507a89b16555b9aa954ef1d750e1c20473481aff675"}, + {file = "brotli-1.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3173e1e57cebb6d1de186e46b5680afbd82fd4301d7b2465beebe83ed317066d"}, + {file = "brotli-1.2.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:71a66c1c9be66595d628467401d5976158c97888c2c9379c034e1e2312c5b4f5"}, + {file = "brotli-1.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:1e68cdf321ad05797ee41d1d09169e09d40fdf51a725bb148bff892ce04583d7"}, + {file = "brotli-1.2.0-cp38-cp38-win32.whl", hash = "sha256:f16dace5e4d3596eaeb8af334b4d2c820d34b8278da633ce4a00020b2eac981c"}, + {file = "brotli-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:14ef29fc5f310d34fc7696426071067462c9292ed98b5ff5a27ac70a200e5470"}, + {file = "brotli-1.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8d4f47f284bdd28629481c97b5f29ad67544fa258d9091a6ed1fda47c7347cd1"}, + {file = "brotli-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2881416badd2a88a7a14d981c103a52a23a276a553a8aacc1346c2ff47c8dc17"}, + {file = "brotli-1.2.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d39b54b968f4b49b5e845758e202b1035f948b0561ff5e6385e855c96625971"}, + {file = "brotli-1.2.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95db242754c21a88a79e01504912e537808504465974ebb92931cfca2510469e"}, + {file = "brotli-1.2.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bba6e7e6cfe1e6cb6eb0b7c2736a6059461de1fa2c0ad26cf845de6c078d16c8"}, + {file = "brotli-1.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:88ef7d55b7bcf3331572634c3fd0ed327d237ceb9be6066810d39020a3ebac7a"}, + {file = "brotli-1.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:7fa18d65a213abcfbb2f6cafbb4c58863a8bd6f2103d65203c520ac117d1944b"}, + {file = "brotli-1.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:09ac247501d1909e9ee47d309be760c89c990defbb2e0240845c892ea5ff0de4"}, + {file = "brotli-1.2.0-cp39-cp39-win32.whl", hash = "sha256:c25332657dee6052ca470626f18349fc1fe8855a56218e19bd7a8c6ad4952c49"}, + {file = "brotli-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:1ce223652fd4ed3eb2b7f78fbea31c52314baecfac68db44037bb4167062a937"}, + {file = "brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a"}, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "os_name == \"nt\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" or implementation_name == \"pypy\"" +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "charset-normalizer" +version = "3.4.6" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-win32.whl", hash = "sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-win_amd64.whl", hash = "sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win32.whl", hash = "sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win_amd64.whl", hash = "sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win_arm64.whl", hash = "sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8"}, + {file = "charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69"}, + {file = "charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6"}, +] + +[[package]] +name = "click" +version = "8.3.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "configargparse" +version = "1.7.5" +description = "A drop-in replacement for argparse that allows options to also be set via config files and/or environment variables." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "configargparse-1.7.5-py3-none-any.whl", hash = "sha256:1e63fdffedf94da9cd435fc13a1cd24777e76879dd2343912c1f871d4ac8c592"}, + {file = "configargparse-1.7.5.tar.gz", hash = "sha256:e3f9a7bb6be34d66b2e3c4a2f58e3045f8dfae47b0dc039f87bcfaa0f193fb0f"}, +] + +[package.extras] +test = ["PyYAML", "black", "mock", "pytest", "pytest-cov", "pytest-subtests", "toml"] +yaml = ["PyYAML"] + +[[package]] +name = "flake8" +version = "7.3.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, + {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.14.0,<2.15.0" +pyflakes = ">=3.4.0,<3.5.0" + +[[package]] +name = "flake8-html" +version = "0.4.3" +description = "Generate HTML reports of flake8 violations" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "flake8-html-0.4.3.tar.gz", hash = "sha256:8b870299620cc4a06f73644a1b4d457799abeca1cc914c62ae71ec5bf65c79a5"}, + {file = "flake8_html-0.4.3-py2.py3-none-any.whl", hash = "sha256:8f126748b1b0edd6cd39e87c6192df56e2f8655b0aa2bb00ffeac8cf27be4325"}, +] + +[package.dependencies] +flake8 = ">=3.3.0" +jinja2 = ">=3.1.0" +pygments = ">=2.2.0" + +[[package]] +name = "flask" +version = "3.1.3" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c"}, + {file = "flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb"}, +] + +[package.dependencies] +blinker = ">=1.9.0" +click = ">=8.1.3" +itsdangerous = ">=2.2.0" +jinja2 = ">=3.1.2" +markupsafe = ">=2.1.1" +werkzeug = ">=3.1.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "flask-cors" +version = "5.0.0" +description = "A Flask extension adding a decorator for CORS support" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "Flask_Cors-5.0.0-py2.py3-none-any.whl", hash = "sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc"}, + {file = "flask_cors-5.0.0.tar.gz", hash = "sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef"}, +] + +[package.dependencies] +Flask = ">=0.9" + +[[package]] +name = "flask-login" +version = "0.6.3" +description = "User authentication and session management for Flask." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "Flask-Login-0.6.3.tar.gz", hash = "sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333"}, + {file = "Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d"}, +] + +[package.dependencies] +Flask = ">=1.0.4" +Werkzeug = ">=1.0.1" + +[[package]] +name = "flask-testing" +version = "0.8.1" +description = "Unit testing for Flask" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "Flask-Testing-0.8.1.tar.gz", hash = "sha256:0a734d7b68e63a9410b413cd7b1f96456f9a858bd09a6222d465650cc782eb01"}, +] + +[package.dependencies] +Flask = "*" + +[[package]] +name = "gevent" +version = "25.9.1" +description = "Coroutine-based network library" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "gevent-25.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:856b990be5590e44c3a3dc6c8d48a40eaccbb42e99d2b791d11d1e7711a4297e"}, + {file = "gevent-25.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fe1599d0b30e6093eb3213551751b24feeb43db79f07e89d98dd2f3330c9063e"}, + {file = "gevent-25.9.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:f0d8b64057b4bf1529b9ef9bd2259495747fba93d1f836c77bfeaacfec373fd0"}, + {file = "gevent-25.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b56cbc820e3136ba52cd690bdf77e47a4c239964d5f80dc657c1068e0fe9521c"}, + {file = "gevent-25.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c5fa9ce5122c085983e33e0dc058f81f5264cebe746de5c401654ab96dddfca8"}, + {file = "gevent-25.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:03c74fec58eda4b4edc043311fca8ba4f8744ad1632eb0a41d5ec25413581975"}, + {file = "gevent-25.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a8ae9f895e8651d10b0a8328a61c9c53da11ea51b666388aa99b0ce90f9fdc27"}, + {file = "gevent-25.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5aff9e8342dc954adb9c9c524db56c2f3557999463445ba3d9cbe3dada7b7"}, + {file = "gevent-25.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1cdf6db28f050ee103441caa8b0448ace545364f775059d5e2de089da975c457"}, + {file = "gevent-25.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:812debe235a8295be3b2a63b136c2474241fa5c58af55e6a0f8cfc29d4936235"}, + {file = "gevent-25.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b28b61ff9216a3d73fe8f35669eefcafa957f143ac534faf77e8a19eb9e6883a"}, + {file = "gevent-25.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5e4b6278b37373306fc6b1e5f0f1cf56339a1377f67c35972775143d8d7776ff"}, + {file = "gevent-25.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d99f0cb2ce43c2e8305bf75bee61a8bde06619d21b9d0316ea190fc7a0620a56"}, + {file = "gevent-25.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:72152517ecf548e2f838c61b4be76637d99279dbaa7e01b3924df040aa996586"}, + {file = "gevent-25.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:46b188248c84ffdec18a686fcac5dbb32365d76912e14fda350db5dc0bfd4f86"}, + {file = "gevent-25.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f2b54ea3ca6f0c763281cd3f96010ac7e98c2e267feb1221b5a26e2ca0b9a692"}, + {file = "gevent-25.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7a834804ac00ed8a92a69d3826342c677be651b1c3cd66cc35df8bc711057aa2"}, + {file = "gevent-25.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:323a27192ec4da6b22a9e51c3d9d896ff20bc53fdc9e45e56eaab76d1c39dd74"}, + {file = "gevent-25.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6ea78b39a2c51d47ff0f130f4c755a9a4bbb2dd9721149420ad4712743911a51"}, + {file = "gevent-25.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dc45cd3e1cc07514a419960af932a62eb8515552ed004e56755e4bf20bad30c5"}, + {file = "gevent-25.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34e01e50c71eaf67e92c186ee0196a039d6e4f4b35670396baed4a2d8f1b347f"}, + {file = "gevent-25.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acd6bcd5feabf22c7c5174bd3b9535ee9f088d2bbce789f740ad8d6554b18f3"}, + {file = "gevent-25.9.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:4f84591d13845ee31c13f44bdf6bd6c3dbf385b5af98b2f25ec328213775f2ed"}, + {file = "gevent-25.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9cdbb24c276a2d0110ad5c978e49daf620b153719ac8a548ce1250a7eb1b9245"}, + {file = "gevent-25.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:88b6c07169468af631dcf0fdd3658f9246d6822cc51461d43f7c44f28b0abb82"}, + {file = "gevent-25.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b7bb0e29a7b3e6ca9bed2394aa820244069982c36dc30b70eb1004dd67851a48"}, + {file = "gevent-25.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2951bb070c0ee37b632ac9134e4fdaad70d2e660c931bb792983a0837fe5b7d7"}, + {file = "gevent-25.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4e17c2d57e9a42e25f2a73d297b22b60b2470a74be5a515b36c984e1a246d47"}, + {file = "gevent-25.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d94936f8f8b23d9de2251798fcb603b84f083fdf0d7f427183c1828fb64f117"}, + {file = "gevent-25.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb51c5f9537b07da673258b4832f6635014fee31690c3f0944d34741b69f92fa"}, + {file = "gevent-25.9.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1a3fe4ea1c312dbf6b375b416925036fe79a40054e6bf6248ee46526ea628be1"}, + {file = "gevent-25.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0adb937f13e5fb90cca2edf66d8d7e99d62a299687400ce2edee3f3504009356"}, + {file = "gevent-25.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:427f869a2050a4202d93cf7fd6ab5cffb06d3e9113c10c967b6e2a0d45237cb8"}, + {file = "gevent-25.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c049880175e8c93124188f9d926af0a62826a3b81aa6d3074928345f8238279e"}, + {file = "gevent-25.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5a67a0974ad9f24721034d1e008856111e0535f1541499f72a733a73d658d1c"}, + {file = "gevent-25.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d0f5d8d73f97e24ea8d24d8be0f51e0cf7c54b8021c1fddb580bf239474690f"}, + {file = "gevent-25.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddd3ff26e5c4240d3fbf5516c2d9d5f2a998ef87cfb73e1429cfaeaaec860fa6"}, + {file = "gevent-25.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:bb63c0d6cb9950cc94036a4995b9cc4667b8915366613449236970f4394f94d7"}, + {file = "gevent-25.9.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f18f80aef6b1f6907219affe15b36677904f7cfeed1f6a6bc198616e507ae2d7"}, + {file = "gevent-25.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b274a53e818124a281540ebb4e7a2c524778f745b7a99b01bdecf0ca3ac0ddb0"}, + {file = "gevent-25.9.1-cp39-cp39-win32.whl", hash = "sha256:c6c91f7e33c7f01237755884316110ee7ea076f5bdb9aa0982b6dc63243c0a38"}, + {file = "gevent-25.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:012a44b0121f3d7c800740ff80351c897e85e76a7e4764690f35c5ad9ec17de5"}, + {file = "gevent-25.9.1.tar.gz", hash = "sha256:adf9cd552de44a4e6754c51ff2e78d9193b7fa6eab123db9578a210e657235dd"}, +] + +[package.dependencies] +cffi = {version = ">=1.17.1", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} +greenlet = {version = ">=3.2.2", markers = "platform_python_implementation == \"CPython\""} +"zope.event" = "*" +"zope.interface" = "*" + +[package.extras] +dnspython = ["dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\""] +docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"] +monitor = ["psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""] +recommended = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""] +test = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "coverage (>=5.0) ; sys_platform != \"win32\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "objgraph", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\"", "requests"] + +[[package]] +name = "geventhttpclient" +version = "2.3.9" +description = "HTTP client library for gevent" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "geventhttpclient-2.3.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25c03073a1136c2b93189488bb1bfc0868d90aa106dd49f15ac964d2454296c6"}, + {file = "geventhttpclient-2.3.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1e711eb91085585f61445c7313e1a0acb159b5dc11327930e673b4899ebd84f"}, + {file = "geventhttpclient-2.3.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:829454480d001f43bce4a8373bfe282a418b09817c32ce9b369ce637ae5240ab"}, + {file = "geventhttpclient-2.3.9-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f7cf60062d3aebd5e83f4d197a59609194effe25a25bcab01ae3775be18c877e"}, + {file = "geventhttpclient-2.3.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b9587beaccac950619f1defe0e1b9499a275edf8d912095f041060c62cb1aa3"}, + {file = "geventhttpclient-2.3.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c71796fda35bfe5b4ae93cdca62fd4932ee95c2b36812ce65878183ca7da517"}, + {file = "geventhttpclient-2.3.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f74053954f4599afb48b2c7765532c7e0cb5b0f1d0a62da8342ac4b5aadb76f9"}, + {file = "geventhttpclient-2.3.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:09b82815247a1044c648bac1cee1e766e03e762950cae49cf61efffaeff667c4"}, + {file = "geventhttpclient-2.3.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ecd2e843d1649cb5fba678240bb9778f6229b7315faa07a3696ccabcf289f609"}, + {file = "geventhttpclient-2.3.9-cp310-cp310-win32.whl", hash = "sha256:1d0c2af2aff5b802cdec4b6b216348a32a2452f4e5f5f2e19fc5f84d77443649"}, + {file = "geventhttpclient-2.3.9-cp310-cp310-win_amd64.whl", hash = "sha256:d980c54f98bc623e10f94595de633690bbf690b915e6ef2298df6728b31f0285"}, + {file = "geventhttpclient-2.3.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cf18417cabb210be64d1b610ced94387f4222fa4e0942486d5d5a6237d2dd9fa"}, + {file = "geventhttpclient-2.3.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3fc084a475eca84257b1f77dd584678c7e4bdc625f66b0279f2cfa54901a5ef8"}, + {file = "geventhttpclient-2.3.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c840b05ec56d16783f24926de25ce38d3453673ce4786896c63febd2fb34a6cf"}, + {file = "geventhttpclient-2.3.9-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4f35a5adbb0770824e98372dcec6805180c3ee99287e52598a4fb3b5d1a2b8aa"}, + {file = "geventhttpclient-2.3.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0658266fa594931e5260f17c6f52f867597e5cb257e85f73990b2f61bad58ec7"}, + {file = "geventhttpclient-2.3.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83da9a0cab4c990ac48316bed696aa1ffc0e678cbca725c3e904b84ee9c5d3e1"}, + {file = "geventhttpclient-2.3.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e3d120c2dbaf931fb1690ede4b7022bcaad82fa181e288b04d2f8a5e2d3d7eab"}, + {file = "geventhttpclient-2.3.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1dac4df42a954e19d3e737c4c4351332cf27e415c0e7b8850070fd8056237a04"}, + {file = "geventhttpclient-2.3.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:269e861e7fc38994b315b50469c8e629e3a78321a049598c4f4a0f21053e5503"}, + {file = "geventhttpclient-2.3.9-cp311-cp311-win32.whl", hash = "sha256:e8b30889ee4d5629904321da2a068ffb3a6114c7bcd46416051e869911b20a90"}, + {file = "geventhttpclient-2.3.9-cp311-cp311-win_amd64.whl", hash = "sha256:224e4a959ece6673f4c57113013fc20ed020e661d6de3c820aa3afe2f1cf2e99"}, + {file = "geventhttpclient-2.3.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39afb8046fe04358a85956555aa6a1d931710bac386a2fceb5c24bbd4d7c10e7"}, + {file = "geventhttpclient-2.3.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:04b8feec69fd662eb46b4f81013206f5a23d179b195cbaf590d4a59f641ed0fc"}, + {file = "geventhttpclient-2.3.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5b542025a0c9905c847d1459e598ccbdcd21dc0dd050cc1d3813ce7e01bd350f"}, + {file = "geventhttpclient-2.3.9-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c582a6697c82a948d3d42094da941544606a0ebee31fc0aa6731e248eeba0e9b"}, + {file = "geventhttpclient-2.3.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39011c8cdd7ef8b6ab07592525f83018cd1504e8133cce5114bfcee5547b9bb5"}, + {file = "geventhttpclient-2.3.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b91fb31523725ddc777c14b444ccedaf2043dcb9af0ede29056a9b8146c79a7"}, + {file = "geventhttpclient-2.3.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d91139a4fafd77fa985535966d7a6c2e64753f340ab1395508ee83cd8de70c38"}, + {file = "geventhttpclient-2.3.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0ff40ca5b848f96c6390bd8cc3a4c4598c119be08125cf1c30103201adc00940"}, + {file = "geventhttpclient-2.3.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9091db18eeb53626a81e9280d602ae9e29706ee4c1e7a05edc8b07cc632b3fc"}, + {file = "geventhttpclient-2.3.9-cp312-cp312-win32.whl", hash = "sha256:4110273531fc9ac2ec197a44a90d9c7b4266b51a070747368e38213be281d5c2"}, + {file = "geventhttpclient-2.3.9-cp312-cp312-win_amd64.whl", hash = "sha256:98f3582a1c9effb56bc2db4f43d382cedd921217a139d5737eeaad3a1e307047"}, + {file = "geventhttpclient-2.3.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9d0568d38cf74cecd37fd1ef65459f60ecd26dbc0d33bc2a1e0d8df4af24f07d"}, + {file = "geventhttpclient-2.3.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:02e06a2f78a225b70e616b493317073f3e2fddd4e51ddfc44569d188f368bd8d"}, + {file = "geventhttpclient-2.3.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eec8e442214d4086e40a3ae7fe1e1e3ecbc422157d8d2118059cf9977336d9f"}, + {file = "geventhttpclient-2.3.9-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a18b28d2f8bc7fcfc721227733bccb647602399db6b0fd093c00ff9699717b74"}, + {file = "geventhttpclient-2.3.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b16e30dbbc528453a4130210d83638444229357c073eb911421eb44e3367359"}, + {file = "geventhttpclient-2.3.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06df5597edf65d4c691052fce3e37620cbc037879a3b872bc16a7b2a0941d59a"}, + {file = "geventhttpclient-2.3.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:47a303bcac3d69569f025d0c81781c5f0c1a48c9f225e43082d1b56e4c0440f8"}, + {file = "geventhttpclient-2.3.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e73b25415e83064f5a334e83495d97b138e66f67a98cfcad154068c257733973"}, + {file = "geventhttpclient-2.3.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:98ff3350d8be75586076140bde565c35ccdd72a6840b88f94037ec6595407383"}, + {file = "geventhttpclient-2.3.9-cp313-cp313-win32.whl", hash = "sha256:af7931f55522cddedf84e837769c66d9ceb130b29182ad1e2d0201f501df899f"}, + {file = "geventhttpclient-2.3.9-cp313-cp313-win_amd64.whl", hash = "sha256:14daf2f0361f19b0221f900d7e9d563c184bb7186676e61fe848495b1f2483d3"}, + {file = "geventhttpclient-2.3.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c06e243de53f54942b098f81622917f4a33c16f44733c9371ea98a2cd5ce12e"}, + {file = "geventhttpclient-2.3.9-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:549155d557de403612336ca36cd93a049e67acbf9a29e6b6b971d0f4cb56786d"}, + {file = "geventhttpclient-2.3.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:31b463324d5fde983657247b2faea77f8f8a40f3f7ac0c2897a2fe3afa27d610"}, + {file = "geventhttpclient-2.3.9-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e1ac3a39e3c4ae36024ddf1694eb82b0cc22c4516f176477f94f98bcd56ce6cf"}, + {file = "geventhttpclient-2.3.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3d24480c3a2cc88311c41a042bc12ab8e4104dad6029591ecbf5a1e933e8a44"}, + {file = "geventhttpclient-2.3.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b244adcbf5814a29d5cea8b2fc079f9242d92765191faa4dc5eccc0421840ae"}, + {file = "geventhttpclient-2.3.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:83dc6f037a50b7d2dc45af58a7e7978016a06320a5f823d1bd544c85d69f2058"}, + {file = "geventhttpclient-2.3.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:caf8779ca686497e0fab1048b026b4e48fb14fb9e88ddbfd14ca1a1a4c4bfa89"}, + {file = "geventhttpclient-2.3.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cd4efebba798c7f585aa1ceb9aba9524b12ebc51b26ad62de5234b8264d9b94d"}, + {file = "geventhttpclient-2.3.9-cp314-cp314-win32.whl", hash = "sha256:7b60c0b650c77d2644374149c38dfee34510e88e569ca85f38fe15f40ecaea1c"}, + {file = "geventhttpclient-2.3.9-cp314-cp314-win_amd64.whl", hash = "sha256:c4d5e1b9b1ac9baab42a1789bbfae7e97e40e8e83e09a32b353c6eb985f36071"}, + {file = "geventhttpclient-2.3.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ae44cec808193bb70b634fabdfdd89f0850744ace5668dc98063d633cf50c417"}, + {file = "geventhttpclient-2.3.9-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:53977ca41809eaef73cf38af170484baa53bde5f16bafbca7b77b670c343f48f"}, + {file = "geventhttpclient-2.3.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0f66a33c95e4d6d343fc6ace458b13c613684bf7cfd6832b61cc9c42eaf394f3"}, + {file = "geventhttpclient-2.3.9-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cfe23d419aa676492677374bdd37e364c921895d1090a180173be5d5f87f82b9"}, + {file = "geventhttpclient-2.3.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e3b279da39ad3eee69a5df9e1b602f87bcd2cec7eb258d3cc801e2170682383"}, + {file = "geventhttpclient-2.3.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:38535589a564822c64d1b4c2a5d6dcc27159d0d7d76500f2c8c8d21d9dd54880"}, + {file = "geventhttpclient-2.3.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a6436cd77885a8ef7cdc6d225cddd732560a17e92969c74e997836cf3135baa0"}, + {file = "geventhttpclient-2.3.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5000c9fb0553818c4e4c1de248ee4e9a56de0a245a30ef76b687542a935f4645"}, + {file = "geventhttpclient-2.3.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:52516d5c153fcef0d3d2447e533244dc6360e8c2a190b958861137db6f227605"}, + {file = "geventhttpclient-2.3.9-cp314-cp314t-win32.whl", hash = "sha256:14eaa836bde26a70952e95ca462018f3a47c1c92642327315aa6502e54141016"}, + {file = "geventhttpclient-2.3.9-cp314-cp314t-win_amd64.whl", hash = "sha256:b9bbcbc7d5d875e5180f2b1f1c6fa8e092ef80d9debfb6ba22a4ec28f0565395"}, + {file = "geventhttpclient-2.3.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:47be91fae0e9cd6eb9eab821278994d519cf27678f9e47eb302d41efcc6cae1a"}, + {file = "geventhttpclient-2.3.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1ed08b02b7c275397a528028d526d28f056bce3dc5cd285a7a9d7d78b0975f5a"}, + {file = "geventhttpclient-2.3.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ec656fd34f10fb0442446faece025dae848fc51e0b22f0ba1fc14c93e0f06ebb"}, + {file = "geventhttpclient-2.3.9-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a9757738669caebc4c96b529372362abc0c2cfe326b27bb9e67b5601fd5651f1"}, + {file = "geventhttpclient-2.3.9-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e82a219e9f3d644832c8ed18dd0bc615f21ed18659e1d26e187b29f81dff9e1"}, + {file = "geventhttpclient-2.3.9-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:08f0df30086a0ce05d75180753095b913a5e676c83ff92f8f9779bd064536d3c"}, + {file = "geventhttpclient-2.3.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b1f6c26e10b367629a2675bbd43ddedce1ba7ade13eb9ae3b3418e651c98fd9c"}, + {file = "geventhttpclient-2.3.9-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d32fc9ba6d82c4f8586a6bd6e99c7e15a25404f94743dce00367aec826809d"}, + {file = "geventhttpclient-2.3.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:90c1f2ca4d378a19005eb2a60376cfff4d6bceca4a569ca7bfec645b7a572c59"}, + {file = "geventhttpclient-2.3.9-cp39-cp39-win32.whl", hash = "sha256:5c824839aae388636a0496c71d71d7de0487c0458bfdd366da252265539aa88c"}, + {file = "geventhttpclient-2.3.9-cp39-cp39-win_amd64.whl", hash = "sha256:61b046492e5a831c97b8c47623f980f2d1f9f36fdc94e858bc786fa7e4dffca5"}, + {file = "geventhttpclient-2.3.9.tar.gz", hash = "sha256:16807578dc4a175e8d97e6e39d65a10b04b5237a8c55f7a5ef39044e869baeb8"}, +] + +[package.dependencies] +brotli = "*" +certifi = "*" +gevent = "*" +urllib3 = "*" + +[package.extras] +benchmarks = ["httplib2", "httpx", "requests", "urllib3"] +dev = ["dpkt", "pytest", "requests"] +examples = ["oauth2"] + +[[package]] +name = "greenlet" +version = "3.3.2" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\"" +files = [ + {file = "greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d"}, + {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13"}, + {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e"}, + {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7"}, + {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f"}, + {file = "greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef"}, + {file = "greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca"}, + {file = "greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f"}, + {file = "greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358"}, + {file = "greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99"}, + {file = "greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be"}, + {file = "greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5"}, + {file = "greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd"}, + {file = "greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070"}, + {file = "greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79"}, + {file = "greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395"}, + {file = "greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f"}, + {file = "greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643"}, + {file = "greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab"}, + {file = "greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a"}, + {file = "greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b"}, + {file = "greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124"}, + {file = "greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327"}, + {file = "greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506"}, + {file = "greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce"}, + {file = "greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5"}, + {file = "greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492"}, + {file = "greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71"}, + {file = "greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4"}, + {file = "greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727"}, + {file = "greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e"}, + {file = "greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a"}, + {file = "greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil", "setuptools"] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[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 = "locust" +version = "2.43.3" +description = "Developer-friendly load testing framework" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "locust-2.43.3-py3-none-any.whl", hash = "sha256:e032c119b54a9d984cb74a936ee83cfd7d68b3c76c8f308af63d04f11396b553"}, + {file = "locust-2.43.3.tar.gz", hash = "sha256:b5d2c48f8f7d443e3abdfdd6ec2f7aebff5cd74fab986bcf1e95b375b5c5a54b"}, +] + +[package.dependencies] +configargparse = ">=1.7.1" +flask = ">=2.0.0" +flask-cors = ">=3.0.10" +flask-login = ">=0.6.3" +gevent = ">=24.10.1,<25.8.1 || >25.8.1,<26.0.0" +geventhttpclient = ">=2.3.1" +msgpack = ">=1.0.0" +psutil = ">=5.9.1" +pytest = ">=8.3.3,<10" +python-engineio = ">=4.12.2" +python-socketio = {version = ">=5.13.0", extras = ["client"]} +pywin32 = {version = "*", markers = "sys_platform == \"win32\""} +pyzmq = ">=25.0.0" +requests = ">=2.32.2" +werkzeug = ">=2.0.0" + +[package.extras] +dns = ["dnspython (>=2.8.0)"] +milvus = ["pymilvus (>=2.5.0)"] +mqtt = ["paho-mqtt (>=2.1.0)"] +otel = ["opentelemetry-exporter-otlp-proto-grpc (>=1.38.0)", "opentelemetry-exporter-otlp-proto-http (>=1.38.0)", "opentelemetry-instrumentation-requests (>=0.59b0)", "opentelemetry-instrumentation-urllib3 (>=0.59b0)", "opentelemetry-sdk (>=1.38.0)"] + +[[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" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "msgpack" +version = "1.1.2" +description = "MessagePack serializer" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2"}, + {file = "msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87"}, + {file = "msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251"}, + {file = "msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a"}, + {file = "msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f"}, + {file = "msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f"}, + {file = "msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9"}, + {file = "msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa"}, + {file = "msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c"}, + {file = "msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0"}, + {file = "msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296"}, + {file = "msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef"}, + {file = "msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c"}, + {file = "msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e"}, + {file = "msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e"}, + {file = "msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68"}, + {file = "msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406"}, + {file = "msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa"}, + {file = "msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb"}, + {file = "msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f"}, + {file = "msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42"}, + {file = "msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9"}, + {file = "msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620"}, + {file = "msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029"}, + {file = "msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b"}, + {file = "msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69"}, + {file = "msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf"}, + {file = "msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7"}, + {file = "msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999"}, + {file = "msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e"}, + {file = "msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162"}, + {file = "msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794"}, + {file = "msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c"}, + {file = "msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9"}, + {file = "msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84"}, + {file = "msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00"}, + {file = "msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939"}, + {file = "msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e"}, + {file = "msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931"}, + {file = "msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014"}, + {file = "msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2"}, + {file = "msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717"}, + {file = "msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b"}, + {file = "msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af"}, + {file = "msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a"}, + {file = "msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b"}, + {file = "msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245"}, + {file = "msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90"}, + {file = "msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20"}, + {file = "msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27"}, + {file = "msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b"}, + {file = "msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff"}, + {file = "msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46"}, + {file = "msgpack-1.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea5405c46e690122a76531ab97a079e184c0daf491e588592d6a23d3e32af99e"}, + {file = "msgpack-1.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9fba231af7a933400238cb357ecccf8ab5d51535ea95d94fc35b7806218ff844"}, + {file = "msgpack-1.1.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a8f6e7d30253714751aa0b0c84ae28948e852ee7fb0524082e6716769124bc23"}, + {file = "msgpack-1.1.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94fd7dc7d8cb0a54432f296f2246bc39474e017204ca6f4ff345941d4ed285a7"}, + {file = "msgpack-1.1.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:350ad5353a467d9e3b126d8d1b90fe05ad081e2e1cef5753f8c345217c37e7b8"}, + {file = "msgpack-1.1.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6bde749afe671dc44893f8d08e83bf475a1a14570d67c4bb5cec5573463c8833"}, + {file = "msgpack-1.1.2-cp39-cp39-win32.whl", hash = "sha256:ad09b984828d6b7bb52d1d1d0c9be68ad781fa004ca39216c8a1e63c0f34ba3c"}, + {file = "msgpack-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:67016ae8c8965124fdede9d3769528ad8284f14d635337ffa6a713a580f6c030"}, + {file = "msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e"}, +] + +[[package]] +name = "outcome" +version = "1.3.0.post0" +description = "Capture the outcome of Python function calls." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, + {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, +] + +[package.dependencies] +attrs = ">=19.2.0" + +[[package]] +name = "packaging" +version = "26.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "psutil" +version = "7.2.2" +description = "Cross-platform lib for process and system monitoring." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b"}, + {file = "psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea"}, + {file = "psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63"}, + {file = "psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312"}, + {file = "psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b"}, + {file = "psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9"}, + {file = "psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00"}, + {file = "psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9"}, + {file = "psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a"}, + {file = "psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf"}, + {file = "psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1"}, + {file = "psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841"}, + {file = "psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486"}, + {file = "psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979"}, + {file = "psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9"}, + {file = "psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e"}, + {file = "psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8"}, + {file = "psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc"}, + {file = "psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988"}, + {file = "psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee"}, + {file = "psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372"}, +] + +[package.extras] +dev = ["abi3audit", "black", "check-manifest", "colorama ; os_name == \"nt\"", "coverage", "packaging", "psleak", "pylint", "pyperf", "pypinfo", "pyreadline3 ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] +test = ["psleak", "pytest", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "setuptools", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, + {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, +] + +[[package]] +name = "pycparser" +version = "3.0" +description = "C parser in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "(os_name == \"nt\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" or implementation_name == \"pypy\") and implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, + {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, +] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + +[[package]] +name = "pytest" +version = "9.0.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, + {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-flask" +version = "1.3.0" +description = "A set of py.test fixtures to test Flask applications." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "pytest-flask-1.3.0.tar.gz", hash = "sha256:58be1c97b21ba3c4d47e0a7691eb41007748506c36bf51004f78df10691fa95e"}, + {file = "pytest_flask-1.3.0-py3-none-any.whl", hash = "sha256:c0e36e6b0fddc3b91c4362661db83fa694d1feb91fa505475be6732b5bc8c253"}, +] + +[package.dependencies] +Flask = "*" +pytest = ">=5.2" +Werkzeug = "*" + +[package.extras] +docs = ["Sphinx", "sphinx-rtd-theme"] + +[[package]] +name = "python-engineio" +version = "4.13.1" +description = "Engine.IO server and client for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "python_engineio-4.13.1-py3-none-any.whl", hash = "sha256:f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399"}, + {file = "python_engineio-4.13.1.tar.gz", hash = "sha256:0a853fcef52f5b345425d8c2b921ac85023a04dfcf75d7b74696c61e940fd066"}, +] + +[package.dependencies] +simple-websocket = ">=0.10.0" + +[package.extras] +asyncio-client = ["aiohttp (>=3.11)"] +client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] +dev = ["tox"] +docs = ["furo", "sphinx"] + +[[package]] +name = "python-socketio" +version = "5.16.1" +description = "Socket.IO server and client for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35"}, + {file = "python_socketio-5.16.1.tar.gz", hash = "sha256:f863f98eacce81ceea2e742f6388e10ca3cdd0764be21d30d5196470edf5ea89"}, +] + +[package.dependencies] +bidict = ">=0.21.0" +python-engineio = ">=4.11.0" +requests = {version = ">=2.21.0", optional = true, markers = "extra == \"client\""} +websocket-client = {version = ">=0.54.0", optional = true, markers = "extra == \"client\""} + +[package.extras] +asyncio-client = ["aiohttp (>=3.4)"] +client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] +dev = ["tox"] +docs = ["furo", "sphinx"] + +[[package]] +name = "pywin32" +version = "311" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, + {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, + {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"}, + {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"}, + {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"}, + {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"}, + {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"}, + {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"}, + {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"}, + {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"}, + {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"}, + {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"}, + {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"}, + {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"}, + {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"}, + {file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"}, + {file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"}, + {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"}, + {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, + {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +description = "Python bindings for 0MQ" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4"}, + {file = "pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556"}, + {file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b"}, + {file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e"}, + {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526"}, + {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1"}, + {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386"}, + {file = "pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda"}, + {file = "pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f"}, + {file = "pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32"}, + {file = "pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86"}, + {file = "pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581"}, + {file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f"}, + {file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e"}, + {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e"}, + {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2"}, + {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394"}, + {file = "pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f"}, + {file = "pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97"}, + {file = "pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07"}, + {file = "pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc"}, + {file = "pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113"}, + {file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233"}, + {file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31"}, + {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28"}, + {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856"}, + {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496"}, + {file = "pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd"}, + {file = "pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf"}, + {file = "pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f"}, + {file = "pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5"}, + {file = "pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6"}, + {file = "pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7"}, + {file = "pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05"}, + {file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9"}, + {file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128"}, + {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39"}, + {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97"}, + {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db"}, + {file = "pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c"}, + {file = "pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2"}, + {file = "pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e"}, + {file = "pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a"}, + {file = "pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea"}, + {file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96"}, + {file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d"}, + {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146"}, + {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd"}, + {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a"}, + {file = "pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92"}, + {file = "pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0"}, + {file = "pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7"}, + {file = "pyzmq-27.1.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:18339186c0ed0ce5835f2656cdfb32203125917711af64da64dbaa3d949e5a1b"}, + {file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:753d56fba8f70962cd8295fb3edb40b9b16deaa882dd2b5a3a2039f9ff7625aa"}, + {file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b721c05d932e5ad9ff9344f708c96b9e1a485418c6618d765fca95d4daacfbef"}, + {file = "pyzmq-27.1.0-cp38-cp38-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be883ff3d722e6085ee3f4afc057a50f7f2e0c72d289fd54df5706b4e3d3a50"}, + {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b2e592db3a93128daf567de9650a2f3859017b3f7a66bc4ed6e4779d6034976f"}, + {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad68808a61cbfbbae7ba26d6233f2a4aa3b221de379ce9ee468aa7a83b9c36b0"}, + {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e2687c2d230e8d8584fbea433c24382edfeda0c60627aca3446aa5e58d5d1831"}, + {file = "pyzmq-27.1.0-cp38-cp38-win32.whl", hash = "sha256:a1aa0ee920fb3825d6c825ae3f6c508403b905b698b6460408ebd5bb04bbb312"}, + {file = "pyzmq-27.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:df7cd397ece96cf20a76fae705d40efbab217d217897a5053267cd88a700c266"}, + {file = "pyzmq-27.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb"}, + {file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429"}, + {file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d"}, + {file = "pyzmq-27.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345"}, + {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968"}, + {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098"}, + {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f"}, + {file = "pyzmq-27.1.0-cp39-cp39-win32.whl", hash = "sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78"}, + {file = "pyzmq-27.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db"}, + {file = "pyzmq-27.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50081a4e98472ba9f5a02850014b4c9b629da6710f8f14f3b15897c666a28f1b"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:510869f9df36ab97f89f4cff9d002a89ac554c7ac9cadd87d444aa4cf66abd27"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1f8426a01b1c4098a750973c37131cf585f61c7911d735f729935a0c701b68d3"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:726b6a502f2e34c6d2ada5e702929586d3ac948a4dbbb7fed9854ec8c0466027"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:bd67e7c8f4654bef471c0b1ca6614af0b5202a790723a58b79d9584dc8022a78"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9"}, + {file = "pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540"}, +] + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "selenium" +version = "4.41.0" +description = "Official Python bindings for Selenium WebDriver" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "selenium-4.41.0-py3-none-any.whl", hash = "sha256:b8ccde8d2e7642221ca64af184a92c19eee6accf2e27f20f30472f5efae18eb1"}, + {file = "selenium-4.41.0.tar.gz", hash = "sha256:003e971f805231ad63e671783a2b91a299355d10cefb9de964c36ff3819115aa"}, +] + +[package.dependencies] +certifi = ">=2026.1.4" +trio = ">=0.31.0,<1.0" +trio-websocket = ">=0.12.2,<1.0" +typing_extensions = ">=4.15.0,<5.0" +urllib3 = {version = ">=2.6.3,<3.0", extras = ["socks"]} +websocket-client = ">=1.8.0,<2.0" + +[[package]] +name = "simple-websocket" +version = "1.1.0" +description = "Simple WebSocket server and client for Python" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c"}, + {file = "simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4"}, +] + +[package.dependencies] +wsproto = "*" + +[package.extras] +dev = ["flake8", "pytest", "pytest-cov", "tox"] +docs = ["sphinx"] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95"}, + {file = "soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"}, +] + +[[package]] +name = "trio" +version = "0.33.0" +description = "A friendly Python library for async concurrency and I/O" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "trio-0.33.0-py3-none-any.whl", hash = "sha256:3bd5d87f781d9b0192d592aef28691f8951d6c2e41b7e1da4c25cde6c180ae9b"}, + {file = "trio-0.33.0.tar.gz", hash = "sha256:a29b92b73f09d4b48ed249acd91073281a7f1063f09caba5dc70465b5c7aa970"}, +] + +[package.dependencies] +attrs = ">=23.2.0" +cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} +idna = "*" +outcome = "*" +sniffio = ">=1.3.0" +sortedcontainers = "*" + +[[package]] +name = "trio-websocket" +version = "0.12.2" +description = "WebSocket library for Trio" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6"}, + {file = "trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae"}, +] + +[package.dependencies] +outcome = ">=1.2.0" +trio = ">=0.11" +wsproto = ">=0.14" + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, +] + +[package.dependencies] +pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "websocket-client" +version = "1.9.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef"}, + {file = "websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx_rtd_theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["pytest", "websockets"] + +[[package]] +name = "werkzeug" +version = "3.1.6" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131"}, + {file = "werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25"}, +] + +[package.dependencies] +markupsafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "wsproto" +version = "1.3.2" +description = "Pure-Python WebSocket protocol implementation" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584"}, + {file = "wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294"}, +] + +[package.dependencies] +h11 = ">=0.16.0,<1" + +[[package]] +name = "zope-event" +version = "6.1" +description = "Very basic event publishing system" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "zope_event-6.1-py3-none-any.whl", hash = "sha256:0ca78b6391b694272b23ec1335c0294cc471065ed10f7f606858fc54566c25a0"}, + {file = "zope_event-6.1.tar.gz", hash = "sha256:6052a3e0cb8565d3d4ef1a3a7809336ac519bc4fe38398cb8d466db09adef4f0"}, +] + +[package.extras] +docs = ["Sphinx"] +test = ["zope.testrunner (>=6.4)"] + +[[package]] +name = "zope-interface" +version = "8.2" +description = "Interfaces for Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "zope_interface-8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:788c293f3165964ec6527b2d861072c68eef53425213f36d3893ebee89a89623"}, + {file = "zope_interface-8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9a4e785097e741a1c953b3970ce28f2823bd63c00adc5d276f2981dd66c96c15"}, + {file = "zope_interface-8.2-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:16c69da19a06566664ddd4785f37cad5693a51d48df1515d264c20d005d322e2"}, + {file = "zope_interface-8.2-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c31acfa3d7cde48bec45701b0e1f4698daffc378f559bfb296837d8c834732f6"}, + {file = "zope_interface-8.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0723507127f8269b8f3f22663168f717e9c9742107d1b6c9f419df561b71aa6d"}, + {file = "zope_interface-8.2-cp310-cp310-win_amd64.whl", hash = "sha256:3bf73a910bb27344def2d301a03329c559a79b308e1e584686b74171d736be4e"}, + {file = "zope_interface-8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c65ade7ea85516e428651048489f5e689e695c79188761de8c622594d1e13322"}, + {file = "zope_interface-8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1ef4b43659e1348f35f38e7d1a6bbc1682efde239761f335ffc7e31e798b65b"}, + {file = "zope_interface-8.2-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:dfc4f44e8de2ff4eba20af4f0a3ca42d3c43ab24a08e49ccd8558b7a4185b466"}, + {file = "zope_interface-8.2-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8f094bfb49179ec5dc9981cb769af1275702bd64720ef94874d9e34da1390d4c"}, + {file = "zope_interface-8.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d2bb8e7364e18f083bf6744ccf30433b2a5f236c39c95df8514e3c13007098ce"}, + {file = "zope_interface-8.2-cp311-cp311-win_amd64.whl", hash = "sha256:6f4b4dfcfdfaa9177a600bb31cebf711fdb8c8e9ed84f14c61c420c6aa398489"}, + {file = "zope_interface-8.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:624b6787fc7c3e45fa401984f6add2c736b70a7506518c3b537ffaacc4b29d4c"}, + {file = "zope_interface-8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc9ded9e97a0ed17731d479596ed1071e53b18e6fdb2fc33af1e43f5fd2d3aaa"}, + {file = "zope_interface-8.2-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:532367553e4420c80c0fc0cabcc2c74080d495573706f66723edee6eae53361d"}, + {file = "zope_interface-8.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2bf9cf275468bafa3c72688aad8cfcbe3d28ee792baf0b228a1b2d93bd1d541a"}, + {file = "zope_interface-8.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0009d2d3c02ea783045d7804da4fd016245e5c5de31a86cebba66dd6914d59a2"}, + {file = "zope_interface-8.2-cp312-cp312-win_amd64.whl", hash = "sha256:845d14e580220ae4544bd4d7eb800f0b6034fe5585fc2536806e0a26c2ee6640"}, + {file = "zope_interface-8.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:6068322004a0158c80dfd4708dfb103a899635408c67c3b10e9acec4dbacefec"}, + {file = "zope_interface-8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2499de92e8275d0dd68f84425b3e19e9268cd1fa8507997900fa4175f157733c"}, + {file = "zope_interface-8.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f777e68c76208503609c83ca021a6864902b646530a1a39abb9ed310d1100664"}, + {file = "zope_interface-8.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b05a919fdb0ed6ea942e5a7800e09a8b6cdae6f98fee1bef1c9d1a3fc43aaa0"}, + {file = "zope_interface-8.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ccc62b5712dd7bd64cfba3ee63089fb11e840f5914b990033beeae3b2180b6cb"}, + {file = "zope_interface-8.2-cp313-cp313-win_amd64.whl", hash = "sha256:34f877d1d3bb7565c494ed93828fa6417641ca26faf6e8f044e0d0d500807028"}, + {file = "zope_interface-8.2-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:46c7e4e8cbc698398a67e56ca985d19cb92365b4aafbeb6a712e8c101090f4cb"}, + {file = "zope_interface-8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a87fc7517f825a97ff4a4ca4c8a950593c59e0f8e7bfe1b6f898a38d5ba9f9cf"}, + {file = "zope_interface-8.2-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:ccf52f7d44d669203c2096c1a0c2c15d52e36b2e7a9413df50f48392c7d4d080"}, + {file = "zope_interface-8.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aae807efc7bd26302eb2fea05cd6de7d59269ed6ae23a6de1ee47add6de99b8c"}, + {file = "zope_interface-8.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05a0e42d6d830f547e114de2e7cd15750dc6c0c78f8138e6c5035e51ddfff37c"}, + {file = "zope_interface-8.2-cp314-cp314-win_amd64.whl", hash = "sha256:561ce42390bee90bae51cf1c012902a8033b2aaefbd0deed81e877562a116d48"}, + {file = "zope_interface-8.2.tar.gz", hash = "sha256:afb20c371a601d261b4f6edb53c3c418c249db1a9717b0baafc9a9bb39ba1224"}, +] + +[package.extras] +docs = ["Sphinx", "furo", "repoze.sphinx.autointerface"] +test = ["coverage[toml]", "zope.event", "zope.testing"] +testing = ["coverage[toml]", "zope.event", "zope.testing"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.13" +content-hash = "608feb26b439b7b60a424812084a9bbc5b830f03b8ad226b380df93523c8435c" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..828d0b793 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[project] +name = "openclassrooms-project-11" +version = "0.1.0" +description = "" +authors = [ + {name = "NM",email = "nicolas.marie.nm@gmail.com"} +] +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "beautifulsoup4 (>=4.14.3,<5.0.0)", + "flask (>=3.1.3,<4.0.0)", + "pytest (>=9.0.2,<10.0.0)", + "pytest-flask (>=1.3.0,<2.0.0)", + "selenium (>=4.41.0,<5.0.0)", + "flask-testing (>=0.8.1,<0.9.0)", + "locust (>=2.43.3,<3.0.0)", + "flake8 (>=7.3.0,<8.0.0)", + "flake8-html (>=0.4.3,<0.5.0)" +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt index 139affa05..ac9cb4c18 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/server.py b/server.py index 4084baeac..1448b81d8 100644 --- a/server.py +++ b/server.py @@ -1,59 +1,326 @@ -import json -from flask import Flask,render_template,request,redirect,flash,url_for +import os +from flask import Flask, render_template, request, redirect, flash, url_for, session +from werkzeug.security import generate_password_hash, check_password_hash -def loadClubs(): - with open('clubs.json') as c: - listOfClubs = json.load(c)['clubs'] - return listOfClubs +app = Flask(__name__) +app.secret_key = 'something_special' -def loadCompetitions(): - with open('competitions.json') as comps: - listOfCompetitions = json.load(comps)['competitions'] - return listOfCompetitions +app.config["CLUBS_JSON"] = os.environ.get("CLUBS_JSON") +app.config["COMPETITIONS_JSON"] = os.environ.get("COMPETITIONS_JSON") +CLUB_POINTS = 15 -app = Flask(__name__) -app.secret_key = 'something_special' +import utils +import exceptions + +utils.clubs = utils.load_clubs() +utils.competitions = utils.load_competitions() -competitions = loadCompetitions() -clubs = loadClubs() @app.route('/') def index(): - return render_template('index.html') + """ + Route to home page + Returns: + The template for home page + """ + return render_template(template_name_or_list='index.html') -@app.route('/showSummary',methods=['POST']) -def showSummary(): - club = [club for club in clubs if club['email'] == request.form['email']][0] - return render_template('welcome.html',club=club,competitions=competitions) +@app.route('/signUp') +def sign_up(): + """ + Route to sign up page + Returns: + The template for sign up page + """ + return render_template(template_name_or_list='sign_up.html') + + +@app.route('/profile/', methods=['GET']) +def profile(club): + """ + Route to club profile page + Args: + club (str): The club name + + Returns: + The template for club profile page. The template may display a message to the user. + """ + if "club" in session and session['club'] == club: + the_club = next((c for c in utils.clubs if c['name'] == club), None) + + if the_club is None: + flash(message="Sorry, that club was not found.") + return render_template(template_name_or_list="index.html", error="Club not found"), 404 + + return render_template(template_name_or_list='profile.html', club=the_club) + + flash(message="Sorry, you are not allow to see that profile.") + return render_template(template_name_or_list='index.html', error="Not allow"), 403 + + +@app.route('/profile', methods=['POST']) +def profile_post(): + """ + Route for signing up. + Returns: + The template to profile page if correctly signed up or sign up page otherwise. + """ + club_name = request.form['name'] + club_email = request.form['email'] + club_password = request.form['password'] + club_password_confirmation = request.form['confirm_password'] + + try: + utils.validate_profile_fields(club_name, club_email, club_password, club_password_confirmation) + + except exceptions.ValidationError as e: + flash(message=e.message) + return redirect(location=url_for('sign_up')) + + try: + utils.validate_email_format(club_email) + + except exceptions.ValidationError as e: + flash(message=e.message) + return redirect(location=url_for('sign_up')) + + found_clubs = [c for c in utils.clubs if c['email'] == club_email or c['name'] == club_name] + + if not found_clubs: + if club_password != club_password_confirmation: + flash(message='Sorry, passwords do not match') + return redirect(location=url_for('sign_up')) + + hashed_password = generate_password_hash(club_password) + utils.add_club(name=club_name, + email=club_email, + password=hashed_password, + points=str(CLUB_POINTS)) + + the_club = next((c for c in utils.clubs if c['email'] == club_email), None) + + if the_club is None: + flash(message="Sorry, something went wrong. Please try again.") + return render_template(template_name_or_list='sign_up.html') + + flash(message="Great! You have successfully signed up.") + session['club'] = the_club['name'] + return render_template(template_name_or_list='profile.html', club=the_club) -@app.route('/book//') -def book(competition,club): - foundClub = [c for c in clubs if c['name'] == club][0] - foundCompetition = [c for c in competitions if c['name'] == competition][0] - if foundClub and foundCompetition: - return render_template('booking.html',club=foundClub,competition=foundCompetition) else: - flash("Something went wrong-please try again") - return render_template('welcome.html', club=club, competitions=competitions) + flash(message="Sorry, the club already exists.") + return render_template(template_name_or_list='sign_up.html') + + +@app.route('/changePassword/', methods=['GET', 'POST']) +def change_password(club): + """ + Route to change password. + Args: + club (str): The club name + + Returns: + The template for club profile page. The index template otherwise. + """ + if "club" in session and session['club'] == club: + if request.method == 'GET': + the_club = next((c for c in utils.clubs if c['name'] == club), None) + + if the_club is None: + flash(message="Sorry, that club was not found.") + return render_template(template_name_or_list="index.html", error="Email not found"), 404 + + return render_template(template_name_or_list='change_password.html', club=the_club) + else: + club_password = request.form['password'] + club_password_confirmation = request.form['confirm_password'] + + the_club = next((c for c in utils.clubs if c['name'] == club), None) + + try: + utils.validate_password(password=club_password, + password2=club_password_confirmation, + club=the_club) + except exceptions.ValidationError as e: + flash(e.message) + return render_template(template_name_or_list='change_password.html', + club=the_club, + error=e.tag), 200 + + the_club = utils.update_club_password(the_club, club_password) + + if the_club: + flash(message="Great! You have successfully changed your password.") + return render_template(template_name_or_list='profile.html', club=the_club) + + flash(message="Sorry, something went wrong. Please try again.") + return render_template(template_name_or_list='index.html') + + flash(message="Sorry, you are not allow to do this action.") + return render_template(template_name_or_list='index.html', error="Not allow"), 403 + + +@app.route('/showSummary/', methods=['GET']) +def show_summary(club): + """ + Route to club summary page + Args: + club (str): The club name + + Returns: + The welcome template if authorized. The index template otherwise. + """ + if "club" in session and session['club'] == club: + the_club = next((c for c in utils.clubs if c['name'] == club), None) + + return render_template(template_name_or_list='welcome.html', + club=the_club, + competitions=utils.competitions) + + flash(message="Sorry, you are not allow to do this action.") + return render_template(template_name_or_list='index.html', error="Not allow"), 403 + + +@app.route('/showSummary', methods=['POST']) +def show_summary_post(): + """ + Route to log in. + Returns: + The welcome template if success. The index template otherwise. + """ + try: + utils.validate_login_fields(request.form['email'], request.form['password']) + except exceptions.ValidationError as e: + flash(message=e.message) + return render_template(template_name_or_list="index.html", error=e.tag), 404 + + the_club = next((c for c in utils.clubs if c['email'] == request.form['email']), None) + + if the_club is None: + flash(message="Sorry, that email was not found.") + return render_template(template_name_or_list="index.html", error="Email not found"), 404 + + if not check_password_hash(the_club['password'], request.form['password']): + flash(message="Sorry, the password is incorrect.") + return render_template(template_name_or_list="index.html", error="Incorrect password"), 403 + + session["club"] = the_club["name"] + + flash("Great! You are successfully logged in.") + return render_template(template_name_or_list='welcome.html', + club=the_club, + competitions=utils.competitions) + + +@app.route('/book//') +def book(competition, club): + """ + Route to book page + Args: + competition (str): The competition name + club (str): The club name + + Returns: + The booking template if success. The welcome template if error. The index template otherwise. + """ + if "club" in session and session['club'] == club: + found_club = [c for c in utils.clubs if c['name'] == club][0] + found_competition = [c for c in utils.competitions if c['name'] == competition][0] + + try: + utils.validate_competition(the_competition=found_competition) + + except exceptions.ValidationError as e: + flash(message=e.message) + + the_club = next((a_club for a_club in utils.clubs if a_club['name'] == club), None) + + return render_template(template_name_or_list='welcome.html', + club=the_club, + competitions=utils.competitions, + error=e.tag), 200 + + if found_club and found_competition: + return render_template(template_name_or_list='booking.html', + club=found_club, + competition=found_competition), 200 + else: + flash(message="Sorry, something went wrong. Please try again.") + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=utils.competitions) + + flash(message="Sorry, you are not allow to do this action.") + return render_template(template_name_or_list='index.html', error="Not allow"), 403 + + +@app.route('/purchasePlaces', methods=['POST']) +def purchase_places(): + """ + Route to purchase places page + Returns: + The welcome template if success. The welcome template with error message otherwise. + """ + competition = [c for c in utils.competitions if c['name'] == request.form['competition']][0] + club = [c for c in utils.clubs if c['name'] == request.form['club']][0] + + places_required = int(request.form['places']) if request.form['places'] else 0 + + try: + utils.validate_places(places_required=places_required, + club=club, + the_competition=competition) + + except exceptions.ValidationError as e: + flash(message=e.message) + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=utils.competitions, + error=e.tag), 200 + + utils.update_club_booked_places(club=club, + places=places_required, + competition_name=competition["name"]) + + utils.update_competition_available_places(the_competition=competition, places=places_required) + + flash(message=f""" + Great! Booking of {places_required} place(s) for {competition['name']} competition complete! + """) + + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=utils.competitions) -@app.route('/purchasePlaces',methods=['POST']) -def purchasePlaces(): - competition = [c for c in competitions if c['name'] == request.form['competition']][0] - club = [c for c in clubs if c['name'] == request.form['club']][0] - placesRequired = int(request.form['places']) - competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired - flash('Great-booking complete!') - return render_template('welcome.html', club=club, competitions=competitions) +@app.route('/pointsBoard') +def points_board(): + """ + Route to points board page + Returns: + The points board template. + """ + clubs_for_board = utils.copy_clubs_for_board() + club = session.get('club') -# TODO: Add route for points display + return render_template(template_name_or_list='points_board.html', + clubs=clubs_for_board, + club=club) @app.route('/logout') def logout(): - return redirect(url_for('index')) \ No newline at end of file + """ + Route for logging out + Returns: + The index template. + """ + flash(message="Great! You are successfully logged out.") + if 'club' in session: + session.pop('club') + return redirect(location=url_for('index')) diff --git a/static/logo_gudlft.png b/static/logo_gudlft.png new file mode 100644 index 000000000..880a76e2a Binary files /dev/null and b/static/logo_gudlft.png differ diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 000000000..be69df0c3 --- /dev/null +++ b/static/styles.css @@ -0,0 +1,225 @@ +* { + box-sizing: border-box; +} + +a { + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +body { + background-color: #f0f0f1; + font-family: Verdana, Geneva, Tahoma, sans-serif; + text-align: center; + display: block; + align-items: center; + justify-content: center; + margin-top: 100px; +} + +body img { + width: 114px; +} + +main { + +} + +.message { + font-style: italic; + color: darkorange; +} + +.main_content, .footer, .points_board { + display: block; + width: 860px; + margin: 10px auto; + background-color: #ffffff; + text-align: center; + border-radius: 10px; + border: 1px solid #c3c4c7; + padding: 15px; + box-shadow: 1px 1px 3px #ccc; +} + +.main_content { + border-left: 5px solid #72aee6; +} + +.points_board { + border-left: 5px solid #dba617; +} + +.footer { + border-left: 5px solid #93003f; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + margin-top: 15px; + font-size: 13px; +} + +h1 { + color: #72aee6; +} + +h5 { + font-weight: normal; +} + +.main_content a, .footer a:visited { + color: #72aee6; +} + +.points_board a, .points_board a:visited { + color: #dba617; + font-weight: bold; + border: 1px dashed #dba617; + border-radius: 5px; + padding: 5px; +} + +.footer a, .footer a:visited { + color: #93003f; +} + +.points_board a:hover { + border: 1px solid #dba617; + box-shadow: 1px 1px 3px #ccc; +} + +.board{ + display: block; + border: 1px black dotted; + border-radius: 10px; + width: fit-content; + margin: 10px auto; + box-shadow: 1px 1px 3px #ccc; +} + +.board:hover { + border: 1px black solid; + box-shadow: 1px 1px 5px #ccc; +} + +.board table{ + width:100%; +} + +.board table tr{ + width: 100%; +} + +.board table thead div{ + background-color: #000000; + color: white; + font-weight: bold; +} + +.board table td{ + color: black; +} + +.board table td p{ + text-align: center; +} + +.board .cell, .board .cell_title{ + border-radius: 10px; + align-items: center; + display: flex; + justify-content: center; + padding: 3px 10px; +} + +.board .cell_title{ + padding: 15px 30px; +} + +.separator { + width: 50%; + height: 1px; + background: linear-gradient(to right, transparent, #72aee6, transparent); + align-self: stretch; + margin: 30px auto 30px auto; +} + +ul { + width: fit-content; + margin: 20px auto; + padding: 0; + list-style: none; + background-color: #ffffff; + border-radius: 10px; + gap: 5px; + display: flex; + flex-wrap: wrap; + flex-direction: row; + font-size: 14px; +} + +ul li { + list-style-type: none; + margin: 10px auto; +} + +.profile { + display: block; +} + +ul li.competition { + padding: 10px 5px; + text-align: center; + background-color: rgba(114, 174, 230, 0.1); + margin: 10px auto; + border: 1px solid #2271b1; + border-radius: 10px; + +} + +ul li.competition:hover { + background-color: #f7f0df; +} + +h6 { + font-weight: normal; +} + +button, .book, .past, .full { + border-radius: 5px; + font-weight: bold; + font-size: 16px; + padding: 5px; + background-color: #ffffff; + cursor: pointer; + margin: 10px auto; +} + +button, .book { + border: 1px solid #72aee6; + color: #72aee6; +} + +.past, .full { + cursor: default; + display: inline-block; + margin: -5px auto; +} + +.past { + border: 1px solid grey; + color: grey; +} + +.full { + border: 1px solid darkorange; + color: darkorange; +} + +button:hover, .book:hover { + box-shadow: 1px 1px 3px #ccc; +} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 000000000..4f654b65a --- /dev/null +++ b/templates/base.html @@ -0,0 +1,25 @@ + + + + + GÜDLFT Portal + + + +
+
+ +

Welcome to the GÜDLFT Portal!

+ {% block content %}{% endblock %} +
+
+
+ + + + \ No newline at end of file diff --git a/templates/booking.html b/templates/booking.html index 06ae1156c..7a6b0233f 100644 --- a/templates/booking.html +++ b/templates/booking.html @@ -1,17 +1,13 @@ - - - - - Booking for {{competition['name']}} || GUDLFT - - +{% extends 'base.html' %} +{% block content %}

{{competition['name']}}

- Places available: {{competition['numberOfPlaces']}} + Places available: {{competition['number_of_places']}}

- - +    +

+
- - \ No newline at end of file + Go back +{% endblock %} \ No newline at end of file diff --git a/templates/change_password.html b/templates/change_password.html new file mode 100644 index 000000000..796d130cb --- /dev/null +++ b/templates/change_password.html @@ -0,0 +1,38 @@ +{% extends 'base.html' %} +{% block content %} +
+ {% with messages = get_flashed_messages()%} + {% if messages %} +
    + {% for message in messages %} + {% if "Sorry" in message %} + {% if error %} +
  • {{ error }} - ❌ {{message}}
  • + {% else %} +
  • ❌ {{message}}
  • + {% endif %} + {% else %} +
  • ✅ {{message}}
  • + {% endif %} + {% endfor %} +
+ {% endif%} + {%endwith%} +
+

Change Password

+ ⯈ Please enter your new password:

+ Name : {{club['name']}}
+ Email : {{club['email']}} +
+
+
+ + + + + +

+ +
+ Go to profile +{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 926526b7d..5efb26fcb 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,16 +1,35 @@ - - - - - GUDLFT Registration - - -

Welcome to the GUDLFT Registration Portal!

- Please enter your secretary email to continue: +{% extends 'base.html' %} +{% block content %} +
+ {% with messages = get_flashed_messages()%} + {% if messages %} +
    + {% for message in messages %} + {% if "Sorry" in message %} + {% if error %} +
  • {{ error }} - ❌ {{message}}
  • + {% else %} +
  • ❌ {{message}}
  • + {% endif %} + {% else %} +
  • ✅ {{message}}
  • + {% endif %} + {% endfor %} +
+ {% endif%} + {%endwith%} +
+

Authentication

+ ⯈ Please enter your secretary email and your password to continue or sign up +
+
- - + + + +

+
- - \ No newline at end of file +
+{% endblock %} \ No newline at end of file diff --git a/templates/points_board.html b/templates/points_board.html new file mode 100644 index 000000000..da56c498d --- /dev/null +++ b/templates/points_board.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} +{% block content %} +

Points board

+
+ ⯈ Here is the board for all the clubs and their points. +
+ + + + + + {% for club in clubs %} + + + + + {% endfor %} +
Club
Points
{{ club.name }}
{{ club.points }}
+
+
+ To continue into the application, + {% if club %} + go to summary + {% else %} + please log in + {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 000000000..32bb22bc0 --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} +{% block content %} +
+ {% with messages = get_flashed_messages()%} + {% if messages %} +
    + {% for message in messages %} + {% if "Sorry" in message %} + {% if error %} +
  • {{ error }} - ❌ {{message}}
  • + {% else %} +
  • ❌ {{message}}
  • + {% endif %} + {% else %} +
  • ✅ {{message}}
  • + {% endif %} + {% endfor %} +
+ {% endif%} + {%endwith%} +
+

Welcome, {{club['email']}}

+

- Profile -

+
    +
  • Name : {{club['name']}}
  • +
  • Email : {{club['email']}}
  • +
  • Change password
  • +
  • Points available: {{club['points']}}
  • +
+ Go to summary +{% endblock %} \ No newline at end of file diff --git a/templates/sign_up.html b/templates/sign_up.html new file mode 100644 index 000000000..3a929cdf7 --- /dev/null +++ b/templates/sign_up.html @@ -0,0 +1,40 @@ +{% extends 'base.html' %} +{% block content %} +
+ {% with messages = get_flashed_messages()%} + {% if messages %} +
    + {% for message in messages %} + {% if "Sorry" in message %} + {% if error %} +
  • {{ error }} - ❌ {{message}}
  • + {% else %} +
  • ❌ {{message}}
  • + {% endif %} + {% else %} +
  • ✅ {{message}}
  • + {% endif %} + {% endfor %} +
+ {% endif%} + {%endwith%} +
+

Registration

+ ⯈ Please enter your details +

+
+ + + + +

+ + + + +
+
+ +
+ Go back +{% endblock %}l> \ No newline at end of file diff --git a/templates/welcome.html b/templates/welcome.html index ff6b261a2..95dde42ab 100644 --- a/templates/welcome.html +++ b/templates/welcome.html @@ -1,36 +1,47 @@ - - - - - Summary | GUDLFT Registration - - -

Welcome, {{club['email']}}

Logout +{% extends 'base.html' %} +{% block content %} +
{% with messages = get_flashed_messages()%} - {% if messages %} -
    - {% for message in messages %} -
  • {{message}}
  • - {% endfor %} -
- {% endif%} - Points available: {{club['points']}} -

Competitions:

-
    - {% for comp in competitions%} -
  • - {{comp['name']}}
    - Date: {{comp['date']}}
    - Number of Places: {{comp['numberOfPlaces']}} - {%if comp['numberOfPlaces']|int >0%} - Book Places - {%endif%} -
  • -
    - {% endfor %} -
- {%endwith%} + {% if messages %} +
    + {% for message in messages %} + {% if "Sorry" in message %} + {% if error %} +
  • {{ error }} - ❌ {{message}}
  • + {% else %} +
  • ❌ {{message}}
  • + {% endif %} + {% else %} +
  • ✅ {{message}}
  • + {% endif %} + {% endfor %} +
+ {% endif%} + {%endwith%} +
+

Welcome, {{club['email']}}

- - \ No newline at end of file + Go to profile +

+ Points available: {{club['points']}} +

- Competitions -

+
    + {% for comp in competitions%} +
  • + {{comp['name']}}
    + Date: {{comp['date']}}
    + Number of Places: {{comp['number_of_places']}}

    + {% if comp.is_past %} +

    Already happened

    + {% else %} + {%if comp['number_of_places']|int >0%} + Book Places + {% else %} +

    Sold out

    + {% endif %} + {% endif %} +
  • + {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..5383eae93 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,278 @@ +import threading +import pytest +import time + +from random import randint +from werkzeug.security import generate_password_hash +from server import app + + +@pytest.fixture +def client(): + my_app = app + with my_app.test_client() as client: + yield client + + +@pytest.fixture +def get_clubs(): + """ + Fixture that returns fictive clubs list + Returns: + The list of fictive clubs + """ + the_clubs = [ + { + "name": "Simply Lift", + "email": "john@simplylift.co", + "password": generate_password_hash("tp1_Tmn28"), + "points": "13" + }, + { + "name": "Iron Temple", + "email": "admin@irontemple.com", + "password": generate_password_hash("tp2_Tmn29"), + "points": "4" + }, + { + "name": "She Lifts", + "email": "kate@shelifts.co.uk", + "password": generate_password_hash("tp3_Tmn30"), + "points": "12", + "booked_places": { + "Spring Festival": "7" + } + }, + { + "name": "Power Lift", + "email": "admin@powerlift.com", + "password": generate_password_hash("tp4_Tmn40"), + "points": "5" + } + ] + return the_clubs + + +@pytest.fixture +def get_competitions(): + """ + Method that returns a list of fictive competitions + Returns: + The list of fictive competitions + """ + the_competitions = [ + { + "name": "Fall Classic", + "date": "2020-10-22 13:30:00", + "number_of_places": "13" + }, + { + "name": "Spring Festival", + "date": "2026-07-27 10:00:00", + "number_of_places": "25" + }, + { + "name": "Winter Power", + "date": "2026-06-26 12:16:00", + "number_of_places": "4" + }, + { + "name": "Summer Stronger", + "date": "2026-05-30 18:23:40", + "number_of_places": "0" + } + ] + return the_competitions + + +@pytest.fixture +def get_credentials(): + data = {"email": "kate@shelifts.co.uk", "password": "tp3_Tmn30"} + return data + + +@pytest.fixture +def get_bad_credentials(): + data = {"email": "kate@shelifts.co.uk", "password": "tp4_Tmn30"} + return data + + +@pytest.fixture +def get_credentials_2(): + data = {"email": "admin@irontemple.com", "password": "tp2_Tmn29"} + return data + + +@pytest.fixture +def get_credentials_3(): + data = {"email": "admin@powerlift.com", "password": "tp4_Tmn40"} + return data + + +@pytest.fixture +def get_details(): + data = {"name": "Name Test", + "email": "admin@test.com", + "password": "tp4_Tmn40", + "password2": "tp4_Tmn40"} + return data + + +@pytest.fixture +def get_details_2(): + data = {"name": "Name Test", + "email": "admin@test.com", + "password": "tp4_Tmn40", + "confirm_password": "tp4_Tmn40"} + return data + + +@pytest.fixture +def get_wrong_details(): + data = {"name": "", + "email": "admin@test.com", + "password": "", + "password2": "tp4_Tmn40"} + return data + + +@pytest.fixture +def get_wrong_details_2(): + data = {"name": "", + "email": "admin@test.com", + "password": "", + "confirm_password": "tp4_Tmn40"} + return data + + +@pytest.fixture +def get_unexisting_credentials(): + data = {"email": "nicolas.marie@unexisting.com", "password": "er45_shet"} + return data + + +@pytest.fixture +def get_existing_competition_and_club(): + data = {"competition": "Spring Festival", "club": "She Lifts"} + return data + + +@pytest.fixture +def get_existing_competition_and_club_2(): + data = {"competition": "Spring Festival", "club": "Iron Temple"} + return data + + +@pytest.fixture +def get_existing_competition_and_club_3(): + data = {"competition": "Fall Classic", "club": "Iron Temple"} + return data + + +@pytest.fixture +def get_existing_competition_and_club_4(): + data = {"competition": "Summer Stronger", "club": "Power Lift"} + return data + + +@pytest.fixture +def get_existing_competition_and_club_5(): + data = {"competition": "Winter Power", "club": "Power Lift"} + return data + + +@pytest.fixture +def get_existing_competition_and_club_6(): + data = {"competition": "Spring Festival", "club": "Power Lift"} + return data + + +@pytest.fixture +def get_consistent_purchasing_data(): + competition = "Spring Festival" + club_name = "She Lifts" + places_to_book = randint(1, 5) + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + + +@pytest.fixture +def get_new_club(): + club = { + "name": "New Club", + "email": "new@newclub.com", + "password": generate_password_hash("tp6_Tmn60"), + "points": "12" + } + return club + + +@pytest.fixture +def get_inconsistent_purchasing_data(): + competition = "Spring Festival" + club_name = "Iron Temple" + club_points = 4 + places_to_book = randint(club_points + 1, 12) + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + + +@pytest.fixture +def purchasing_over_12_places(): + competition = "Spring Festival" + club_name = "She Lifts" + places_to_book = 13 + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + + +@pytest.fixture +def purchasing_13_cumulative_places(): + competition = "Spring Festival" + club_name = "She Lifts" + places_to_book = 6 + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + + +@pytest.fixture +def purchasing_with_negative_places(): + competition = "Spring Festival" + club_name = "She Lifts" + places_to_book = -2 + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + + +@pytest.fixture +def purchasing_places_more_than_available(): + competition = "Winter Power" + club_name = "Power Lift" + places_to_book = 5 + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + + +@pytest.fixture(scope="module") +def live_server(): + app.config["TESTING"] = True + + ctx = app.app_context() + ctx.push() + + server_thread = threading.Thread( + target=app.run, + kwargs={"port": 5000, "use_reloader": False}, + daemon=True + ) + server_thread.start() + time.sleep(1) + + yield "http://127.0.0.1:5000" + ctx.pop() diff --git a/tests/functional_tests/__init__.py b/tests/functional_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/functional_tests/test_app.py b/tests/functional_tests/test_app.py new file mode 100644 index 000000000..24909b8e4 --- /dev/null +++ b/tests/functional_tests/test_app.py @@ -0,0 +1,63 @@ +import time +import pytest +from selenium import webdriver +from selenium.webdriver.common.by import By + + +class TestFunctionalApp: + @pytest.fixture(autouse=True) + def setup(self, mocker, get_clubs, get_competitions): + mocker.patch('utils.clubs', get_clubs) + mocker.patch('utils.competitions', get_competitions) + + mocker.patch('utils.save_clubs') + mocker.patch('utils.save_competitions') + + def test_signup(self, live_server): + """ + Test that the signup process is correctly done with given details. + Args: + live_server (threading.Thread): A live server fixture. + """ + browser = webdriver.Chrome() + browser.get(f"{live_server}/signUp") + + name = browser.find_element(By.ID, "name") + name.send_keys("Test Club") + email = browser.find_element(By.ID, "email") + email.send_keys("doe@testclub.com") + password1 = browser.find_element(By.ID, "password") + password1.send_keys("tgl_Prn_C6") + password2 = browser.find_element(By.ID, "confirm-password") + password2.send_keys("tgl_Prn_C6") + signup = browser.find_element(By.ID, "signup") + signup.click() + + time.sleep(1) + + assert browser.find_element("tag name", "h2").text == "Welcome, doe@testclub.com" + + browser.quit() + + def test_login(self, live_server, get_credentials): + """ + Test that the login process is correctly done with given credentials. + Args: + live_server (threading.Thread): A live server fixture. + get_credentials (dict): The credentials. + """ + browser = webdriver.Chrome() + browser.get(f"{live_server}/") + + email = browser.find_element(By.ID, "email") + email.send_keys(get_credentials["email"]) + password = browser.find_element(By.ID, "password") + password.send_keys(get_credentials["password"]) + login = browser.find_element(By.ID, "login") + login.click() + + time.sleep(1) + + assert browser.find_element("tag name", "h2").text == "Welcome, kate@shelifts.co.uk" + + browser.quit() diff --git a/tests/integration_tests/__init__.py b/tests/integration_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration_tests/test_views.py b/tests/integration_tests/test_views.py new file mode 100644 index 000000000..8abfc673a --- /dev/null +++ b/tests/integration_tests/test_views.py @@ -0,0 +1,436 @@ +import pytest +import utils + +from bs4 import BeautifulSoup +from flask import url_for + + +class TestIntegrationViews: + @pytest.fixture(autouse=True, scope='function') + def setup(self, mocker, get_clubs, get_competitions): + mocker.patch('utils.clubs', get_clubs) + mocker.patch('utils.competitions', get_competitions) + + mocker.patch('utils.save_clubs') + mocker.patch('utils.save_competitions') + + @staticmethod + def test_summary_logout_redirect_returns_welcome(client, get_credentials): + """ + Test that the logout redirects to the index page with the 200 status code and appropriate + information. + Args: + client (FlaskClient): A Flask client + get_credentials (dict): The credentials + """ + client.post('/showSummary', data=get_credentials) + + logout_response = client.get('/logout') + soup = BeautifulSoup(logout_response.data.decode(), features="html.parser") + url = soup.find_all('a')[0].get('href') + redirect_response = client.get(url, follow_redirects=True) + + assert redirect_response.status_code == 200 + data = redirect_response.data.decode('utf-8') + + assert "Welcome to the GÜDLFT Portal!" in data + assert "Authentication" in data + assert ( + 'Please enter your secretary email and your password to continue ' + 'or sign up' + ) in data + assert "Email:" in data + assert "Password:" in data + + @staticmethod + def test_booking_return_festival_page_booking(client, + get_credentials, + get_existing_competition_and_club): + """ + Test that the competition bookin page is returned with the appropriate information. + Args: + client (FlaskClient): A Flask client + get_credentials (dict): The credentials + get_existing_competition_and_club (dict): The competition and club + """ + client.post('/showSummary', data=get_credentials) + + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) + data = client_response.data.decode('utf-8') + assert "Spring Festival" in data + assert "Places available: " in data + assert "How many places?" in data + + @staticmethod + def test_good_purchasing_places_returns_summary_page(client, + get_credentials, + get_consistent_purchasing_data): + """ + Test that the competition purchasing page is returned with the 200 status code and the + appropriate information. + Args: + client (FlaskClient): A Flask client + get_credentials (dict): The credentials + get_consistent_purchasing_data (dict): The competition and club + """ + client.post('/showSummary', data=get_credentials) + + purchasing_data = get_consistent_purchasing_data + the_club = [club for club in utils.clubs if club["name"] == purchasing_data['club']][0] + the_competition = [competition for competition in utils.competitions + if competition["name"] == purchasing_data['competition']][0] + + client.get(url_for(endpoint='book', + competition=the_competition['name'], + club=the_club['name'])) + + club_points = the_club['points'] + competition_places = the_competition['number_of_places'] + + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') + + new_points = int(club_points) - int(purchasing_data['places']) + new_competition_places = int(competition_places) - int(purchasing_data['places']) + + soup = BeautifulSoup(data, features="html.parser") + all_li = "".join([str(li) for li in soup.find_all('li')]) + the_club_name_utf8 = "%20".join(the_club['name'].split()) + the_competition_name_utf8 = "%20".join(the_competition['name'].split()) + + expected_li = f"""
  • + {the_competition["name"]}
    + Date: 2026-07-27 10:00:00
    + Number of Places: {new_competition_places}

    + Book Places +
  • """ + + assert client_response.status_code == 200 + assert ( + f"Great! Booking of {purchasing_data['places']} place(s) for " + f"{purchasing_data['competition']} competition complete!" + ) in data + + assert f"Welcome, {the_club["email"]} " in data + assert "".join(expected_li.split()) in "".join(all_li.split()) + assert f"Points available: {new_points}" in data + + @staticmethod + def test_purchasing_places_not_enough_points_returns_sorry(client, + get_credentials_2, + get_existing_competition_and_club_2, + get_inconsistent_purchasing_data): + """ + Test that the 200 status code and an appropriate message are returned in case of not enough + points while purchasing. + Args: + client (FlaskClient): A Flask client + get_credentials_2 (dict): The credentials + get_existing_competition_and_club_2 (dict): The competition and club + get_inconsistent_purchasing_data (dict): The purchasing data + """ + client.post('/showSummary', data=get_credentials_2) + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_2['competition'], + club=get_existing_competition_and_club_2['club'])) + + the_club = [club for club in utils.clubs + if club["name"] == get_existing_competition_and_club_2['club']][0] + + client_response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 200 + assert "Not enough points" in data + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, you do not have enough points to purchase." in data + assert f"Points available: {the_club['points']}" in data + + @staticmethod + def test_purchasing_places_over_12_places_returns_sorry(client, + get_credentials, + purchasing_over_12_places): + """ + Test that the 200 status code and an appropriate message are returned in case of purchasing more + than 12 places. + Args: + client (FlaskClient): A Flask client + get_credentials (dict): The credentials + purchasing_over_12_places (dict): The purchasing data + """ + client.post('/showSummary', data=get_credentials) + + purchasing_data = purchasing_over_12_places + + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) + + the_club = [club for club in utils.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 200 + assert "Over 12 places" in data + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data + assert f"Points available: {club_points}" in data + + @staticmethod + def test_purchasing_places_over_12_cumulative_places_returns_sorry(client, + get_credentials, + purchasing_13_cumulative_places): + """ + Test that the 200 status code and an appropriate message are returned in case of purchasing more + than 12 cumulative places. + Args: + client (FlaskClient): A Flask client + get_credentials (dict): The credentials + purchasing_13_cumulative_places (dict): The purchasing data + """ + client.post('/showSummary', data=get_credentials) + + purchasing_data = purchasing_13_cumulative_places + + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) + + the_club = [club for club in utils.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 200 + assert f"Welcome, {the_club["email"]} " in data + assert "Over 12 places" in data + assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data + assert f"Points available: {club_points}" in data + + @staticmethod + def test_purchasing_places_negative_number_returns_sorry(client, + get_credentials, + purchasing_with_negative_places): + """ + Test that the 200 status code and an appropriate message are returned in case of purchasing with + negative value of places. + Args: + client (FlaskClient): A Flask client + get_credentials (dict): The credentials + purchasing_with_negative_places (dict): The purchasing data + """ + client.post('/showSummary', data=get_credentials) + + purchasing_data = purchasing_with_negative_places + + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) + + the_club = [club for club in utils.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 200 + assert "Negative number" in data + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, you should type a positive number." in data + assert f"Points available: {club_points}" in data + + @staticmethod + def test_purchasing_places_past_competitions_returns_sorry(client, + get_credentials_2, + get_existing_competition_and_club_3): + """ + Test that the 200 status code and an appropriate message are returned in case of outdated + competition. + Args: + client (FlaskClient): A Flask client + get_credentials_2 (dict): The credentials + get_existing_competition_and_club_3 (dict): The competition and club + """ + client.post('/showSummary', data=get_credentials_2) + + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_3['competition'], + club=get_existing_competition_and_club_3['club'])) + + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 200 + assert "Outdated" in data + assert "Sorry, this competition is outdated. Booking not possible." in data + + @staticmethod + def test_purchasing_places_over_available_returns_sorry(client, + get_credentials, + purchasing_places_more_than_available): + """ + Test that the 200 status code and an appropriate message are returned in case of purchasing more + than places available in the competition. + Args: + client (FlaskClient): A Flask client + get_credentials (dict): The credentials + purchasing_places_more_than_available (dict): The purchasing data + """ + client.post('/showSummary', data=get_credentials) + + purchasing_data = purchasing_places_more_than_available + + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) + + the_club = [club for club in utils.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 200 + assert "Not enough places" in data + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, there are not enough places available for this competition." in data + assert f"Points available: {club_points}" in data + + @staticmethod + def test_purchasing_places_sold_out_status_code_error(client, + get_credentials_3, + get_existing_competition_and_club_4): + """ + Test that the 200 status code and an appropriate message are returned in case of purchasing in a + sold out competition. + Args: + client (FlaskClient): A Flask client + get_credentials_3 (dict): The credentials + get_existing_competition_and_club_4 (dict): The competition and club + """ + client.post('/showSummary', data=get_credentials_3) + + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_4['competition'], + club=get_existing_competition_and_club_4['club'])) + + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 200 + assert "Sold out" in data + assert "Sorry, this competition is sold out. Booking not possible." in data + + @staticmethod + def test_change_password_status_code_ok(client, + get_credentials_3, + get_existing_competition_and_club_4): + """ + Test that the 200 status code and appropriate information are returned in case of correct + changing password + Args: + client (FlaskClient): A Flask client + get_credentials_3 (dict): The credentials + get_existing_competition_and_club_4 (dict): The competition and club + """ + client.post('/showSummary', data=get_credentials_3) + + passwords = { + "password": "tr_nl_er4", + "confirm_password": "tr_nl_er4", + } + + client_response = client.post(f'/changePassword/{get_existing_competition_and_club_4["club"]}', + data=passwords) + + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 200 + assert "Great! You have successfully changed your password." in data + + @staticmethod + def test_change_password_match_fails(client, + get_credentials_3, + get_existing_competition_and_club_4): + """ + Test that the 406 status code and an error message are returned in case of incorrect + changing password. + Args: + client (FlaskClient): A Flask client + get_credentials_3 (dict): The credentials + get_existing_competition_and_club_4 (dict): The competition and club + """ + client.post('/showSummary', data=get_credentials_3) + + passwords = { + "password": "tr_Pl_er4", + "confirm_password": "tr_nl_er4", + } + + client_response = client.post(f'/changePassword/{get_existing_competition_and_club_4["club"]}', + data=passwords) + + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 200 + assert "Passwords not match" in data + assert "Sorry, passwords do not match." in data + + @staticmethod + def test_change_password_identical(client, + get_credentials_3, + get_existing_competition_and_club_4): + """ + Test that the 406 status code and an error message are returned in case of changing + password with identical password as current. + Args: + client (FlaskClient): A Flask client + get_credentials_3 (dict): The credentials + get_existing_competition_and_club_4 (dict): The competition and club + """ + client.post('/showSummary', data=get_credentials_3) + + passwords = { + "password": "tp4_Tmn40", + "confirm_password": "tp4_Tmn40", + } + + client_response = client.post(f'/changePassword/{get_existing_competition_and_club_4["club"]}', + data=passwords) + + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 200 + assert "Identical password" in data + assert "Sorry, you have to type a new different password." in data + + @staticmethod + def test_change_password_empty_field(client, + get_credentials_3, + get_existing_competition_and_club_4): + """ + Test that the 406 status code and an error message are returned in case of changing + password with at least one empty field. + Args: + client (FlaskClient): A Flask client + get_credentials_3 (dict): The credentials + get_existing_competition_and_club_4 (dict): The competition and club + """ + client.post('/showSummary', data=get_credentials_3) + + passwords = { + "password": "tp4_Tmn40", + "confirm_password": "", + } + + client_response = client.post(f'/changePassword/{get_existing_competition_and_club_4["club"]}', + data=passwords) + + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 200 + assert "Empty field(s)" in data + assert "Sorry, please fill all fields." in data diff --git a/tests/performance_tests/__init__.py b/tests/performance_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/performance_tests/clubs_tmp.json b/tests/performance_tests/clubs_tmp.json new file mode 100644 index 000000000..08a292177 --- /dev/null +++ b/tests/performance_tests/clubs_tmp.json @@ -0,0 +1,96 @@ +{ + "clubs": [ + { + "name": "Power Lift", + "email": "admin@powerlift.com", + "password": "scrypt:32768:8:1$c0ePLkJjwFO0xhpP$a9c637393de25e3f6b121ab38d3d9c3589d66879618157ea3382ae9ee2310c28265be341d4af02c4c1d1f81427ccdbc96464b0bb19697a819f5ad987764c17ad", + "points": "5" + }, + { + "name": "Titanium Tribe", + "email": "info@titaniumtribe.com", + "password": "scrypt:32768:8:1$4bM8hSwBdKH0Dj2n$d6d64d3a635cc260803451f8b0c9acc1e50bf3a48ab071e9389d9e492cce5131d6c011f841d92b7422bbfdfa6609e198ec529fc3394cb27788a1a696baba767e", + "points": "15" + }, + { + "name": "Steel Strength", + "email": "hello@steelstrength.org", + "password": "scrypt:32768:8:1$uGiMzIndzui0kLQp$50ef79e3db9134208a46072a6297637a5636d81ca10b9a42d69883c8a12ee021fdab58df6019339ebc8924312598c4d37a6d71ce8647177241ddf012c69acafc", + "points": "15" + }, + { + "name": "Heavy Hitters", + "email": "admin@heavyhitters.net", + "password": "scrypt:32768:8:1$U4IUVIbiypkLsiwd$b57afe44db3af13b7e9772560a228c1100f41c3222b0cbbaa5c8855632fbca004c7d2ca36d7bef52624ee028373a0cd90df65fb9b73d4eb119ad1edf3a741bf9", + "points": "15" + }, + { + "name": "Power Lifts Club", + "email": "admin@powerlifts.com", + "password": "scrypt:32768:8:1$zdBGCvw6xXxHzAGl$c4534f2b7b2089aeca6f50bf3221f86f885aa4139692844149becf3c56b37a3c8f6a5f7c47e8c4f60405d591f58cf8f9567d1e383396a5f4de87073d441e5fe3", + "points": "15" + }, + { + "name": "Iron Temple", + "email": "admin@irontemple.com", + "password": "scrypt:32768:8:1$rHMWqkkiOSz7QiYU$e1a798fc6d464425359fb999db786ffbe39f6059e195d6d22a15e3ed70be3b5630e1d828399ca241f9d94e109ee5b350067f14a8054079344f01495c73594021", + "points": "4" + }, + { + "name": "Barbell Warriors", + "email": "info@barbellwarriors.com", + "password": "scrypt:32768:8:1$ZmtTQFYQVRwhl8cK$d4abb1c57ba950bb082a6c963605af00a0e120f7b6d7346d2a9419efad6d7d5a78c7d15a32b57d4fc2eab10aa864977be0a3734c24f69bc6694cd9b224b57545", + "points": "15" + }, + { + "name": "Simply Lift", + "email": "john@simplylift.co", + "password": "scrypt:32768:8:1$cYYbKbko75gPN3N3$4883a2360abf24fcc9f8329aedfca168994222748933eff4d89c78e5764f0972fce4c478b5737ece1bd31a3d2a0f1a00586096ffbddb79809b9685eabc5e5f4e", + "points": "13" + }, + { + "name": "The Weightroom", + "email": "team@weightroom.com", + "password": "scrypt:32768:8:1$38I8jEaWVYS5jgcA$0b29c9aa0e7f52b2634516d46b312ba429d2748fd4d72973f83275d4ada39907a6bae0389d31a868acdd4b5b43363515186f86e5f7419f9253d092c94d2a9185", + "points": "15" + }, + { + "name": "Iron Titans", + "email": "contact@irontitans.com", + "password": "scrypt:32768:8:1$kRW7krsT6zGr4iLk$6a24553114aa57f8cf4297786bf6136fa1d6f5f9832c6157fb406eb4459fcde2a82ed2a64d890ebe5e9a6efe73376239c59a124c77b6bd79b371c84095496b78", + "points": "0", + "booked_places": { + "Spring Festival": "2", + "Regional Strength": "5", + "Winter Power": "2", + "Heavyweight First": "5", + "Powerlifting Open": "1" + } + }, + { + "name": "She Lifts", + "email": "kate@shelifts.co.uk", + "password": "scrypt:32768:8:1$E4fcXT3MQ7oVfc0w$cf31c4a6aec02d4532ffcae03670b12f20265036a3ae5ee941fae6d135ca2db5e406fe9e6c6eded7776e0c256f6d8970f2cbfb74ee50a1c7f9b49b1ad6819b40", + "points": "4", + "booked_places": { + "Winter Power": "2", + "Regional Strength": "1", + "Powerlifting Open": "2", + "Heavyweight First": "2", + "Spring Festival": "1" + } + }, + { + "name": "Olympic Lifters", + "email": "contact@olympiclifters.com", + "password": "scrypt:32768:8:1$CO1XjG2GYak7bGad$906886f7afc2f66ac120f12b263e195fd44283a3cf0a39f963722ca25fc62e0909629165ff184ab67e43dfb44b4ca87ec7448ce95bcd896af2d2c32a31ffa604", + "points": "9", + "booked_places": { + "Powerlifting Open": "1", + "Regional Strength": "2", + "Heavyweight First": "2", + "Spring Festival": "1" + } + } + ] +} \ No newline at end of file diff --git a/tests/performance_tests/competitions_tmp.json b/tests/performance_tests/competitions_tmp.json new file mode 100644 index 000000000..f110761c9 --- /dev/null +++ b/tests/performance_tests/competitions_tmp.json @@ -0,0 +1,76 @@ +{ + "competitions": [ + { + "name": "Fall Classic", + "date": "2020-10-22 13:30:00", + "number_of_places": "13", + "is_past": true + }, + { + "name": "Barbell Classic", + "date": "2025-04-15 10:00:00", + "number_of_places": "0", + "is_past": true + }, + { + "name": "Olympic Meet", + "date": "2025-05-20 10:30:00", + "number_of_places": "25", + "is_past": true + }, + { + "name": "Iron Challenge", + "date": "2025-10-22 09:30:00", + "number_of_places": "15", + "is_past": true + }, + { + "name": "Summer Stronger", + "date": "2026-05-30 18:23:40", + "number_of_places": "0", + "is_past": false + }, + { + "name": "Winter Power", + "date": "2026-06-26 12:16:00", + "number_of_places": "0", + "is_past": false + }, + { + "name": "Strength Showdown", + "date": "2026-07-10 14:00:00", + "number_of_places": "0", + "is_past": false + }, + { + "name": "Spring Festival", + "date": "2026-07-27 10:00:00", + "number_of_places": "21", + "is_past": false + }, + { + "name": "Regional Strength", + "date": "2026-09-12 15:00:00", + "number_of_places": "37", + "is_past": false + }, + { + "name": "Heavyweight First", + "date": "2026-11-08 13:00:00", + "number_of_places": "21", + "is_past": false + }, + { + "name": "Powerlifting Open", + "date": "2026-12-05 11:00:00", + "number_of_places": "56", + "is_past": false + }, + { + "name": "Lifting Championship", + "date": "2027-06-18 12:00:00", + "number_of_places": "0", + "is_past": false + } + ] +} \ No newline at end of file diff --git a/tests/performance_tests/locustfile.py b/tests/performance_tests/locustfile.py new file mode 100644 index 000000000..8f82d7248 --- /dev/null +++ b/tests/performance_tests/locustfile.py @@ -0,0 +1,99 @@ +import sys +import random +import json +import tempfile + +from locust import HttpUser, task, between + + +temp_clubs_file = tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix='.json') +temp_comps_file = tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix='.json') +json.dump({"clubs": []}, temp_clubs_file) +json.dump({"competitions": []}, temp_comps_file) +temp_clubs_file.close() +temp_comps_file.close() + +PATH = r"D:\WORK\Formation\Openclassrooms\Python\OpenClassrooms_Project_11\02_Repository\OpenClassrooms_Project_11" + +sys.path.insert(0, PATH) + + +import utils +import copy + +utils.get_clubs_path = lambda: temp_clubs_file.name +utils.get_competitions_path = lambda: temp_comps_file.name + +with open(utils.CLUBS_PATH) as f: + real_clubs = json.load(f)["clubs"] + +with open(utils.COMPETITIONS_PATH) as f: + real_competitions = json.load(f)["competitions"] + + +class TestPerfApp(HttpUser): + wait_time = between(1, 3) + + @staticmethod + def get_clubs(): + return copy.deepcopy(real_clubs) + + @staticmethod + def get_competitions(): + return copy.deepcopy(real_competitions) + + def on_start(self): + self.clubs = copy.deepcopy(real_clubs) + self.competitions = copy.deepcopy(real_competitions) + + utils.clubs = self.clubs + utils.competitions = self.competitions + + passwords_table = { + "Simply Lift": "tgl_Prn_C2", + "Iron Temple": "tgl_Prn_C3", + "Power Lift": "tgl_Prn_C4", + "She Lifts": "tgl_Prn_C5", + "Iron Titans": "tgl_Prn_C6", + "Barbell Warriors": "tgl_Prn_C7", + "Power Lifts Club": "tgl_Prn_C8", + "Steel Strength": "tgl_Prn_C9", + "The Weightroom": "tgl_Prn_C10", + "Olympic Lifters": "tgl_Prn_C11", + "Titanium Tribes": "tgl_Prn_C12", + "Heavy Hitters": "tgl_Prn_C13", + } + + self.club = random.choice(utils.clubs) + self.club["points"] = 60 + + self.client.post( + "/showSummary", + data={ + "email": self.club["email"], + "password": passwords_table[self.club["name"]] + } + ) + + @task(2) + def index_competitions(self): + self.client.get("/") + + @task(3) + def book_places(self): + competition = random.choice(utils.competitions) + + while competition.get("is_past"): + competition = random.choice(utils.competitions) + + competition["number_of_places"] = 60 + + self.client.post('/purchasePlaces', data={ + "competition": competition["name"], + "club": self.club["name"], + "places": "1" + }) + + @task(1) + def logout(self): + self.client.get('/logout') diff --git a/tests/unit_tests/__init__.py b/tests/unit_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_utils.py b/tests/unit_tests/test_utils.py new file mode 100644 index 000000000..3b955d391 --- /dev/null +++ b/tests/unit_tests/test_utils.py @@ -0,0 +1,371 @@ +import pytest +import utils +import exceptions + +from werkzeug.security import check_password_hash + + +class TestUnitUtils: + @pytest.fixture(autouse=True) + def setup(self, mocker, get_clubs, get_competitions): + mocker.patch('utils.clubs', get_clubs) + mocker.patch('utils.competitions', get_competitions) + mocker.patch('utils.save_clubs') + mocker.patch('utils.save_competitions') + + @staticmethod + def test_update_password_ok(get_credentials): + """ + Test that the password is updated correctly. + Args: + get_credentials (dict): The credentials + """ + the_club = next((c for c in utils.clubs if c['email'] == get_credentials["email"]), None) + the_club = utils.update_club_password(the_club, "tp5_Tmn50") + + assert check_password_hash(the_club['password'], "tp5_Tmn50") + + @staticmethod + def test_update_password_fails(get_credentials): + """ + Test that the password is updated wrongly. + Args: + get_credentials (dict): The credentials + """ + the_club = next((c for c in utils.clubs if c['email'] == get_credentials["email"]), None) + the_club = utils.update_club_password(the_club, "tp5_Tmn50") + + assert not check_password_hash(the_club['password'], "tp6_Tmn60") + + @staticmethod + def test_add_club_ok(get_new_club): + """ + Test that the club is added correctly. + Args: + get_new_club (dict): The new club + """ + utils.add_club(name=get_new_club["name"], + email=get_new_club["email"], + password=get_new_club["password"], + points=get_new_club["points"]) + + assert get_new_club in utils.clubs + + @staticmethod + def test_update_booked_places_ok(get_existing_competition_and_club_2): + """ + Test that the required places are booked correctly. + Args: + get_existing_competition_and_club_2 (dict): The competition and club + """ + club_name = get_existing_competition_and_club_2['club'] + club = next((c for c in utils.clubs if c['name'] == club_name), None) + competition_name = get_existing_competition_and_club_2['competition'] + utils.update_club_booked_places(club=club, + places=5, + competition_name=competition_name) + + assert club['booked_places'][competition_name] == str(5) + + @staticmethod + def test_update_booked_places_fails(get_existing_competition_and_club_2): + """ + Test that the required places are booked wrongly. + Args: + get_existing_competition_and_club_2 (dict): The competition and club + """ + club_name = get_existing_competition_and_club_2['club'] + club = next((c for c in utils.clubs if c['name'] == club_name), None) + competition_name = get_existing_competition_and_club_2['competition'] + utils.update_club_booked_places(club=club, + places=5, + competition_name=competition_name) + + assert not club['booked_places'][competition_name] == str(6) + + @staticmethod + def test_update_competition_available_places(get_existing_competition_and_club): + """ + Test that the competition available places are updated correctly. + Args: + get_existing_competition_and_club (dict): The competition and club + """ + competition_name = get_existing_competition_and_club['competition'] + competition = next((c for c in utils.competitions if c['name'] == competition_name), None) + places_available = int(competition['number_of_places']) + utils.update_competition_available_places(the_competition=competition, places=5) + + assert competition['number_of_places'] == str(places_available - 5) + + @staticmethod + def test_update_competition_available_places_fails(get_existing_competition_and_club): + """ + Test that the competition available places are updated wrongly. + Args: + get_existing_competition_and_club (dict): The competition and club + """ + competition_name = get_existing_competition_and_club['competition'] + competition = next((c for c in utils.competitions if c['name'] == competition_name), None) + places_available = int(competition['number_of_places']) + utils.update_competition_available_places(the_competition=competition, places=5) + + assert not competition['number_of_places'] == str(places_available - 4) + + @staticmethod + def test_signup_form_filled_out_ok(get_details): + """ + Test that the form is filled out correctly. + Args: + get_details (dict): The details + """ + name = get_details['name'] + email = get_details['email'] + password = get_details['password'] + password2 = get_details['password2'] + + response = utils.check_signup_all_fields_filled_out(name=name, + email=email, + password=password, + password2=password2) + + assert response + + @staticmethod + def test_signup_form_filled_out_fails(get_wrong_details): + """ + Test that the form is filled out wrongly. + Args: + get_wrong_details (dict): The details + """ + name = get_wrong_details['name'] + email = get_wrong_details['email'] + password = get_wrong_details['password'] + password2 = get_wrong_details['password2'] + + response = utils.check_signup_all_fields_filled_out(name=name, + email=email, + password=password, + password2=password2) + + assert not response + + @staticmethod + def test_login_form_filled_out_ok(get_details): + """ + Test that the form is filled out correctly. + Args: + get_details (dict): The details + """ + name = get_details['name'] + password = get_details['password'] + + response = utils.check_login_all_fields_filled_out(name=name, + password=password,) + + assert response + + @staticmethod + def test_login_form_filled_out_fails(get_wrong_details): + """ + Test that the form is filled out wrongly. + Args: + get_wrong_details (dict): The details + """ + name = get_wrong_details['name'] + password = get_wrong_details['password'] + + response = utils.check_login_all_fields_filled_out(name=name, + password=password) + + assert not response + + @staticmethod + def test_validate_places_with_negative(client, get_existing_competition_and_club): + """ + Test that checks places validation process with negative value. + Args: + client (FlaskClient): A Flask client + get_existing_competition_and_club (dict): The competition and club + """ + club = next( + (c for c in utils.clubs if c['name'] == get_existing_competition_and_club["club"]), None) + competition_name = next( + (c for c in utils.competitions if c['name'] == get_existing_competition_and_club["competition"]), None) + + try: + utils.validate_places(places_required=-2, + club=club, + the_competition=competition_name) + except exceptions.ValidationError as e: + assert e.message == "Sorry, you should type a positive number." + assert e.tag == "Negative number" + + @staticmethod + def test_validate_places_over_12(client, get_existing_competition_and_club): + """ + Test that checks places validation process with more than 12 places. + Args: + client (FlaskClient): A Flask client + get_existing_competition_and_club (dict): The competition and club + """ + club = next( + (c for c in utils.clubs if c['name'] == get_existing_competition_and_club["club"]), None) + competition = next( + (c for c in utils.competitions if c['name'] == get_existing_competition_and_club["competition"]), None) + try: + utils.validate_places(places_required=13, + club=club, + the_competition=competition) + except exceptions.ValidationError as e: + assert e.message == "Sorry, you are not allow to purchase more than 12 places for this competition." + assert e.tag == "Over 12 places" + + @staticmethod + def test_validate_places_requires_more(client, get_existing_competition_and_club_5): + """ + Test that checks places validation process with more than the number of places available. + Args: + client (FlaskClient): A Flask client + get_existing_competition_and_club_5 (dict): The competition and club + """ + club = next( + (c for c in utils.clubs if c['name'] == get_existing_competition_and_club_5["club"]), None) + competition = next( + (c for c in utils.competitions if c['name'] == get_existing_competition_and_club_5["competition"]), None) + try: + utils.validate_places(places_required=5, + club=club, + the_competition=competition) + except exceptions.ValidationError as e: + assert e.message == "Sorry, there are not enough places available for this competition." + assert e.tag == "Not enough places" + + @staticmethod + def test_validate_places_not_enough(client, get_existing_competition_and_club_6): + """ + Test that checks places validation process with not enough places. + Args: + client (FlaskClient): A Flask client + get_existing_competition_and_club_6 (dict): The competition and club + """ + club = next( + (c for c in utils.clubs if c['name'] == get_existing_competition_and_club_6["club"]), None) + competition = next( + (c for c in utils.competitions if c['name'] == get_existing_competition_and_club_6["competition"]), None) + + try: + utils.validate_places(places_required=6, + club=club, + the_competition=competition) + except exceptions.ValidationError as e: + assert e.message == "Sorry, you do not have enough points to purchase." + assert e.tag == "Not enough points" + + @staticmethod + def test_validate_past_competition(client, get_existing_competition_and_club_3): + """ + Test that checks that the competition validation process with an outdated competition. + Args: + client (FlaskClient): A Flask client + get_existing_competition_and_club_3 (dict): The competition and club + """ + competition = next( + (c for c in utils.competitions if c['name'] == get_existing_competition_and_club_3["competition"]), None) + + try: + utils.validate_competition(the_competition=competition) + except exceptions.ValidationError as e: + assert e.message == "Sorry, this competition is outdated. Booking not possible." + assert e.tag == "Outdated" + + @staticmethod + def test_validate_competition_sold_out(client, get_existing_competition_and_club_4): + """ + Test that the competition validation process with a sold out competition. + Args: + client (FlaskClient): A Flask client + get_existing_competition_and_club_4 (dict): The competition and club + """ + competition = next( + (c for c in utils.competitions if c['name'] == get_existing_competition_and_club_4["competition"]), None) + + try: + utils.validate_competition(the_competition=competition) + except exceptions.ValidationError as e: + assert e.message == "Sorry, this competition is sold out. Booking not possible." + assert e.tag == "Sold out" + + @staticmethod + def test_validate_profile_fields_fails(client, get_wrong_details): + """ + Tests the profile validation process with wrong profile fields. + Args: + client (FlaskClient): A Flask client + get_wrong_details (dict): The details for the signup. + """ + try: + utils.validate_profile_fields(get_wrong_details["name"], + get_wrong_details["email"], + get_wrong_details["password"], + get_wrong_details["password2"]) + + except exceptions.ValidationError as e: + + assert e.message == "Sorry, please fill all fields." + assert e.tag == "Empty field(s)" + + @staticmethod + def test_validate_login_fields_fails(client, get_wrong_details): + """ + Tests the login validation process with wrong login fields. + Args: + client (FlaskClient): A Flask client + get_wrong_details (dict): The details for the login. + """ + try: + utils.validate_login_fields(get_wrong_details["email"], get_wrong_details["password"]) + except exceptions.ValidationError as e: + + assert e.message == "Sorry, please fill all fields." + assert e.tag == "Empty field(s)" + + @staticmethod + def test_validate_email_format_ok(client, get_details): + """ + Tests the email format validation process with correct format. + Args: + client (FlaskClient): A Flask client + get_details (dict): The details for the email field. + """ + try: + utils.validate_email_format(get_details["email"]) + assert True + + except exceptions.ValidationError as e: + assert e.message == "Sorry, the e-mail address you entered has invalid format." + assert e.tag == "Invalid email format" + + @staticmethod + def test_validate_email_format_fails(client, get_wrong_details): + """ + Tests the email format validation process with incorrect format. + Args: + client (FlaskClient): A Flask client + get_wrong_details (dict): The details for the email field. + """ + try: + utils.validate_email_format(get_wrong_details["email"]) + + except exceptions.ValidationError as e: + assert e.message == "Sorry, the e-mail address you entered has invalid format." + assert e.tag == "Invalid email format" + + @staticmethod + def test_copy_clubs_for_board(): + """ + Tests the club copy validation process. + """ + copy_clubs = utils.copy_clubs_for_board() + + assert len(copy_clubs) == len(utils.clubs) + assert "color" in copy_clubs[0].keys() diff --git a/tests/unit_tests/test_views.py b/tests/unit_tests/test_views.py new file mode 100644 index 000000000..9a1a6ddc7 --- /dev/null +++ b/tests/unit_tests/test_views.py @@ -0,0 +1,294 @@ +import pytest +import utils + +from flask import url_for + + +class TestUnitViews: + @pytest.fixture(autouse=True) + def setup(self, mocker, get_clubs, get_competitions): + mocker.patch('utils.clubs', get_clubs) + mocker.patch('utils.competitions', get_competitions) + mocker.patch('utils.save_clubs') + mocker.patch('utils.save_competitions') + + @staticmethod + def test_index_status_code_ok(client): + """ + Test that the index page status code is 200 + Args: + client (FlaskClient): A Flask client + """ + client_response = client.get('/') + assert client_response.status_code == 200 + + @staticmethod + def test_index_return_welcome(client): + """ + Test that the index page is correctly return with appropriate information + Args: + client (FlaskClient): A Flask client + """ + client_response = client.get('/') + data = client_response.data.decode('utf-8') + + assert "Welcome to the GÜDLFT Portal!" in data + assert ( + 'Please enter your secretary email and your password to continue ' + 'or sign up' + ) in data + assert "Email:" in data + assert "Password:" in data + + @staticmethod + def test_index_without_authentication_fails(client, get_credentials): + """ + Test that the index page is returned with an error message and with 403 status code in case + of non authentication + Args: + client (FlaskClient): A Flask client + get_credentials (dict): The credentials + """ + club = next((c for c in utils.clubs if c['email'] == get_credentials["email"]), None) + client_response = client.get(f'/showSummary/{club["name"]}') + assert client_response.status_code == 403 + assert "Not allow" in client_response.data.decode('utf-8') + + @staticmethod + def test_index_mail_authentication_ok(client, get_credentials): + """ + Test that the summary page is returned with a 200 status code in case of fine + authentication + Args: + get_credentials (dict): The credentials + client (FlaskClient): A Flask client + """ + client_response = client.post('/showSummary', data=get_credentials) + assert client_response.status_code == 200 + + @staticmethod + def test_index_mail_authentication_fails(client, get_bad_credentials): + """ + Test that the summary page is returned with a 200 status code in case of fine + authentication + Args: + get_bad_credentials (dict): The credentials + client (FlaskClient): A Flask client + """ + client_response = client.post('/showSummary', data=get_bad_credentials) + data = client_response.data.decode('utf-8') + assert client_response.status_code == 403 + assert "Sorry, the password is incorrect." in data + + @staticmethod + def test_index_mail_authentication_returns_summary(client, get_credentials): + """ + Test that the summary page is returned with appropriate information from the club when + authenticated + Args: + client (FlaskClient): A Flask client + get_credentials (dict): The credentials + """ + client_response = client.post('/showSummary', data=get_credentials) + data = client_response.data.decode('utf-8') + + assert "Welcome, kate@shelifts.co.uk" in data + assert "Spring Festival" in data + assert "Fall Classic" in data + assert "Points available: 12" in data + + @staticmethod + def test_index_mail_authentication_not_found(client, get_unexisting_credentials): + """ + Test that the index page is returned with an error message and with 302 status code in case + of authentication with unknown mail. + Args: + client (FlaskClient): A Flask client + get_unexisting_credentials (dict): The credentials + """ + client_response = client.post('/showSummary', data=get_unexisting_credentials) + data = client_response.data.decode('utf-8') + assert client_response.status_code == 404 + assert "Sorry, that email was not found." in data + + @staticmethod + def test_summary_logout_redirect_status_code_ok(client, get_credentials): + """ + Test that the redirect (302) status code is returned in case of logging out. + Args: + client (FlaskClient): A Flask client + get_credentials (dict): The credentials + """ + client.post('/showSummary', data=get_credentials) + logout_response = client.get('/logout') + assert logout_response.status_code == 302 + + @staticmethod + def test_booking_status_code_ok(client, + get_credentials, + get_existing_competition_and_club): + """ + Test that the 200 status code is returned in case of booking. + Args: + client (FlaskClient): A Flask client + get_credentials (dict): The credentials + get_existing_competition_and_club (dict): The competition and club + """ + client.post('/showSummary', data=get_credentials) + + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) + + assert client_response.status_code == 200 + + @staticmethod + def test_get_signup_status_code_ok(client): + """ + Test that the 200 status code is returned in case of signup. + Args: + client (FlaskClient): A Flask client + """ + client_response = client.get('/signUp') + assert client_response.status_code == 200 + + @staticmethod + def test_get_signup_returns_welcome(client): + """ + Test that the profile page is returned with appropriate information from the club when + signing up + Args: + client (FlaskClient): A Flask client + """ + client_response = client.get('/signUp') + data = client_response.data.decode('utf-8') + assert "Welcome to the GÜDLFT Portal!" in data + assert "Registration" in data + assert ("Club name:" in data) + assert ("Email:" in data) + assert ("Password:" in data) + assert ("Confirm Password:" in data) + + @staticmethod + def test_change_password_status_code_ok(client): + """ + Test that the 200 status code is returned in case of changing password + Args: + client (FlaskClient): A Flask client + """ + with client.session_transaction() as session: + session["club"] = "She Lifts" + + client_response = client.get('/changePassword/She Lifts') + assert client_response.status_code == 200 + + @staticmethod + def test_profile_ok(client): + """ + Test that the 200 status code is returned in case of displaying profile by the connected + club. + Args: + client (FlaskClient): A Flask client + """ + with client.session_transaction() as session: + session["club"] = "She Lifts" + + client_response = client.get('/profile/She Lifts') + assert client_response.status_code == 200 + + @staticmethod + def test_profile_without_authentication_fails(client): + """ + Test that the profile page is not accessible in case of non authentication. + 403 status code and an error message are returned. + Args: + client (FlaskClient): A Flask client + """ + client_response = client.get('/profile/Simply Lift') + assert client_response.status_code == 403 + assert "Not allow" in client_response.data.decode('utf-8') + + @staticmethod + def test_profile_returns_welcome(client): + """ + Test that the profile page is returned with appropriate information from the club when + authenticated. + Args: + client (FlaskClient): A Flask client + """ + with client.session_transaction() as session: + session["club"] = "She Lifts" + + client_response = client.get('/profile/She Lifts') + data = client_response.data.decode('utf-8') + assert "Welcome, kate@shelifts.co.uk" in data + assert "Profile" in data + assert "Name : She Lifts" in data + assert "Email : kate@shelifts.co.uk" in data + assert "Points available: 12" in data + + @staticmethod + def test_points_board_status_code_ok(client): + """ + Test that the 200 status code and appropriate information are returned in case of + displaying points board. + Args: + client (FlaskClient): A Flask client + """ + client_response = client.get('/pointsBoard') + data = client_response.data.decode('utf-8') + assert client_response.status_code == 200 + assert "Welcome to the GÜDLFT Portal!" in data + assert "⯈ Here is the board for all the clubs and their points." in data + + @staticmethod + def test_post_signup_status_code_ok(client, get_details_2): + """ + Test that the 200 status code is returned in case of signup. + Args: + client (FlaskClient): A Flask client + get_details_2 (dict): The details for the signup + """ + client_response = client.post('/profile', data=get_details_2, follow_redirects=True) + + assert client_response.status_code == 200 + + @staticmethod + def test_post_signup_returns_welcome(client, get_details_2): + """ + Test that the profile page is returned with appropriate information from the club in case + of signup + Args: + client (FlaskClient): A Flask client + get_details_2 (dict): The details for the signup + """ + client_response = client.post('/profile', data=get_details_2, follow_redirects=True) + + data = client_response.data.decode('utf-8') + + assert "Welcome, admin@test.com" in data + assert "Profile" in data + assert "Name : Name Test" in data + assert "Email : admin@test.com" in data + assert "Points available: 15" in data + + @staticmethod + def test_post_signup_fails(client, get_wrong_details_2): + """ + Test that the welcome page and an error message are returned in case of signup with empty + field(s). + Args: + client (FlaskClient): A Flask client + get_wrong_details_2 (dict): The details for the signup. + """ + client_response = client.post('/profile', data=get_wrong_details_2, follow_redirects=True) + + data = client_response.data.decode('utf-8') + + assert "Sorry, please fill all fields." in data + assert "Welcome to the GÜDLFT Portal!" in data + assert "Registration" in data + assert ("Club name:" in data) + assert ("Email:" in data) + assert ("Password:" in data) + assert ("Confirm Password:" in data) diff --git a/utils.py b/utils.py new file mode 100644 index 000000000..f30d6ab7b --- /dev/null +++ b/utils.py @@ -0,0 +1,303 @@ +import json +import os +import re +from datetime import datetime +from werkzeug.security import generate_password_hash, check_password_hash + +import exceptions + +DIR = r"D:\WORK\Formation\Openclassrooms\Python\OpenClassrooms_Project_11\02_Repository" +CLUBS_PATH = DIR + r"\OpenClassrooms_Project_11\clubs.json" +COMPETITIONS_PATH = DIR + r"\OpenClassrooms_Project_11\competitions.json" + + +def get_clubs_path() -> str: + """ + Method that returns the path to the clubs json file + Returns: + The path to the clubs json file + """ + path = os.environ.get("CLUBS_JSON") + + if not path: + path = CLUBS_PATH + return path + + +def get_competitions_path() -> str: + """ + Method that returns the path to the competitions json file + Returns: + The path to the competitions json file + """ + path = os.environ.get("COMPETITIONS_JSON") + + if not path: + path = COMPETITIONS_PATH + return path + + +def load_clubs() -> list: + """ + Method that loads the clubs json file + Returns: + The list of clubs + """ + path = CLUBS_PATH + + with open(path) as c: + return json.load(c)['clubs'] + + +def load_competitions() -> list: + """ + Method that loads the competitions json file + Returns: + The sorted list of competitions + """ + path = COMPETITIONS_PATH + + with open(path) as comps: + list_of_competitions = json.load(comps)['competitions'] + return sorted(list_of_competitions, key=lambda c: c['date']) + + +competitions = [] + +clubs = [] + + +def save_clubs() -> None: + """ + Method that saves the clubs json file + """ + path = get_clubs_path() + with open(path, 'w') as f: + list_of_clubs = {"clubs": clubs} + json.dump(list_of_clubs, f, indent=4) + + +def save_competitions() -> None: + """ + Method that saves the competitions json file + """ + path = get_competitions_path() + with open(path, 'w') as f: + list_of_competitions = {"competitions": sorted(competitions, key=lambda c: c['date'])} + json.dump(list_of_competitions, f, indent=4) + + +for competition in competitions: + competition['is_past'] = datetime.strptime(competition['date'], "%Y-%m-%d %H:%M:%S") < datetime.now() + + +def update_club_booked_places(club: dict, places: int, competition_name: str) -> None: + """ + Method that updates the club booked places based on the number of places + Args: + club (dict): The club dictionary + places (int): The number of places + competition_name (str): The name of the competition + """ + clubs.remove(club) + + club.setdefault("booked_places", {}) + current = int(club["booked_places"].get(competition_name, 0)) + club["booked_places"][competition_name] = str(current + places) + + club["points"] = str(int(club["points"]) - places) + + clubs.append(club) + save_clubs() + + +def update_competition_available_places(the_competition: dict, places: int) -> None: + """ + Method that updates the competition available places based on the number of places + Args: + the_competition (dict): The competition dictionary + places (int): The number of places + """ + competitions.remove(the_competition) + + the_competition['number_of_places'] = str(int(the_competition['number_of_places']) - places) + + competitions.append(the_competition) + + save_competitions() + + +def add_club(name: str, email: str, password: str, points: str) -> None: + """ + Method that adds a club to the clubs list + Args: + name (str): The name of the club + email (str): The email of the club + password (str): The password of the club + points (str): The points of the club in string format + """ + clubs.append({"name": name, "email": email, "password": password, "points": points}) + save_clubs() + + +def update_club_password(club: dict, password: str) -> dict: + """ + Method that updates the club password based on the password + Args: + club (dict): The club dictionary + password (str): The password of the club + + Returns: + The updated club + """ + hashed_password = generate_password_hash(password) + club["password"] = hashed_password + save_clubs() + return club + + +def check_signup_all_fields_filled_out(name: str, email: str, password: str, password2: str) -> bool: + """ + Method that checks if all the fields are filled out + Args: + name (str): The name of the club + email (str): The email of the club + password (str): The password of the club + password2 (str): The password of the club + + Returns: + A boolean indicating if all the fields are filled out + """ + if len(name) == 0 or len(email) == 0 or len(password) == 0 or len(password2) == 0: + return False + return True + + +def check_login_all_fields_filled_out(name: str, password: str) -> bool: + """ + Method that checks if all the fields are filled out + Args: + name (str): The name of the club + password (str): The password of the club + + Returns: + A boolean indicating if all the fields are filled out + """ + if len(name) == 0 or len(password) == 0: + return False + return True + + +def validate_places(places_required: int, club: dict, the_competition: dict) -> None: + """ + Method that validates the places required + Args: + places_required (int): The number of places required + club (dict): The club dictionary + the_competition (dict): The competition dictionary + """ + booked_places = int(club.get("booked_places", {}).get(the_competition["name"], 0)) + + cumulative_places = places_required + booked_places + + if places_required <= 0: + raise exceptions.NegativePlacesError() + + if cumulative_places > 12: + raise exceptions.Over12PlacesError() + + if places_required > int(the_competition['number_of_places']): + raise exceptions.NotEnoughPlacesError() + + if places_required > int(club['points']): + raise exceptions.NotEnoughPointsError() + + +def validate_competition(the_competition: dict) -> None: + """ + Method that validates the competition + Args: + the_competition (dict): The competition dictionary + """ + now = datetime.now() + + competition_date = datetime.strptime(the_competition['date'], '%Y-%m-%d %H:%M:%S') + + competition_places = int(the_competition['number_of_places']) + + if now > competition_date: + raise exceptions.OutdatedCompetitionError() + + if competition_places == 0: + raise exceptions.CompetitionNullPlacesError() + + +def validate_password(password: str, password2: str, club: dict) -> None: + """ + Method that validates the password + Args: + password (str): The password of the club. + password2 (str): The password of the club. + club (dict): The club dictionary. + """ + if len(password) == 0 or len(password2) == 0: + raise exceptions.EmptyFieldError() + + if password != password2: + raise exceptions.PasswordsNotMatchError() + + if check_password_hash(club['password'], password): + raise exceptions.PasswordNotDifferentError() + + +def validate_profile_fields(club_name: str, + club_email: str, + club_password: str, + club_password_confirmation: str) -> None: + """ + Method that validates the profile fields + Args: + club_name (str): The name of the club + club_email (str): The email of the club + club_password (str): The password of the club + club_password_confirmation (str): The password of the club + """ + if not check_signup_all_fields_filled_out(name=club_name, + email=club_email, + password=club_password, + password2=club_password_confirmation): + raise exceptions.EmptyFieldError() + + +def validate_login_fields(club_name: str, club_password: str) -> None: + """ + Method that validates the login page fields + Args: + club_name (str): The name of the club + club_password (str): The password of the club + """ + if not check_login_all_fields_filled_out(name=club_name, password=club_password): + raise exceptions.EmptyFieldError() + + +def validate_email_format(club_email: str) -> None: + """ + Method that validates the email format + Args: + club_email (str): The email of the club + """ + if not re.match(r'\b[A-Za-z0-9._+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', club_email): + raise exceptions.MailAddressInvalidFormatError() + + +def copy_clubs_for_board() -> list: + clubs_for_board = [] + for club in clubs: + club_copy = club.copy() + if clubs.index(club) % 2 == 0: + club_copy["color"] = "#cccccc" + else: + club_copy["color"] = "#aaaaaa" + clubs_for_board.append(club_copy) + + return clubs_for_board