diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000..8692df254a Binary files /dev/null and b/.coverage differ diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000..624291b553 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,13 @@ +[run] +omit = + */tests.py + */tests/* + */test_*.py + */tests_*.py + */migrations/* + */apps.py + */__init__.py + */admin.py + */asgi.py + */wsgi.py + */settings.py \ No newline at end of file diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml new file mode 100644 index 0000000000..e3062dfc63 --- /dev/null +++ b/.github/workflows/django.yml @@ -0,0 +1,52 @@ +name: OC Lettings site CI + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + +jobs: + ci: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10"] + env: + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run Lint + run: | + flake8 + - name: Upload flake8 report + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: flake8-report + path: flake8-report/ + if-no-files-found: warn + retention-days: 5 + - name: Run Tests + run: | + pytest + - name: Upload coverage report + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: cov_html + path: cov_html/ + if-no-files-found: warn + retention-days: 5 diff --git a/.gitignore b/.gitignore index b4405ebab4..b96a98b209 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,17 @@ +# Temp files **/__pycache__ *.pyc + +# Virtual environment venv +env + +# Idea +.idea + +# Database +backup.sqlite3 +oc-lettings-site_old.sqlite3 + +# .env file +.env \ No newline at end of file diff --git a/README.md b/README.md index c8547803f7..6546bc4914 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,188 @@ -## Résumé +# Django App - OpenClassrooms Project 13 +**Scale a Django application using a modular architecture** -Site web d'Orange County Lettings +--- -## Développement local +## DESCRIPTION -### Prérequis +This project was completed as part of the "Python Developer" path at OpenClassrooms. -- Compte GitHub avec accès en lecture à ce repository -- Git CLI -- SQLite3 CLI -- Interpréteur Python, version 3.6 ou supérieure +The goal was to scale a Django application using a modular architecture : -Dans le reste de la documentation sur le développement local, il est supposé que la commande `python` de votre OS shell exécute l'interpréteur Python ci-dessus (à moins qu'un environnement virtuel ne soit activé). +- Redesign of the modular architecture in the GitHub repository; +- Reduction of various technical debts on the project; +- Addition and deployment of a CI/CD pipeline; +- Application monitoring and error tracking via Sentry; +- Creation of the application's technical documentation using Read The Docs and Sphinx. -### macOS / Linux +The application must: -#### Cloner le repository +- allow the users to view available rentals and all the registered profiles. -- `cd /path/to/put/project/in` -- `git clone https://github.com/OpenClassrooms-Student-Center/Python-OC-Lettings-FR.git` +--- -#### Créer l'environnement virtuel +## PROJECT STRUCTURE +

+ +

-- `cd /path/to/Python-OC-Lettings-FR` -- `python -m venv venv` -- `apt-get install python3-venv` (Si l'étape précédente comporte des erreurs avec un paquet non trouvé sur Ubuntu) -- Activer l'environnement `source venv/bin/activate` -- Confirmer que la commande `python` exécute l'interpréteur Python dans l'environnement virtuel -`which python` -- Confirmer que la version de l'interpréteur Python est la version 3.6 ou supérieure `python --version` -- Confirmer que la commande `pip` exécute l'exécutable pip dans l'environnement virtuel, `which pip` -- Pour désactiver l'environnement, `deactivate` +--- -#### Exécuter le site +## INSTALLATION -- `cd /path/to/Python-OC-Lettings-FR` -- `source venv/bin/activate` -- `pip install --requirement requirements.txt` -- `python manage.py runserver` -- Aller sur `http://localhost:8000` dans un navigateur. -- Confirmer que le site fonctionne et qu'il est possible de naviguer (vous devriez voir plusieurs profils et locations). +- ### Clone the repository : -#### Linting +``` +git clone https://github.com/Tit-Co/OpenClassrooms_Project_13.git +``` -- `cd /path/to/Python-OC-Lettings-FR` -- `source venv/bin/activate` -- `flake8` +- ### Navigate into the project directory : + `cd OpenClassrooms_Project_13` -#### Tests unitaires +- ### Create a virtual environment and dependencies : -- `cd /path/to/Python-OC-Lettings-FR` -- `source venv/bin/activate` -- `pytest` +1. #### With [uv](https://docs.astral.sh/uv/) -#### Base de données + `uv` is an environment and dependencies manager. + + - #### Install environment and dependencies + + `uv sync` -- `cd /path/to/Python-OC-Lettings-FR` -- Ouvrir une session shell `sqlite3` -- Se connecter à la base de données `.open oc-lettings-site.sqlite3` -- Afficher les tables dans la base de données `.tables` -- Afficher les colonnes dans le tableau des profils, `pragma table_info(Python-OC-Lettings-FR_profile);` -- Lancer une requête sur la table des profils, `select user_id, favorite_city from - Python-OC-Lettings-FR_profile where favorite_city like 'B%';` -- `.quit` pour quitter +2. #### With pip -#### Panel d'administration + - #### Install the virtual env : -- Aller sur `http://localhost:8000/admin` -- Connectez-vous avec l'utilisateur `admin`, mot de passe `Abc1234!` + `python -m venv env` -### Windows + - #### Activate the virtual env : + `source env/bin/activate` in Git Bash on Windows or on macOS / Linux + Or + `env\Scripts\activate` on Windows -Utilisation de PowerShell, comme ci-dessus sauf : +3. #### With [Poetry](https://python-poetry.org/docs/) -- Pour activer l'environnement virtuel, `.\venv\Scripts\Activate.ps1` -- Remplacer `which ` par `(Get-Command ).Path` + `Poetry` is a tool for dependency management and packaging in Python. + + - #### Install the virtual env : + `py -3.10 -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` or `uv add -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 oc_lettings_site` +- Activate the virtual environment as described previously +- Create environment variables (to avoid to add raw Sentry key into the code) + - With Power Shell : + ``` + $env:SENTRY_KEY = "my_key" + ``` + - With Git Bash : + ``` + export SENTRY_KEY = "my_key" + ``` +- Launch the local server by typing the command : + - `python manage.py runserver` + +### Launching the APP + +- Finally, in a web browser, open the urls : + - [http://127.0.0.1:8000/](http://127.0.0.1:8000/) + - [http://127.0.0.1:8000/admin](http://127.0.0.1:8000/admin) + - for the admin panel (username: ```admin```, password: ```Abc1234!```) + +--- + +## APP EXAMPLES + +Here are some examples of the application execution. + +- Home page +

+ +

+ +- Lettings index +

+ +

+ +- Letting details +

+ +

+ +- Profiles index +

+ +

+ +- Profile details +

+ +

+ +--- + +## 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` +- The app code has a setup.cfg file that specify Flake 8 options as below : + ``` + format = html + htmldir = flake8-report + max-line-length = 99 + exclude = **/migrations/*,env,cov_html + ``` + +--- + +## TESTS COVERAGE WITH PYTEST + +- Coverage report +

+ + +

+ +- **Type the line below in the terminal to generate another coverage report with pytest** + + `pytest --cov=lettings --cov=profiles --cov=oc_lettings_site --cov-report=html:cov_html` + +--- + +![Python](https://img.shields.io/badge/python-3.10-blue.svg) +![License](https://img.shields.io/badge/license-MIT-green.svg) +![Coverage](https://img.shields.io/badge/coverage-100%25-red) + +--- + +## AUTHOR +**Name**: Nicolas MARIE +**Track**: Python Developer – OpenClassrooms +**Project 13 – Scale a Django application using a modular architecture – May 2026** diff --git a/config.py b/config.py new file mode 100644 index 0000000000..097854e7fc --- /dev/null +++ b/config.py @@ -0,0 +1,11 @@ +""" +Config module for environment variables +""" +import os + +from dotenv import load_dotenv + + +load_dotenv() + +SENTRY_KEY = os.getenv("SENTRY_KEY") diff --git a/cov_html/.gitignore b/cov_html/.gitignore new file mode 100644 index 0000000000..23006f1f32 --- /dev/null +++ b/cov_html/.gitignore @@ -0,0 +1 @@ +# Created by coverage.py diff --git a/cov_html/class_index.html b/cov_html/class_index.html new file mode 100644 index 0000000000..afefc90b74 --- /dev/null +++ b/cov_html/class_index.html @@ -0,0 +1,231 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 100% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.14.0, + created at 2026-05-23 08:26 +0200 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Fileclass statementsmissingexcluded coverage
lettings \ models.pyAddress 100 100%
lettings \ models.pyAddress.Meta 000 100%
lettings \ models.pyLetting 100 100%
lettings \ models.py(no class) 1700 100%
lettings \ urls.py(no class) 400 100%
lettings \ views.py(no class) 2800 100%
oc_lettings_site \ urls.py(no class) 400 100%
oc_lettings_site \ views.py(no class) 1200 100%
profiles \ models.pyProfile 100 100%
profiles \ models.py(no class) 600 100%
profiles \ urls.py(no class) 400 100%
profiles \ views.py(no class) 2800 100%
Total  10600 100%
+

+ 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 0000000000..6f871742cd --- /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 0000000000..8649f0475d 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 0000000000..35b4462211 --- /dev/null +++ b/cov_html/function_index.html @@ -0,0 +1,271 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 100% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.14.0, + created at 2026-05-23 08:26 +0200 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Filefunction statementsmissingexcluded coverage
lettings \ models.pyAddress.__str__ 100 100%
lettings \ models.pyLetting.__str__ 100 100%
lettings \ models.py(no function) 1700 100%
lettings \ urls.py(no function) 400 100%
lettings \ views.pyindex 900 100%
lettings \ views.pyletting 1300 100%
lettings \ views.py(no function) 600 100%
oc_lettings_site \ urls.py(no function) 400 100%
oc_lettings_site \ views.pyindex 800 100%
oc_lettings_site \ views.py(no function) 400 100%
profiles \ models.pyProfile.__str__ 100 100%
profiles \ models.py(no function) 600 100%
profiles \ urls.py(no function) 400 100%
profiles \ views.pyindex 900 100%
profiles \ views.pyprofile 1300 100%
profiles \ views.py(no function) 600 100%
Total  10600 100%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/cov_html/index.html b/cov_html/index.html new file mode 100644 index 0000000000..44c9860f27 --- /dev/null +++ b/cov_html/index.html @@ -0,0 +1,180 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 100% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.14.0, + created at 2026-05-23 08:26 +0200 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
File statementsmissingexcluded coverage
lettings \ models.py 1900 100%
lettings \ urls.py 400 100%
lettings \ views.py 2800 100%
oc_lettings_site \ urls.py 400 100%
oc_lettings_site \ views.py 1200 100%
profiles \ models.py 700 100%
profiles \ urls.py 400 100%
profiles \ views.py 2800 100%
Total 10600 100%
+

+ 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 0000000000..ba119c47df Binary files /dev/null and b/cov_html/keybd_closed_cb_900cfef5.png differ diff --git a/cov_html/status.json b/cov_html/status.json new file mode 100644 index 0000000000..d17d83f315 --- /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.14.0","globals":"5d68ca9844fe654e0fbee8d2e59249f4","files":{"z_a9b1b018a4ee155b_models_py":{"hash":"a5537957613848ba266b88176a8e0ab7","index":{"url":"z_a9b1b018a4ee155b_models_py.html","file":"lettings\\models.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":19,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_a9b1b018a4ee155b_urls_py":{"hash":"926e542cc4f7b882a38e94b89c64788b","index":{"url":"z_a9b1b018a4ee155b_urls_py.html","file":"lettings\\urls.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":4,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_a9b1b018a4ee155b_views_py":{"hash":"1c2d2f4ca195eef879fa15b7da0bfdf8","index":{"url":"z_a9b1b018a4ee155b_views_py.html","file":"lettings\\views.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":28,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_d8907effad88fb1e_urls_py":{"hash":"f5c03788ee38dd1b9a052320b7c8e410","index":{"url":"z_d8907effad88fb1e_urls_py.html","file":"oc_lettings_site\\urls.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":4,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_d8907effad88fb1e_views_py":{"hash":"f96e72908b5332539d929b2009283f82","index":{"url":"z_d8907effad88fb1e_views_py.html","file":"oc_lettings_site\\views.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":12,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_b4872cbce420d762_models_py":{"hash":"2de4f9efdcb0811adaf7246ef9369bbe","index":{"url":"z_b4872cbce420d762_models_py.html","file":"profiles\\models.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":7,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_b4872cbce420d762_urls_py":{"hash":"b9923831fb1c54710d4be641bd654bc2","index":{"url":"z_b4872cbce420d762_urls_py.html","file":"profiles\\urls.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":4,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_b4872cbce420d762_views_py":{"hash":"fccf0e7fd30f69d9db73ddbf8be1274f","index":{"url":"z_b4872cbce420d762_views_py.html","file":"profiles\\views.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":28,"n_excluded":0,"n_missing":0,"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 0000000000..5e304ce5f6 --- /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/cov_html/z_a9b1b018a4ee155b_models_py.html b/cov_html/z_a9b1b018a4ee155b_models_py.html new file mode 100644 index 0000000000..5448b1b187 --- /dev/null +++ b/cov_html/z_a9b1b018a4ee155b_models_py.html @@ -0,0 +1,155 @@ + + + + + Coverage for lettings\models.py: 100% + + + + + +
+
+

+ Coverage for lettings \ models.py: + 100% +

+ +

+ 19 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.14.0, + created at 2026-05-23 08:21 +0200 +

+ +
+
+
+

1""" 

+

2Models module for lettings app 

+

3""" 

+

4from django.db import models 

+

5from django.core.validators import MaxValueValidator, MinLengthValidator 

+

6 

+

7 

+

8class Address(models.Model): 

+

9 """ 

+

10 Address model for Lettings 

+

11 Attributes: 

+

12 number (int): Letting number 

+

13 street (str): Street address 

+

14 city (str): City address 

+

15 state (str): State address 

+

16 zip_code (int): Zip code 

+

17 country_iso_code (int): Country code 

+

18 """ 

+

19 number = models.PositiveIntegerField(validators=[MaxValueValidator(9999)]) 

+

20 street = models.CharField(max_length=64) 

+

21 city = models.CharField(max_length=64) 

+

22 state = models.CharField(max_length=2, validators=[MinLengthValidator(2)]) 

+

23 zip_code = models.PositiveIntegerField(validators=[MaxValueValidator(99999)]) 

+

24 country_iso_code = models.CharField(max_length=3, validators=[MinLengthValidator(3)]) 

+

25 

+

26 class Meta: 

+

27 """ 

+

28 Meta class for Lettings to specify verbose names 

+

29 """ 

+

30 verbose_name = "Address" 

+

31 verbose_name_plural = "Addresses" 

+

32 

+

33 def __str__(self) -> str: 

+

34 """ 

+

35 string method for Lettings 

+

36 Returns: 

+

37 A f-string with number and street address 

+

38 """ 

+

39 return f'{self.number} {self.street}' 

+

40 

+

41 

+

42class Letting(models.Model): 

+

43 """ 

+

44 Letting model for Lettings 

+

45 Attributes: 

+

46 title (str): Letting title 

+

47 address (Address): Letting address 

+

48 """ 

+

49 title = models.CharField(max_length=256) 

+

50 address = models.OneToOneField(Address, on_delete=models.CASCADE) 

+

51 

+

52 def __str__(self) -> str: 

+

53 """ 

+

54 String method for Lettings 

+

55 Returns: 

+

56 A f-string with letting title 

+

57 """ 

+

58 return self.title 

+
+ + + diff --git a/cov_html/z_a9b1b018a4ee155b_urls_py.html b/cov_html/z_a9b1b018a4ee155b_urls_py.html new file mode 100644 index 0000000000..21e9fb25a8 --- /dev/null +++ b/cov_html/z_a9b1b018a4ee155b_urls_py.html @@ -0,0 +1,110 @@ + + + + + Coverage for lettings\urls.py: 100% + + + + + +
+
+

+ Coverage for lettings \ urls.py: + 100% +

+ +

+ 4 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.14.0, + created at 2026-05-23 08:21 +0200 +

+ +
+
+
+

1""" 

+

2URLs module for lettings app 

+

3""" 

+

4from django.urls import path 

+

5 

+

6from . import views 

+

7 

+

8app_name = 'lettings' 

+

9 

+

10urlpatterns = [ 

+

11 path('', views.index, name='index'), 

+

12 path('<int:letting_id>/', views.letting, name='letting'), 

+

13] 

+
+ + + diff --git a/cov_html/z_a9b1b018a4ee155b_views_py.html b/cov_html/z_a9b1b018a4ee155b_views_py.html new file mode 100644 index 0000000000..4299ecb42f --- /dev/null +++ b/cov_html/z_a9b1b018a4ee155b_views_py.html @@ -0,0 +1,183 @@ + + + + + Coverage for lettings\views.py: 100% + + + + + +
+
+

+ Coverage for lettings \ views.py: + 100% +

+ +

+ 28 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.14.0, + created at 2026-05-23 08:21 +0200 +

+ +
+
+
+

1""" 

+

2Views module for lettings app 

+

3""" 

+

4from django.http import HttpRequest, HttpResponse 

+

5from django.shortcuts import render 

+

6 

+

7from .models import Letting 

+

8from monitoring import logger 

+

9 

+

10 

+

11# Aenean leo magna, vestibulum et tincidunt fermentum, consectetur quis velit. Sed non placerat 

+

12# massa. Integer est nunc, pulvinar a tempor et, bibendum id arcu. Vestibulum ante ipsum primis in 

+

13# faucibus orci luctus et ultrices posuere cubilia curae; Cras eget scelerisque 

+

14def index(request: HttpRequest) -> HttpResponse: 

+

15 """ 

+

16 View function for lettings index page 

+

17 Args: 

+

18 request (HttpRequest): Http Request object 

+

19 

+

20 Returns: 

+

21 An HTTP response with the list of lettings or an HTTP response with 500 error. 

+

22 """ 

+

23 try: 

+

24 lettings_list = Letting.objects.all() 

+

25 context = {'lettings_list': lettings_list} 

+

26 

+

27 logger.info(f"Going to lettings index page : {context=}, status = 200.") 

+

28 

+

29 return render(request, template_name='lettings/index.html', context=context, status=200) 

+

30 

+

31 except Exception as e: 

+

32 context = {"error": str(e)} 

+

33 

+

34 logger.error(f"Error 500 returned while reaching lettings index page : {context=}," 

+

35 f" status = 500.") 

+

36 

+

37 return render(request, template_name='error_500.html', context=context, status=500) 

+

38 

+

39 

+

40# Cras ultricies dignissim purus, vitae hendrerit ex varius non. In accumsan porta nisl id 

+

41# eleifend. Praesent dignissim, odio eu consequat pretium, purus urna vulputate arcu, vitae 

+

42# efficitur lacus justo nec purus. Aenean finibus faucibus lectus at porta. Maecenas auctor, est ut 

+

43# luctus congue, dui enim mattis enim, ac condimentum velit libero in magna. Suspendisse potenti. 

+

44# In tempus a nisi sed laoreet. Suspendisse porta dui eget sem accumsan interdum. Ut quis urna 

+

45# pellentesque justo mattis ullamcorper ac non tellus. In tristique mauris eu velit fermentum, 

+

46# tempus pharetra est luctus. Vivamus consequat aliquam libero, eget bibendum lorem. Sed non dolor 

+

47# risus. Mauris condimentum auctor elementum. Donec quis nisi ligula. Integer vehicula tincidunt 

+

48# enim, ac lacinia augue pulvinar sit amet. 

+

49def letting(request: HttpRequest, letting_id: int) -> HttpResponse: 

+

50 """ 

+

51 View function for letting detail page 

+

52 Args: 

+

53 request (HttpRequest): Http Request object 

+

54 letting_id (int): letting id 

+

55 

+

56 Returns: 

+

57 An HTTP response with the letting detail or an HTTP response with 404 error if not found 

+

58 or an HTTP response with 500 error 

+

59 """ 

+

60 try: 

+

61 letting = Letting.objects.get(id=letting_id) 

+

62 

+

63 context = { 

+

64 'title': letting.title, 

+

65 'address': letting.address, 

+

66 } 

+

67 

+

68 logger.info(f"Going to lettings details page : {context=}, status = 200.") 

+

69 

+

70 return render(request, template_name='lettings/letting.html', context=context, status=200) 

+

71 

+

72 except Letting.DoesNotExist as e: 

+

73 context = {"type": "letting", "id": letting_id, "error": str(e)} 

+

74 

+

75 logger.warning(f"Error 404 returned while reaching letting n°{letting_id} : {context=}," 

+

76 f" status = 404.") 

+

77 

+

78 return render(request, template_name='error_404.html', context=context, status=404) 

+

79 

+

80 except Exception as e: 

+

81 context = {"error": str(e)} 

+

82 

+

83 logger.error(f"Error 500 returned while reaching letting details page : {context=}," 

+

84 f" status = 500.") 

+

85 

+

86 return render(request, template_name='error_500.html', context=context, status=500) 

+
+ + + diff --git a/cov_html/z_b4872cbce420d762_models_py.html b/cov_html/z_b4872cbce420d762_models_py.html new file mode 100644 index 0000000000..1adad65e95 --- /dev/null +++ b/cov_html/z_b4872cbce420d762_models_py.html @@ -0,0 +1,121 @@ + + + + + Coverage for profiles\models.py: 100% + + + + + +
+
+

+ Coverage for profiles \ models.py: + 100% +

+ +

+ 7 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.14.0, + created at 2026-05-23 08:21 +0200 +

+ +
+
+
+

1""" 

+

2Models module for profiles app 

+

3""" 

+

4from django.db import models 

+

5from django.contrib.auth.models import User 

+

6 

+

7 

+

8class Profile(models.Model): 

+

9 """ 

+

10 Models class for profiles app 

+

11 Attributes: 

+

12 user (User): user 

+

13 favorite_city (str): The favorite city of the user 

+

14 """ 

+

15 user = models.OneToOneField(User, on_delete=models.CASCADE) 

+

16 favorite_city = models.CharField(max_length=64, blank=True) 

+

17 

+

18 def __str__(self) -> str: 

+

19 """ 

+

20 String method for profile model 

+

21 Returns: 

+

22 The user name of the profile 

+

23 """ 

+

24 return self.user.username 

+
+ + + diff --git a/cov_html/z_b4872cbce420d762_urls_py.html b/cov_html/z_b4872cbce420d762_urls_py.html new file mode 100644 index 0000000000..fca767902a --- /dev/null +++ b/cov_html/z_b4872cbce420d762_urls_py.html @@ -0,0 +1,110 @@ + + + + + Coverage for profiles\urls.py: 100% + + + + + +
+
+

+ Coverage for profiles \ urls.py: + 100% +

+ +

+ 4 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.14.0, + created at 2026-05-23 08:21 +0200 +

+ +
+
+
+

1""" 

+

2URLs module for profiles app 

+

3""" 

+

4from django.urls import path 

+

5 

+

6from . import views 

+

7 

+

8app_name = 'profiles' 

+

9 

+

10urlpatterns = [ 

+

11 path('', views.index, name='index'), 

+

12 path('<str:username>/', views.profile, name='profile'), 

+

13] 

+
+ + + diff --git a/cov_html/z_b4872cbce420d762_views_py.html b/cov_html/z_b4872cbce420d762_views_py.html new file mode 100644 index 0000000000..e37e2028f8 --- /dev/null +++ b/cov_html/z_b4872cbce420d762_views_py.html @@ -0,0 +1,174 @@ + + + + + Coverage for profiles\views.py: 100% + + + + + +
+
+

+ Coverage for profiles \ views.py: + 100% +

+ +

+ 28 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.14.0, + created at 2026-05-23 08:21 +0200 +

+ +
+
+
+

1""" 

+

2Views module for profiles app 

+

3""" 

+

4from django.http import HttpRequest, HttpResponse 

+

5from django.shortcuts import render 

+

6from monitoring import logger 

+

7 

+

8from profiles.models import Profile 

+

9 

+

10 

+

11# Sed placerat quam in pulvinar commodo. Nullam laoreet consectetur ex, sed consequat libero 

+

12# pulvinar eget. Fusc faucibus, urna quis auctor pharetra, massa dolor cursus neque, quis dictum 

+

13# lacus d 

+

14def index(request: HttpRequest) -> HttpResponse: 

+

15 """ 

+

16 View function for profiles index page 

+

17 Args: 

+

18 request (HttpRequest): request object 

+

19 

+

20 Returns: 

+

21 An HTTP response with the list of profiles or HTTP response with 500 error. 

+

22 """ 

+

23 try: 

+

24 profiles_list = Profile.objects.all() 

+

25 context = {'profiles_list': profiles_list} 

+

26 

+

27 logger.info(f"Going to profiles index page : {context=}, status = 200.") 

+

28 

+

29 return render(request, template_name='profiles/index.html', context=context, status=200) 

+

30 

+

31 except Exception as e: 

+

32 context = {"error": str(e)} 

+

33 

+

34 logger.error(f"Error 500 returned while reaching profiles index page : {context=}" 

+

35 f", status = 500.") 

+

36 

+

37 return render(request, template_name='error_500.html', context=context, status=500) 

+

38 

+

39 

+

40# Aliquam sed metus eget nisi tincidunt ornare accumsan eget lac 

+

41# laoreet neque quis, pellentesque dui. Nullam facilisis pharetra vulputate. Sed tincidunt, dolor 

+

42# id facilisis fringilla, eros leo tristique lacus, it. Nam aliquam dignissim congue. Pellentesque 

+

43# habitant morbi tristique senectus et netus et males 

+

44def profile(request: HttpRequest, username: str): 

+

45 """ 

+

46 View function for profile details page 

+

47 Args: 

+

48 request (HttpRequest): request object 

+

49 username (str): username 

+

50 

+

51 Returns: 

+

52 An HTTP response with the profile or HTTP response with 404 error if not found 

+

53 or an HTTP response with 500 error 

+

54 """ 

+

55 try: 

+

56 profile = Profile.objects.get(user__username=username) 

+

57 context = {'profile': profile} 

+

58 

+

59 logger.info(f"Going to profile details page : {context=}, status = 200.") 

+

60 

+

61 return render(request, template_name='profiles/profile.html', context=context, status=200) 

+

62 

+

63 except Profile.DoesNotExist as e: 

+

64 context = {"type": "profile", "name": username, "error": str(e)} 

+

65 

+

66 logger.warning(f"Error 404 returned while reaching profile {username} : {context=}," 

+

67 f" status = 404.") 

+

68 

+

69 return render(request, template_name='error_404.html', context=context, status=404) 

+

70 

+

71 except Exception as e: 

+

72 context = {"error": str(e)} 

+

73 

+

74 logger.error(f"Error 500 returned while reaching profile details page : {context=}," 

+

75 f" status = 500.") 

+

76 

+

77 return render(request, template_name='error_500.html', context=context, status=500) 

+
+ + + diff --git a/cov_html/z_d8907effad88fb1e_urls_py.html b/cov_html/z_d8907effad88fb1e_urls_py.html new file mode 100644 index 0000000000..17ddcba14f --- /dev/null +++ b/cov_html/z_d8907effad88fb1e_urls_py.html @@ -0,0 +1,112 @@ + + + + + Coverage for oc_lettings_site\urls.py: 100% + + + + + +
+
+

+ Coverage for oc_lettings_site \ urls.py: + 100% +

+ +

+ 4 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.14.0, + created at 2026-05-23 08:21 +0200 +

+ +
+
+
+

1""" 

+

2URLs module for oc_lettings_site app. 

+

3Include the lettings and profiles apps urls 

+

4""" 

+

5from django.contrib import admin 

+

6from django.urls import path, include 

+

7 

+

8from . import views 

+

9 

+

10urlpatterns = [ 

+

11 path('', views.index, name='index'), 

+

12 path('lettings/', include('lettings.urls')), 

+

13 path('profiles/', include('profiles.urls')), 

+

14 path('admin/', admin.site.urls), 

+

15] 

+
+ + + diff --git a/cov_html/z_d8907effad88fb1e_views_py.html b/cov_html/z_d8907effad88fb1e_views_py.html new file mode 100644 index 0000000000..74dfe7f33b --- /dev/null +++ b/cov_html/z_d8907effad88fb1e_views_py.html @@ -0,0 +1,134 @@ + + + + + Coverage for oc_lettings_site\views.py: 100% + + + + + +
+
+

+ Coverage for oc_lettings_site \ views.py: + 100% +

+ +

+ 12 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.14.0, + created at 2026-05-23 08:21 +0200 +

+ +
+
+
+

1""" 

+

2Views module for oc_lettings_site app 

+

3""" 

+

4from django.http import HttpRequest, HttpResponse 

+

5from django.shortcuts import render 

+

6 

+

7from monitoring import init_sentry, logger 

+

8 

+

9 

+

10# Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque molestie quam lobortis leo 

+

11# consectetur ullamcorper non id est. Praesent dictum, nulla eget feugiat sagittis, sem mi 

+

12# convallis eros, vitae dapibus nisi lorem dapibus sem. Maecenas pharetra purus ipsum, eget 

+

13# consequat ipsum lobortis quis. Phasellus eleifend ex auctor venenatis tempus. Aliquam vitae erat 

+

14# ac orci placerat luctus. Nullam elementum urna nisi, pellentesque iaculis enim cursus in. 

+

15# Praesent volutpat porttitor magna, non finibus neque cursus id. 

+

16def index(request: HttpRequest) -> HttpResponse: 

+

17 """ 

+

18 View function for home page 

+

19 Args: 

+

20 request (HttpRequest): Http Request object 

+

21 

+

22 Returns: 

+

23 An HTTP response with index page or HTTP response with 500 error. 

+

24 """ 

+

25 init_sentry() 

+

26 try: 

+

27 logger.info(f"Going to home page : status = 200.") 

+

28 

+

29 return render(request, template_name='index.html') 

+

30 

+

31 except Exception as e: 

+

32 context = {'error': str(e)} 

+

33 

+

34 logger.error(f"Error 500 returned while reaching home page : {context=}" 

+

35 f", status = 500.") 

+

36 

+

37 return render(request, template_name='error_500.html', context=context) 

+
+ + + diff --git a/docs/cov_report_1.png b/docs/cov_report_1.png new file mode 100644 index 0000000000..8da9ced1d2 Binary files /dev/null and b/docs/cov_report_1.png differ diff --git a/docs/cov_report_2.png b/docs/cov_report_2.png new file mode 100644 index 0000000000..58c9a2c22e Binary files /dev/null and b/docs/cov_report_2.png differ diff --git a/docs/flake8_report.png b/docs/flake8_report.png new file mode 100644 index 0000000000..3736cee381 Binary files /dev/null and b/docs/flake8_report.png differ diff --git a/docs/screenshots/home_page_screenshot.png b/docs/screenshots/home_page_screenshot.png new file mode 100644 index 0000000000..798f0950ee Binary files /dev/null and b/docs/screenshots/home_page_screenshot.png differ diff --git a/docs/screenshots/letting_details_screenshot.png b/docs/screenshots/letting_details_screenshot.png new file mode 100644 index 0000000000..ab0dbd83bb Binary files /dev/null and b/docs/screenshots/letting_details_screenshot.png differ diff --git a/docs/screenshots/lettings_list_screenshot.png b/docs/screenshots/lettings_list_screenshot.png new file mode 100644 index 0000000000..21f41d766f Binary files /dev/null and b/docs/screenshots/lettings_list_screenshot.png differ diff --git a/docs/screenshots/profile_details_screenshot.png b/docs/screenshots/profile_details_screenshot.png new file mode 100644 index 0000000000..57ca31328d Binary files /dev/null and b/docs/screenshots/profile_details_screenshot.png differ diff --git a/docs/screenshots/profiles_list_screenshot.png b/docs/screenshots/profiles_list_screenshot.png new file mode 100644 index 0000000000..835b72dbdd Binary files /dev/null and b/docs/screenshots/profiles_list_screenshot.png differ diff --git a/docs/structure.png b/docs/structure.png new file mode 100644 index 0000000000..af2dd62183 Binary files /dev/null and b/docs/structure.png differ diff --git a/flake8-report/back.svg b/flake8-report/back.svg new file mode 100644 index 0000000000..ce80d2e6da --- /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 0000000000..98706cfe53 --- /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 0000000000..195631a111 --- /dev/null +++ b/flake8-report/index.html @@ -0,0 +1,30 @@ + + + + flake8 violations + + + + +
+
+

flake8 violations

+

Generated on 2026-05-22 21:12 + with mccabe: 0.6.1, pycodestyle: 2.5.0, pyflakes: 2.1.1 +

+
    + +
  • +
    + + + +

    All good!

    +

    No flake8 errors found in 32 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 0000000000..6e0e447a6e --- /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/lettings/__init__.py b/lettings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lettings/admin.py b/lettings/admin.py new file mode 100644 index 0000000000..2b5abe26f8 --- /dev/null +++ b/lettings/admin.py @@ -0,0 +1,10 @@ +""" +Admin registration module for lettings app +""" +from django.contrib import admin + +from lettings.models import Address, Letting + + +admin.site.register(Letting) +admin.site.register(Address) diff --git a/lettings/apps.py b/lettings/apps.py new file mode 100644 index 0000000000..9a30d0d289 --- /dev/null +++ b/lettings/apps.py @@ -0,0 +1,13 @@ +""" +Namespace module for lettings app +""" +from django.apps import AppConfig + + +class LettingsConfig(AppConfig): + """ + Namespace class for Lettings + Attributes: + name: namespace of the lettings app + """ + name = 'lettings' diff --git a/lettings/migrations/0001_initial.py b/lettings/migrations/0001_initial.py new file mode 100644 index 0000000000..9a008d3d77 --- /dev/null +++ b/lettings/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 3.0 on 2026-05-18 12:53 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Address', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('number', models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(9999)])), + ('street', models.CharField(max_length=64)), + ('city', models.CharField(max_length=64)), + ('state', models.CharField(max_length=2, validators=[django.core.validators.MinLengthValidator(2)])), + ('zip_code', models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(99999)])), + ('country_iso_code', models.CharField(max_length=3, validators=[django.core.validators.MinLengthValidator(3)])), + ], + ), + migrations.CreateModel( + name='Letting', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=256)), + ('address', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='new_letting', to='lettings.Address')), + ], + ), + ] diff --git a/lettings/migrations/0002_auto_20260518_1543.py b/lettings/migrations/0002_auto_20260518_1543.py new file mode 100644 index 0000000000..47cb6b8db2 --- /dev/null +++ b/lettings/migrations/0002_auto_20260518_1543.py @@ -0,0 +1,48 @@ +# Generated by Django 3.0 on 2026-05-18 13:43 + +from django.db import migrations + + +def copy_address(apps, schema_editor): + OldAddress = apps.get_model('oc_lettings_site', 'Address') + NewAddress = apps.get_model('lettings', 'Address') + + for obj in OldAddress.objects.all(): + NewAddress.objects.create( + id=obj.id, + number=obj.number, + street=obj.street, + city=obj.city, + state=obj.state, + zip_code=obj.zip_code, + country_iso_code=obj.country_iso_code, + ) + + +def copy_lettings(apps, schema_editor): + OldLetting = apps.get_model('oc_lettings_site', 'Letting') + NewLetting = apps.get_model('lettings', 'Letting') + NewAddress = apps.get_model('lettings', 'Address') + + for obj in OldLetting.objects.all(): + + new_address = NewAddress.objects.get(id=obj.address.id) + + NewLetting.objects.create( + id=obj.id, + title=obj.title, + address=new_address, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('lettings', '0001_initial'), + ('oc_lettings_site', '0001_initial'), + ] + + operations = [ + migrations.RunPython(copy_address), + migrations.RunPython(copy_lettings), + ] diff --git a/lettings/migrations/0003_auto_20260518_1551.py b/lettings/migrations/0003_auto_20260518_1551.py new file mode 100644 index 0000000000..11b098bd67 --- /dev/null +++ b/lettings/migrations/0003_auto_20260518_1551.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0 on 2026-05-18 13:51 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('lettings', '0002_auto_20260518_1543'), + ] + + operations = [ + migrations.AlterField( + model_name='letting', + name='address', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='lettings.Address'), + ), + ] diff --git a/lettings/migrations/__init__.py b/lettings/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lettings/models.py b/lettings/models.py new file mode 100644 index 0000000000..081de2a663 --- /dev/null +++ b/lettings/models.py @@ -0,0 +1,58 @@ +""" +Models module for lettings app +""" +from django.db import models +from django.core.validators import MaxValueValidator, MinLengthValidator + + +class Address(models.Model): + """ + Address model for Lettings + Attributes: + number (int): Letting number + street (str): Street address + city (str): City address + state (str): State address + zip_code (int): Zip code + country_iso_code (int): Country code + """ + number = models.PositiveIntegerField(validators=[MaxValueValidator(9999)]) + street = models.CharField(max_length=64) + city = models.CharField(max_length=64) + state = models.CharField(max_length=2, validators=[MinLengthValidator(2)]) + zip_code = models.PositiveIntegerField(validators=[MaxValueValidator(99999)]) + country_iso_code = models.CharField(max_length=3, validators=[MinLengthValidator(3)]) + + class Meta: + """ + Meta class for Lettings to specify verbose names + """ + verbose_name = "Address" + verbose_name_plural = "Addresses" + + def __str__(self) -> str: + """ + string method for Lettings + Returns: + A f-string with number and street address + """ + return f'{self.number} {self.street}' + + +class Letting(models.Model): + """ + Letting model for Lettings + Attributes: + title (str): Letting title + address (Address): Letting address + """ + title = models.CharField(max_length=256) + address = models.OneToOneField(Address, on_delete=models.CASCADE) + + def __str__(self) -> str: + """ + String method for Lettings + Returns: + A f-string with letting title + """ + return self.title diff --git a/templates/lettings_index.html b/lettings/templates/lettings/index.html similarity index 89% rename from templates/lettings_index.html rename to lettings/templates/lettings/index.html index 92857a78d9..a85f3a348e 100644 --- a/templates/lettings_index.html +++ b/lettings/templates/lettings/index.html @@ -20,7 +20,7 @@

Lettings

@@ -36,7 +36,7 @@

Lettings

Home - + Profiles diff --git a/templates/letting.html b/lettings/templates/lettings/letting.html similarity index 95% rename from templates/letting.html rename to lettings/templates/lettings/letting.html index 7e5f3a73fd..252d68035e 100644 --- a/templates/letting.html +++ b/lettings/templates/lettings/letting.html @@ -25,14 +25,14 @@

{{ title }}

diff --git a/lettings/tests/__init__.py b/lettings/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lettings/tests/conftest.py b/lettings/tests/conftest.py new file mode 100644 index 0000000000..66ffe3eac4 --- /dev/null +++ b/lettings/tests/conftest.py @@ -0,0 +1,46 @@ +""" +Fixture module for lettings tests +""" +import pytest + +from _pytest.monkeypatch import MonkeyPatch + +from lettings.models import Address, Letting + + +@pytest.fixture(autouse=True) +def disable_sentry(monkeypatch: MonkeyPatch): + monkeypatch.setattr( + "monitoring.sentry_sdk.init", + lambda *args, **kwargs: None + ) + + +@pytest.fixture +def get_address(): + """ + Fixture that returns fictive address + Returns: + The address object + """ + return Address.objects.create( + number=500, + street="Address test", + city="City test", + state="State test", + zip_code=99999, + country_iso_code="Country iso code test" + ) + + +@pytest.fixture +def get_letting(get_address: Address): + """ + Fixture that returns fictive letting + Returns: + The letting object + """ + return Letting.objects.create( + title="Test Letting", + address=get_address + ) diff --git a/lettings/tests/tests_model.py b/lettings/tests/tests_model.py new file mode 100644 index 0000000000..74f035e3bc --- /dev/null +++ b/lettings/tests/tests_model.py @@ -0,0 +1,20 @@ +""" +Tests module for lettings app models +""" +import pytest + +from lettings.models import Letting, Address + + +class TestLettingsModel: + @pytest.mark.django_db + def test_lettings_address_model_ok(self, get_address: Address): + expected = f"{get_address.number} {get_address.street}" + + assert str(get_address) == expected + + @pytest.mark.django_db + def test_lettings_letting_model_ok(self, get_letting: Letting): + expected = f"{get_letting.title}" + + assert str(get_letting) == expected diff --git a/lettings/tests/tests_url.py b/lettings/tests/tests_url.py new file mode 100644 index 0000000000..0a7309ee62 --- /dev/null +++ b/lettings/tests/tests_url.py @@ -0,0 +1,23 @@ +""" +Tests module for lettings app urls +""" +import pytest + +from django.urls import reverse, resolve + +from lettings.models import Letting, Address + + +class TestLettingsUrl: + def test_lettings_index_url(self): + path = reverse(viewname="lettings:index") + + assert path == "/lettings/" + assert resolve(path).view_name == "lettings:index" + + @pytest.mark.django_db + def test_lettings_letting_url(self, get_address: Address, get_letting: Letting): + path = reverse(viewname="lettings:letting", kwargs={"letting_id": get_letting.id}) + + assert path == "/lettings/1/" + assert resolve(path).view_name == "lettings:letting" diff --git a/lettings/tests/tests_view.py b/lettings/tests/tests_view.py new file mode 100644 index 0000000000..0731a4e8df --- /dev/null +++ b/lettings/tests/tests_view.py @@ -0,0 +1,102 @@ +""" +Tests module for lettings app views +""" +import pytest + +from django.urls import reverse +from django.test import Client +from pytest_django.asserts import assertTemplateUsed +from _pytest.monkeypatch import MonkeyPatch + +from lettings.models import Letting, Address + + +class TestLettingsView: + @pytest.mark.django_db + def test_lettings_index_view_ok(self, get_letting: Letting): + client = Client() + path = reverse(viewname="lettings:index") + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = '

Lettings

' + expected_content = f'{get_letting.title}' + + assert expected_h1 in content + assert expected_content in content + assert response.status_code == 200 + assertTemplateUsed(response, template_name="lettings/index.html") + + @pytest.mark.django_db + def test_lettings_index_view_returns_500(self, monkeypatch: MonkeyPatch, get_letting: Letting): + def raise_error(): + raise Exception("forced error") + + monkeypatch.setattr("lettings.views.Letting.objects.all", raise_error) + + client = Client() + path = reverse(viewname="lettings:index") + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = (f'

500 Error : ' + f'something wrong with the server - forced error

') + + assert expected_h1 in content + assert response.status_code == 500 + assertTemplateUsed(response, template_name="error_500.html") + + @pytest.mark.django_db + def test_lettings_letting_view_ok(self, get_address: Address, get_letting: Letting): + client = Client() + path = reverse(viewname="lettings:letting", kwargs={"letting_id": get_letting.id}) + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = f'

{get_letting.title}

' + expected_content = [f'{get_address.number} {get_address.street}', + f'{get_address.city}, {get_address.state}', + f'{get_address.zip_code}', + f'{get_address.country_iso_code}'] + + assert expected_h1 in content + for expected_child in expected_content: + assert expected_child in content + assert response.status_code == 200 + assertTemplateUsed(response, template_name="lettings/letting.html") + + @pytest.mark.django_db + def test_lettings_letting_view_returns_404(self, get_address: Address, get_letting: Letting): + client = Client() + path = reverse(viewname="lettings:letting", kwargs={"letting_id": 2}) + + response = client.get(path=path) + content = response.content.decode() + + expected_h1 = (f'

404 Error : ' + f'letting n\xb0 2 not found !

') + + assert expected_h1 in content + assert response.status_code == 404 + assertTemplateUsed(response, template_name="error_404.html") + + @pytest.mark.django_db + def test_lettings_letting_view_returns_500(self, + monkeypatch: MonkeyPatch, + get_letting: Letting): + def raise_error(*args, **kwargs): + raise Exception("forced error") + + monkeypatch.setattr("lettings.views.Letting.objects.get", raise_error) + + client = Client() + path = reverse(viewname="lettings:letting", kwargs={"letting_id": get_letting.id}) + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = (f'

500 Error : ' + f'something wrong with the server - forced error

') + + assert expected_h1 in content + assert response.status_code == 500 + assertTemplateUsed(response, template_name="error_500.html") diff --git a/lettings/urls.py b/lettings/urls.py new file mode 100644 index 0000000000..61a7ca5c89 --- /dev/null +++ b/lettings/urls.py @@ -0,0 +1,13 @@ +""" +URLs module for lettings app +""" +from django.urls import path + +from . import views + +app_name = 'lettings' + +urlpatterns = [ + path('', views.index, name='index'), + path('/', views.letting, name='letting'), +] diff --git a/lettings/views.py b/lettings/views.py new file mode 100644 index 0000000000..2536865100 --- /dev/null +++ b/lettings/views.py @@ -0,0 +1,86 @@ +""" +Views module for lettings app +""" +from django.http import HttpRequest, HttpResponse +from django.shortcuts import render + +from .models import Letting +from monitoring import logger + + +# Aenean leo magna, vestibulum et tincidunt fermentum, consectetur quis velit. Sed non placerat +# massa. Integer est nunc, pulvinar a tempor et, bibendum id arcu. Vestibulum ante ipsum primis in +# faucibus orci luctus et ultrices posuere cubilia curae; Cras eget scelerisque +def index(request: HttpRequest) -> HttpResponse: + """ + View function for lettings index page + Args: + request (HttpRequest): Http Request object + + Returns: + An HTTP response with the list of lettings or an HTTP response with 500 error. + """ + try: + lettings_list = Letting.objects.all() + context = {'lettings_list': lettings_list} + + logger.info(f"Going to lettings index page : {context=}, status = 200.") + + return render(request, template_name='lettings/index.html', context=context, status=200) + + except Exception as e: + context = {"error": str(e)} + + logger.error(f"Error 500 returned while reaching lettings index page : {context=}," + f" status = 500.") + + return render(request, template_name='error_500.html', context=context, status=500) + + +# Cras ultricies dignissim purus, vitae hendrerit ex varius non. In accumsan porta nisl id +# eleifend. Praesent dignissim, odio eu consequat pretium, purus urna vulputate arcu, vitae +# efficitur lacus justo nec purus. Aenean finibus faucibus lectus at porta. Maecenas auctor, est ut +# luctus congue, dui enim mattis enim, ac condimentum velit libero in magna. Suspendisse potenti. +# In tempus a nisi sed laoreet. Suspendisse porta dui eget sem accumsan interdum. Ut quis urna +# pellentesque justo mattis ullamcorper ac non tellus. In tristique mauris eu velit fermentum, +# tempus pharetra est luctus. Vivamus consequat aliquam libero, eget bibendum lorem. Sed non dolor +# risus. Mauris condimentum auctor elementum. Donec quis nisi ligula. Integer vehicula tincidunt +# enim, ac lacinia augue pulvinar sit amet. +def letting(request: HttpRequest, letting_id: int) -> HttpResponse: + """ + View function for letting detail page + Args: + request (HttpRequest): Http Request object + letting_id (int): letting id + + Returns: + An HTTP response with the letting detail or an HTTP response with 404 error if not found + or an HTTP response with 500 error + """ + try: + letting = Letting.objects.get(id=letting_id) + + context = { + 'title': letting.title, + 'address': letting.address, + } + + logger.info(f"Going to lettings details page : {context=}, status = 200.") + + return render(request, template_name='lettings/letting.html', context=context, status=200) + + except Letting.DoesNotExist as e: + context = {"type": "letting", "id": letting_id, "error": str(e)} + + logger.warning(f"Error 404 returned while reaching letting n°{letting_id} : {context=}," + f" status = 404.") + + return render(request, template_name='error_404.html', context=context, status=404) + + except Exception as e: + context = {"error": str(e)} + + logger.error(f"Error 500 returned while reaching letting details page : {context=}," + f" status = 500.") + + return render(request, template_name='error_500.html', context=context, status=500) diff --git a/manage.py b/manage.py index c0e27e034a..961c945a68 100755 --- a/manage.py +++ b/manage.py @@ -1,3 +1,6 @@ +""" +Main module +""" import os import sys diff --git a/monitoring.py b/monitoring.py new file mode 100644 index 0000000000..309dd0ca46 --- /dev/null +++ b/monitoring.py @@ -0,0 +1,35 @@ +""" +Module for monitoring with Sentry +""" +import logging +import sentry_sdk + +from sentry_sdk.integrations.logging import LoggingIntegration + +from config import SENTRY_KEY + +logger = logging.getLogger(__name__) + + +def init_sentry(): + """ + Method to initialize sentry integration + """ + logging.basicConfig(level=logging.INFO) + + sentry_logging = LoggingIntegration( + level=logging.INFO, + event_level=logging.ERROR, + ) + + sentry_sdk.init( + dsn=SENTRY_KEY, + # Add request headers and IP for users, + # see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info + send_default_pii=True, + + # Enable logs to be sent to Sentry + enable_logs=True, + + integrations=[sentry_logging] + ) diff --git a/oc-lettings-site.sqlite3 b/oc-lettings-site.sqlite3 index 3d885414f9..92f150a14e 100644 Binary files a/oc-lettings-site.sqlite3 and b/oc-lettings-site.sqlite3 differ diff --git a/oc_lettings_site/admin.py b/oc_lettings_site/admin.py index 63328c6dd3..e69de29bb2 100644 --- a/oc_lettings_site/admin.py +++ b/oc_lettings_site/admin.py @@ -1,10 +0,0 @@ -from django.contrib import admin - -from .models import Letting -from .models import Address -from .models import Profile - - -admin.site.register(Letting) -admin.site.register(Address) -admin.site.register(Profile) diff --git a/oc_lettings_site/apps.py b/oc_lettings_site/apps.py index 6489692f04..805a860bb6 100644 --- a/oc_lettings_site/apps.py +++ b/oc_lettings_site/apps.py @@ -1,5 +1,13 @@ +""" +App config module for oc_lettings_site app +""" from django.apps import AppConfig class OCLettingsSiteConfig(AppConfig): + """ + Namespace class for oc_lettings_site app + Attributes: + name (str): Name of the app + """ name = 'oc_lettings_site' diff --git a/oc_lettings_site/migrations/0002_auto_20260518_1551.py b/oc_lettings_site/migrations/0002_auto_20260518_1551.py new file mode 100644 index 0000000000..586b4a1db8 --- /dev/null +++ b/oc_lettings_site/migrations/0002_auto_20260518_1551.py @@ -0,0 +1,32 @@ +# Generated by Django 3.0 on 2026-05-18 13:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('oc_lettings_site', '0001_initial'), + ('lettings', '0003_auto_20260518_1551'), + ('profiles', '0003_auto_20260518_1551'), + ] + + operations = [ + migrations.RemoveField( + model_name='letting', + name='address', + ), + migrations.RemoveField( + model_name='profile', + name='user', + ), + migrations.DeleteModel( + name='Address', + ), + migrations.DeleteModel( + name='Letting', + ), + migrations.DeleteModel( + name='Profile', + ), + ] diff --git a/oc_lettings_site/models.py b/oc_lettings_site/models.py deleted file mode 100644 index ed255e8c11..0000000000 --- a/oc_lettings_site/models.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.db import models -from django.core.validators import MaxValueValidator, MinLengthValidator -from django.contrib.auth.models import User - - -class Address(models.Model): - number = models.PositiveIntegerField(validators=[MaxValueValidator(9999)]) - street = models.CharField(max_length=64) - city = models.CharField(max_length=64) - state = models.CharField(max_length=2, validators=[MinLengthValidator(2)]) - zip_code = models.PositiveIntegerField(validators=[MaxValueValidator(99999)]) - country_iso_code = models.CharField(max_length=3, validators=[MinLengthValidator(3)]) - - def __str__(self): - return f'{self.number} {self.street}' - - -class Letting(models.Model): - title = models.CharField(max_length=256) - address = models.OneToOneField(Address, on_delete=models.CASCADE) - - def __str__(self): - return self.title - - -class Profile(models.Model): - user = models.OneToOneField(User, on_delete=models.CASCADE) - favorite_city = models.CharField(max_length=64, blank=True) - - def __str__(self): - return self.user.username diff --git a/oc_lettings_site/settings.py b/oc_lettings_site/settings.py index a18bee8106..d2e781dc2c 100644 --- a/oc_lettings_site/settings.py +++ b/oc_lettings_site/settings.py @@ -1,3 +1,6 @@ +""" +Settings module for oc_lettings_site app +""" import os from pathlib import Path @@ -22,6 +25,8 @@ INSTALLED_APPS = [ 'oc_lettings_site.apps.OCLettingsSiteConfig', + 'lettings.apps.LettingsConfig', + 'profiles.apps.ProfilesConfig', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -111,4 +116,4 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') STATIC_URL = '/static/' -STATICFILES_DIRS = [BASE_DIR / "static",] +STATICFILES_DIRS = [BASE_DIR / "static", ] diff --git a/oc_lettings_site/tests.py b/oc_lettings_site/tests.py deleted file mode 100644 index 3fd62bb718..0000000000 --- a/oc_lettings_site/tests.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_dummy(): - assert 1 diff --git a/oc_lettings_site/tests/__init__.py b/oc_lettings_site/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/oc_lettings_site/tests/conftest.py b/oc_lettings_site/tests/conftest.py new file mode 100644 index 0000000000..943f1fbb0b --- /dev/null +++ b/oc_lettings_site/tests/conftest.py @@ -0,0 +1,14 @@ +""" +Fixture module for oc_lettings_site tests +""" +import pytest + +from _pytest.monkeypatch import MonkeyPatch + + +@pytest.fixture(autouse=True) +def disable_sentry(monkeypatch: MonkeyPatch): + monkeypatch.setattr( + "monitoring.sentry_sdk.init", + lambda *args, **kwargs: None + ) diff --git a/oc_lettings_site/tests/tests_url.py b/oc_lettings_site/tests/tests_url.py new file mode 100644 index 0000000000..1140fabfdd --- /dev/null +++ b/oc_lettings_site/tests/tests_url.py @@ -0,0 +1,12 @@ +""" +Tests module for oc_lettings_site app urls +""" +from django.urls import reverse, resolve + + +class TestOcLettingsSiteUrl: + def test_index_url(self): + path = reverse("index") + + assert path == "/" + assert resolve(path).view_name == 'index' diff --git a/oc_lettings_site/tests/tests_view.py b/oc_lettings_site/tests/tests_view.py new file mode 100644 index 0000000000..c6c2330a3b --- /dev/null +++ b/oc_lettings_site/tests/tests_view.py @@ -0,0 +1,46 @@ +""" +Tests module for oc_lettings_site app views +""" +import pytest + +from django.template.response import TemplateResponse +from django.test import Client +from django.urls import reverse +from pytest_django.asserts import assertTemplateUsed +from _pytest.monkeypatch import MonkeyPatch + + +class TestOcLettingsSiteView: + @pytest.mark.django_db + def test_oc_lettings_site_index_view_ok(self): + client = Client() + path = reverse(viewname="index") + + response = client.get(path=path) + content = response.content.decode() + expected = "Welcome to Holiday Homes" + + assert expected in content + assert response.status_code == 200 + assertTemplateUsed(response, template_name="index.html") + + @pytest.mark.django_db + def test_oc_lettings_site_index_view_returns_500(self, monkeypatch: MonkeyPatch): + def side_effect(request, template_name, context=None, status=500): + if template_name == "index.html": + raise Exception("forced error") + return TemplateResponse(request, template_name, context or {}, status=status) + + monkeypatch.setattr("oc_lettings_site.views.render", side_effect) + + client = Client() + path = reverse(viewname="index") + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = (f'

500 Error : ' + f'something wrong with the server - forced error

') + + assert expected_h1 in content + assert response.status_code == 500 + assertTemplateUsed(response, template_name="error_500.html") diff --git a/oc_lettings_site/urls.py b/oc_lettings_site/urls.py index f0ff5897ab..87eebf52d6 100644 --- a/oc_lettings_site/urls.py +++ b/oc_lettings_site/urls.py @@ -1,13 +1,15 @@ +""" +URLs module for oc_lettings_site app. +Include the lettings and profiles apps urls +""" from django.contrib import admin -from django.urls import path +from django.urls import path, include from . import views urlpatterns = [ path('', views.index, name='index'), - path('lettings/', views.lettings_index, name='lettings_index'), - path('lettings//', views.letting, name='letting'), - path('profiles/', views.profiles_index, name='profiles_index'), - path('profiles//', views.profile, name='profile'), + path('lettings/', include('lettings.urls')), + path('profiles/', include('profiles.urls')), path('admin/', admin.site.urls), ] diff --git a/oc_lettings_site/views.py b/oc_lettings_site/views.py index a72db27074..12b562c384 100644 --- a/oc_lettings_site/views.py +++ b/oc_lettings_site/views.py @@ -1,45 +1,37 @@ +""" +Views module for oc_lettings_site app +""" +from django.http import HttpRequest, HttpResponse from django.shortcuts import render -from .models import Letting, Profile - - - - -# Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque molestie quam lobortis leo consectetur ullamcorper non id est. Praesent dictum, nulla eget feugiat sagittis, sem mi convallis eros, -# vitae dapibus nisi lorem dapibus sem. Maecenas pharetra purus ipsum, eget consequat ipsum lobortis quis. Phasellus eleifend ex auctor venenatis tempus. -# Aliquam vitae erat ac orci placerat luctus. Nullam elementum urna nisi, pellentesque iaculis enim cursus in. Praesent volutpat porttitor magna, non finibus neque cursus id. -def index(request): - return render(request, 'index.html') - -# Aenean leo magna, vestibulum et tincidunt fermentum, consectetur quis velit. Sed non placerat massa. Integer est nunc, pulvinar a -# tempor et, bibendum id arcu. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Cras eget scelerisque -def lettings_index(request): - lettings_list = Letting.objects.all() - context = {'lettings_list': lettings_list} - return render(request, 'lettings_index.html', context) - - -#Cras ultricies dignissim purus, vitae hendrerit ex varius non. In accumsan porta nisl id eleifend. Praesent dignissim, odio eu consequat pretium, purus urna vulputate arcu, vitae efficitur -# lacus justo nec purus. Aenean finibus faucibus lectus at porta. Maecenas auctor, est ut luctus congue, dui enim mattis enim, ac condimentum velit libero in magna. Suspendisse potenti. In tempus a nisi sed laoreet. -# Suspendisse porta dui eget sem accumsan interdum. Ut quis urna pellentesque justo mattis ullamcorper ac non tellus. In tristique mauris eu velit fermentum, tempus pharetra est luctus. Vivamus consequat aliquam libero, eget bibendum lorem. Sed non dolor risus. Mauris condimentum auctor elementum. Donec quis nisi ligula. Integer vehicula tincidunt enim, ac lacinia augue pulvinar sit amet. -def letting(request, letting_id): - letting = Letting.objects.get(id=letting_id) - context = { - 'title': letting.title, - 'address': letting.address, - } - return render(request, 'letting.html', context) - -# Sed placerat quam in pulvinar commodo. Nullam laoreet consectetur ex, sed consequat libero pulvinar eget. Fusc -# faucibus, urna quis auctor pharetra, massa dolor cursus neque, quis dictum lacus d -def profiles_index(request): - profiles_list = Profile.objects.all() - context = {'profiles_list': profiles_list} - return render(request, 'profiles_index.html', context) - -# Aliquam sed metus eget nisi tincidunt ornare accumsan eget lac -# laoreet neque quis, pellentesque dui. Nullam facilisis pharetra vulputate. Sed tincidunt, dolor id facilisis fringilla, eros leo tristique lacus, -# it. Nam aliquam dignissim congue. Pellentesque habitant morbi tristique senectus et netus et males -def profile(request, username): - profile = Profile.objects.get(user__username=username) - context = {'profile': profile} - return render(request, 'profile.html', context) + +from monitoring import init_sentry, logger + + +# Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque molestie quam lobortis leo +# consectetur ullamcorper non id est. Praesent dictum, nulla eget feugiat sagittis, sem mi +# convallis eros, vitae dapibus nisi lorem dapibus sem. Maecenas pharetra purus ipsum, eget +# consequat ipsum lobortis quis. Phasellus eleifend ex auctor venenatis tempus. Aliquam vitae erat +# ac orci placerat luctus. Nullam elementum urna nisi, pellentesque iaculis enim cursus in. +# Praesent volutpat porttitor magna, non finibus neque cursus id. +def index(request: HttpRequest) -> HttpResponse: + """ + View function for home page + Args: + request (HttpRequest): Http Request object + + Returns: + An HTTP response with index page or HTTP response with 500 error. + """ + init_sentry() + try: + logger.info(f"Going to home page : status = 200.") + + return render(request, template_name='index.html') + + except Exception as e: + context = {'error': str(e)} + + logger.error(f"Error 500 returned while reaching home page : {context=}" + f", status = 500.") + + return render(request, template_name='error_500.html', context=context) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000000..222502339e --- /dev/null +++ b/poetry.lock @@ -0,0 +1,733 @@ +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. + +[[package]] +name = "asgiref" +version = "3.11.1" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133"}, + {file = "asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce"}, +] + +[package.dependencies] +typing_extensions = {version = ">=4", markers = "python_version < \"3.11\""} + +[package.extras] +tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"] + +[[package]] +name = "certifi" +version = "2026.4.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a"}, + {file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"}, +] + +[[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 = "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 = "coverage" +version = "7.14.0" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075"}, + {file = "coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85"}, + {file = "coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323"}, + {file = "coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a"}, + {file = "coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480"}, + {file = "coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c"}, + {file = "coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3"}, + {file = "coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1"}, + {file = "coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627"}, + {file = "coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5"}, + {file = "coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595"}, + {file = "coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27"}, + {file = "coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2"}, + {file = "coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d"}, + {file = "coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef"}, + {file = "coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe"}, + {file = "coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae"}, + {file = "coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e"}, + {file = "coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96"}, + {file = "coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90"}, + {file = "coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477"}, + {file = "coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab"}, + {file = "coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917"}, + {file = "coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8"}, + {file = "coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d"}, + {file = "coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca"}, + {file = "coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828"}, + {file = "coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d"}, + {file = "coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9"}, + {file = "coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1"}, + {file = "coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db"}, + {file = "coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2"}, + {file = "coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644"}, + {file = "coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b"}, + {file = "coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1"}, + {file = "coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "django" +version = "3.0" +description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "Django-3.0-py3-none-any.whl", hash = "sha256:6f857bd4e574442ba35a7172f1397b303167dae964cf18e53db5e85fe248d000"}, + {file = "Django-3.0.tar.gz", hash = "sha256:d98c9b6e5eed147bc51f47c014ff6826bd1ab50b166956776ee13db5a58804ae"}, +] + +[package.dependencies] +asgiref = ">=3.2,<4.0" +pytz = "*" +sqlparse = ">=0.2.2" + +[package.extras] +argon2 = ["argon2-cffi (>=16.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "entrypoints" +version = "0.3" +description = "Discover and load entry points from installed packages." +optional = false +python-versions = ">=2.7" +groups = ["main"] +files = [ + {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, + {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version == \"3.10\"" +files = [ + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "flake8" +version = "3.7.0" +description = "the modular source code checker: pep8, pyflakes and co" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "flake8-3.7.0-py2.py3-none-any.whl", hash = "sha256:7eda8e5c29ac9e0d3c0fd44649298c29768efc79a9b872b41c8f05ca5f502c83"}, + {file = "flake8-3.7.0.tar.gz", hash = "sha256:2baac1c277d917f3e01ce17a60dcfad4a4ce13a2c5d50a15d0811302a3bdf7aa"}, +] + +[package.dependencies] +entrypoints = ">=0.3.0,<0.4.0" +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.5.0,<2.6.0" +pyflakes = ">=2.1.0,<2.2.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 = "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 = "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 = "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.6.1" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] + +[[package]] +name = "packaging" +version = "26.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, + {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, +] + +[[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 = "pycodestyle" +version = "2.5.0" +description = "Python style guide checker" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "pycodestyle-2.5.0-py2.py3-none-any.whl", hash = "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56"}, + {file = "pycodestyle-2.5.0.tar.gz", hash = "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"}, +] + +[[package]] +name = "pyflakes" +version = "2.1.1" +description = "passive checker of Python programs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "pyflakes-2.1.1-py2.py3-none-any.whl", hash = "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0"}, + {file = "pyflakes-2.1.1.tar.gz", hash = "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"}, +] + +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "9.0.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, + {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"}, + {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"}, +] + +[package.dependencies] +coverage = {version = ">=7.10.6", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=7" + +[package.extras] +testing = ["process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-django" +version = "4.12.0" +description = "A Django plugin for pytest." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85"}, + {file = "pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[[package]] +name = "python-dotenv" +version = "1.2.2" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"}, + {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "pytz" +version = "2026.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126"}, + {file = "pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a"}, +] + +[[package]] +name = "sentry-sdk" +version = "2.60.0" +description = "Python client for Sentry (https://sentry.io)" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "sentry_sdk-2.60.0-py3-none-any.whl", hash = "sha256:28a536c03291c8bcb363cf35c611b32738ec118ff64d8d6383b096448ac4c803"}, + {file = "sentry_sdk-2.60.0.tar.gz", hash = "sha256:0bd25e54e78ca02d0be512529fa644bbbf9e8470d7b26371294012d4ca93c978"}, +] + +[package.dependencies] +certifi = "*" +urllib3 = ">=1.26.11" + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +anthropic = ["anthropic (>=0.16)"] +arq = ["arq (>=0.23)"] +asyncio = ["httpcore[asyncio] (==1.*)"] +asyncpg = ["asyncpg (>=0.23)"] +beam = ["apache-beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +celery-redbeat = ["celery-redbeat (>=2)"] +chalice = ["chalice (>=1.16.0)"] +clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +fastapi = ["fastapi (>=0.79.0)"] +flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] +google-genai = ["google-genai (>=1.29.0)"] +grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"] +http2 = ["httpcore[http2] (==1.*)"] +httpx = ["httpx (>=0.16.0)"] +huey = ["huey (>=2)"] +huggingface-hub = ["huggingface_hub (>=0.22)"] +langchain = ["langchain (>=0.0.210)"] +langgraph = ["langgraph (>=0.6.6)"] +launchdarkly = ["launchdarkly-server-sdk (>=9.8.0)"] +litellm = ["litellm (>=1.77.5,!=1.82.7,!=1.82.8)"] +litestar = ["litestar (>=2.0.0)"] +loguru = ["loguru (>=0.5)"] +mcp = ["mcp (>=1.15.0)"] +openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] +openfeature = ["openfeature-sdk (>=0.7.1)"] +opentelemetry = ["opentelemetry-distro (>=0.35b0)"] +opentelemetry-experimental = ["opentelemetry-distro"] +opentelemetry-otlp = ["opentelemetry-distro[otlp] (>=0.35b0)"] +pure-eval = ["asttokens", "executing", "pure_eval"] +pydantic-ai = ["pydantic-ai (>=1.0.0)"] +pymongo = ["pymongo (>=3.1)"] +pyspark = ["pyspark (>=2.4.4)"] +quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +starlette = ["starlette (>=0.19.1)"] +starlite = ["starlite (>=1.48)"] +statsig = ["statsig (>=0.55.3)"] +tornado = ["tornado (>=6)"] +unleash = ["UnleashClient (>=6.0.1)"] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sqlparse" +version = "0.5.5" +description = "A non-validating SQL parser." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba"}, + {file = "sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e"}, +] + +[package.extras] +dev = ["build"] +doc = ["sphinx"] + +[[package]] +name = "tomli" +version = "2.4.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_full_version <= \"3.11.0a6\"" +files = [ + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, +] + +[[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"] +markers = "python_version == \"3.10\"" +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.7.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, + {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, +] + +[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\""] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.10" +content-hash = "6e2f2adfbc0ae06398494a726625a155716005abb006e86c1ed008ddd97cbbab" diff --git a/profiles/__init__.py b/profiles/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/profiles/admin.py b/profiles/admin.py new file mode 100644 index 0000000000..2b417bb0da --- /dev/null +++ b/profiles/admin.py @@ -0,0 +1,9 @@ +""" +Admin registration module for profiles app +""" +from django.contrib import admin + +from profiles.models import Profile + + +admin.site.register(Profile) diff --git a/profiles/apps.py b/profiles/apps.py new file mode 100644 index 0000000000..b0c6010f11 --- /dev/null +++ b/profiles/apps.py @@ -0,0 +1,13 @@ +""" +Namespace module for profiles app +""" +from django.apps import AppConfig + + +class ProfilesConfig(AppConfig): + """ + Namespace class for profiles app + Attributes: + name: namespace of the profiles app + """ + name = 'profiles' diff --git a/profiles/migrations/0001_initial.py b/profiles/migrations/0001_initial.py new file mode 100644 index 0000000000..069a3babcc --- /dev/null +++ b/profiles/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0 on 2026-05-18 12:53 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('favorite_city', models.CharField(blank=True, max_length=64)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='new_profile', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/profiles/migrations/0002_auto_20260518_1548.py b/profiles/migrations/0002_auto_20260518_1548.py new file mode 100644 index 0000000000..7be66c52bf --- /dev/null +++ b/profiles/migrations/0002_auto_20260518_1548.py @@ -0,0 +1,27 @@ +# Generated by Django 3.0 on 2026-05-18 13:48 + +from django.db import migrations + + +def copy_profile(apps, schema_editor): + OldProfile = apps.get_model('oc_lettings_site', 'Profile') + NewProfile = apps.get_model('profiles', 'Profile') + + for obj in OldProfile.objects.all(): + NewProfile.objects.create( + id=obj.id, + user=obj.user, + favorite_city=obj.favorite_city, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0001_initial'), + ('oc_lettings_site', '0001_initial'), + ] + + operations = [ + migrations.RunPython(copy_profile), + ] diff --git a/profiles/migrations/0003_auto_20260518_1551.py b/profiles/migrations/0003_auto_20260518_1551.py new file mode 100644 index 0000000000..61fbe40024 --- /dev/null +++ b/profiles/migrations/0003_auto_20260518_1551.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0 on 2026-05-18 13:51 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('profiles', '0002_auto_20260518_1548'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/profiles/migrations/__init__.py b/profiles/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/profiles/models.py b/profiles/models.py new file mode 100644 index 0000000000..5893b3c972 --- /dev/null +++ b/profiles/models.py @@ -0,0 +1,24 @@ +""" +Models module for profiles app +""" +from django.db import models +from django.contrib.auth.models import User + + +class Profile(models.Model): + """ + Models class for profiles app + Attributes: + user (User): user + favorite_city (str): The favorite city of the user + """ + user = models.OneToOneField(User, on_delete=models.CASCADE) + favorite_city = models.CharField(max_length=64, blank=True) + + def __str__(self) -> str: + """ + String method for profile model + Returns: + The user name of the profile + """ + return self.user.username diff --git a/templates/profiles_index.html b/profiles/templates/profiles/index.html similarity index 88% rename from templates/profiles_index.html rename to profiles/templates/profiles/index.html index 4ad1daf92f..563b7a0166 100644 --- a/templates/profiles_index.html +++ b/profiles/templates/profiles/index.html @@ -18,7 +18,7 @@

Profiles

@@ -34,7 +34,7 @@

Profiles

Home - + Lettings
diff --git a/templates/profile.html b/profiles/templates/profiles/profile.html similarity index 96% rename from templates/profile.html rename to profiles/templates/profiles/profile.html index d150d30e63..4b1af37496 100644 --- a/templates/profile.html +++ b/profiles/templates/profiles/profile.html @@ -24,14 +24,14 @@

{{ profile.user.username }}

diff --git a/profiles/tests/__init__.py b/profiles/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/profiles/tests/conftest.py b/profiles/tests/conftest.py new file mode 100644 index 0000000000..f6addc0755 --- /dev/null +++ b/profiles/tests/conftest.py @@ -0,0 +1,34 @@ +""" +Fixture module for profiles tests +""" +import pytest + +from _pytest.monkeypatch import MonkeyPatch +from django.contrib.auth.models import User + +from profiles.models import Profile + + +@pytest.fixture(autouse=True) +def disable_sentry(monkeypatch: MonkeyPatch): + monkeypatch.setattr( + "monitoring.sentry_sdk.init", + lambda *args, **kwargs: None + ) + + +@pytest.fixture +def get_profile(): + """ + Fixture that returns fictive profile + Returns: + The profile object + """ + return Profile.objects.create( + user=User.objects.create_user(username='Username', + first_name='First', + last_name='Last', + email='test@test.com', + password='test_pwd'), + favorite_city="City Test" + ) diff --git a/profiles/tests/tests_model.py b/profiles/tests/tests_model.py new file mode 100644 index 0000000000..44970b1485 --- /dev/null +++ b/profiles/tests/tests_model.py @@ -0,0 +1,14 @@ +""" +Tests module for profiles app models +""" +import pytest + +from profiles.models import Profile + + +class TestProfilesModel: + @pytest.mark.django_db + def test_profiles_profile_model_ok(self, get_profile: Profile): + expected = f"{get_profile.user.username}" + + assert str(get_profile) == expected diff --git a/profiles/tests/tests_url.py b/profiles/tests/tests_url.py new file mode 100644 index 0000000000..1d52508917 --- /dev/null +++ b/profiles/tests/tests_url.py @@ -0,0 +1,30 @@ +""" +Tests module for profiles app urls +""" +import pytest + +from django.contrib.auth.models import User +from django.urls import reverse, resolve + +from profiles.models import Profile + + +class TestProfilesUrl: + def test_profiles_index_url(self): + path = reverse("profiles:index") + + assert path == "/profiles/" + assert resolve(path).view_name == 'profiles:index' + + @pytest.mark.django_db + def test_profiles_profile_url(self): + Profile.objects.create(user=User.objects.create_user(username="Username", + first_name='First Name', + last_name='Last Name', + email='test@test.com'), + favorite_city="Paris") + + path = reverse(viewname="profiles:profile", kwargs={'username': "Username"}) + + assert path == "/profiles/Username/" + assert resolve(path).view_name == "profiles:profile" diff --git a/profiles/tests/tests_view.py b/profiles/tests/tests_view.py new file mode 100644 index 0000000000..1951f139fb --- /dev/null +++ b/profiles/tests/tests_view.py @@ -0,0 +1,104 @@ +""" +Tests module for profiles app views +""" +import pytest + +from django.urls import reverse +from django.test import Client +from pytest_django.asserts import assertTemplateUsed +from _pytest.monkeypatch import MonkeyPatch + +from profiles.models import Profile + + +class TestProfilesView: + @pytest.mark.django_db + def test_profiles_index_view_ok(self, get_profile: Profile): + client = Client() + path = reverse(viewname="profiles:index") + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = '

Profiles

' + expected_content = (f'' + f'{get_profile.user.username}') + + assert expected_h1 in content + assert expected_content in content + assert response.status_code == 200 + assertTemplateUsed(response, template_name="profiles/index.html") + + @pytest.mark.django_db + def test_profiles_index_view_returns_500(self, monkeypatch: MonkeyPatch, get_profile: Profile): + def raise_error(): + raise Exception("forced error") + + monkeypatch.setattr("profiles.views.Profile.objects.all", raise_error) + + client = Client() + path = reverse(viewname="profiles:index") + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = (f'

500 Error : ' + f'something wrong with the server - forced error

') + + assert expected_h1 in content + assert response.status_code == 500 + assertTemplateUsed(response, template_name="error_500.html") + + @pytest.mark.django_db + def test_profiles_profile_view_ok(self, get_profile: Profile): + client = Client() + path = reverse(viewname="profiles:profile", kwargs={"username": get_profile.user.username}) + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = (f'

' + f'{get_profile.user.username}

') + expected_content = [f'First name : {get_profile.user.first_name}', + f'Last name : {get_profile.user.last_name}', + f'Email : {get_profile.user.email}', + f'Favorite city : {get_profile.favorite_city}'] + + assert expected_h1 in content + for expected_child in expected_content: + assert expected_child in content + assert response.status_code == 200 + assertTemplateUsed(response, template_name="profiles/profile.html") + + @pytest.mark.django_db + def test_profiles_profile_view_returns_404(self, get_profile: Profile): + client = Client() + path = reverse(viewname="profiles:profile", kwargs={"username": "test"}) + + response = client.get(path=path) + content = response.content.decode() + + expected_h1 = (f'

404 Error : ' + f'profile \'test\' not found !

') + + assert expected_h1 in content + assert response.status_code == 404 + assertTemplateUsed(response, template_name="error_404.html") + + @pytest.mark.django_db + def test_profiles_profile_view_returns_500(self, + monkeypatch: MonkeyPatch, + get_profile: Profile): + def raise_error(*args, **kwargs): + raise Exception("forced error") + + monkeypatch.setattr("profiles.views.Profile.objects.get", raise_error) + + client = Client() + path = reverse(viewname="profiles:profile", kwargs={"username": get_profile.user.username}) + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = (f'

500 Error : ' + f'something wrong with the server - forced error

') + + assert expected_h1 in content + assert response.status_code == 500 + assertTemplateUsed(response, template_name="error_500.html") diff --git a/profiles/urls.py b/profiles/urls.py new file mode 100644 index 0000000000..1efb5f1a21 --- /dev/null +++ b/profiles/urls.py @@ -0,0 +1,13 @@ +""" +URLs module for profiles app +""" +from django.urls import path + +from . import views + +app_name = 'profiles' + +urlpatterns = [ + path('', views.index, name='index'), + path('/', views.profile, name='profile'), +] diff --git a/profiles/views.py b/profiles/views.py new file mode 100644 index 0000000000..e6b3d808f0 --- /dev/null +++ b/profiles/views.py @@ -0,0 +1,77 @@ +""" +Views module for profiles app +""" +from django.http import HttpRequest, HttpResponse +from django.shortcuts import render +from monitoring import logger + +from profiles.models import Profile + + +# Sed placerat quam in pulvinar commodo. Nullam laoreet consectetur ex, sed consequat libero +# pulvinar eget. Fusc faucibus, urna quis auctor pharetra, massa dolor cursus neque, quis dictum +# lacus d +def index(request: HttpRequest) -> HttpResponse: + """ + View function for profiles index page + Args: + request (HttpRequest): request object + + Returns: + An HTTP response with the list of profiles or HTTP response with 500 error. + """ + try: + profiles_list = Profile.objects.all() + context = {'profiles_list': profiles_list} + + logger.info(f"Going to profiles index page : {context=}, status = 200.") + + return render(request, template_name='profiles/index.html', context=context, status=200) + + except Exception as e: + context = {"error": str(e)} + + logger.error(f"Error 500 returned while reaching profiles index page : {context=}" + f", status = 500.") + + return render(request, template_name='error_500.html', context=context, status=500) + + +# Aliquam sed metus eget nisi tincidunt ornare accumsan eget lac +# laoreet neque quis, pellentesque dui. Nullam facilisis pharetra vulputate. Sed tincidunt, dolor +# id facilisis fringilla, eros leo tristique lacus, it. Nam aliquam dignissim congue. Pellentesque +# habitant morbi tristique senectus et netus et males +def profile(request: HttpRequest, username: str): + """ + View function for profile details page + Args: + request (HttpRequest): request object + username (str): username + + Returns: + An HTTP response with the profile or HTTP response with 404 error if not found + or an HTTP response with 500 error + """ + try: + profile = Profile.objects.get(user__username=username) + context = {'profile': profile} + + logger.info(f"Going to profile details page : {context=}, status = 200.") + + return render(request, template_name='profiles/profile.html', context=context, status=200) + + except Profile.DoesNotExist as e: + context = {"type": "profile", "name": username, "error": str(e)} + + logger.warning(f"Error 404 returned while reaching profile {username} : {context=}," + f" status = 404.") + + return render(request, template_name='error_404.html', context=context, status=404) + + except Exception as e: + context = {"error": str(e)} + + logger.error(f"Error 500 returned while reaching profile details page : {context=}," + f" status = 500.") + + return render(request, template_name='error_500.html', context=context, status=500) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..fe5853fc61 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[project] +name = "openclassrooms-project-13" +version = "0.1.0" +description = "" +authors = [ + {name = "NM",email = "nicolas.marie.nm@gmail.com"} +] +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "django (==3.0)", + "flake8 (==3.7.0)", + "flake8-html (==0.4.3)", + "pytest (==9.0.3)", + "pytest-django (==4.12.0)", + "pytest-cov (==7.1.0)", + "six (==1.17.0)", + "sentry-sdk (>=2.60.0,<3.0.0)", + "python-dotenv (>=1.2.2,<2.0.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 c48c84ea40..66496eaeec 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/setup.cfg b/setup.cfg index 9346841bbc..6199fd8563 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,8 +1,10 @@ [flake8] +format = html +htmldir = flake8-report max-line-length = 99 -exclude = **/migrations/*,venv +exclude = **/migrations/*,env,cov_html [tool:pytest] DJANGO_SETTINGS_MODULE = oc_lettings_site.settings -python_files = tests.py -addopts = -v +python_files = tests*.py +addopts = -v --cov=lettings --cov=profiles --cov=oc_lettings_site --cov-report=html:cov_html diff --git a/templates/base.html b/templates/base.html index ab7addba01..403b342755 100644 --- a/templates/base.html +++ b/templates/base.html @@ -24,10 +24,10 @@
Logo Orange County Lettings diff --git a/templates/error_404.html b/templates/error_404.html new file mode 100644 index 0000000000..bc8477aadb --- /dev/null +++ b/templates/error_404.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% block title %}Holiday Homes{% endblock title %} + +{% block content %} + + +
+
+
+ {% if type == "letting" %} +

404 Error : {{ type }} n° {{ id }} not found !

+ {% elif type == "profile" %} +

404 Error : {{ type }} '{{ name }}' not found !

+ {% endif %} +
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/templates/error_500.html b/templates/error_500.html new file mode 100644 index 0000000000..f7d65b087f --- /dev/null +++ b/templates/error_500.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} +{% block title %}Holiday Homes{% endblock title %} + +{% block content %} + + +
+
+
+

500 Error : something wrong with the server - {{ error }}

+
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 71a8e61a46..fc9a76c7ab 100644 --- a/templates/index.html +++ b/templates/index.html @@ -14,10 +14,10 @@

Welcome to Holiday Homes