From 627959f52b872595eb72e58c9dc08c205b68c052 Mon Sep 17 00:00:00 2001 From: Iliyan Angelov Date: Sun, 23 Nov 2025 18:59:18 +0200 Subject: [PATCH] updates --- Backend/.env.example | 8 + .../add_group_booking_tables.cpython-312.pyc | Bin 0 -> 18482 bytes ...d_guest_profile_crm_tables.cpython-312.pyc | Bin 12179 -> 12179 bytes ...d_rate_plan_id_to_bookings.cpython-312.pyc | Bin 0 -> 1699 bytes .../versions/add_borica_payment_method.py | 20 + .../versions/add_group_booking_tables.py | 193 + .../versions/add_rate_plan_id_to_bookings.py | 30 + Backend/pytest.ini | 18 + Backend/requirements.txt | 7 + Backend/run.py | 2 +- Backend/src/__pycache__/main.cpython-312.pyc | Bin 19260 -> 21162 bytes .../__pycache__/database.cpython-312.pyc | Bin 2337 -> 2337 bytes .../__pycache__/settings.cpython-312.pyc | Bin 6981 -> 7794 bytes Backend/src/config/settings.py | 7 + Backend/src/main.py | 19 +- Backend/src/middleware/ip_whitelist.py | 195 + Backend/src/models/__init__.py | 14 +- .../__pycache__/__init__.cpython-312.pyc | Bin 2645 -> 5010 bytes .../__pycache__/booking.cpython-312.pyc | Bin 2754 -> 3065 bytes .../email_campaign.cpython-312.pyc | Bin 0 -> 12843 bytes .../gdpr_compliance.cpython-312.pyc | Bin 0 -> 4668 bytes .../__pycache__/group_booking.cpython-312.pyc | Bin 0 -> 8673 bytes .../housekeeping_task.cpython-312.pyc | Bin 0 -> 3102 bytes .../__pycache__/notification.cpython-312.pyc | Bin 0 -> 7195 bytes .../__pycache__/package.cpython-312.pyc | Bin 0 -> 4221 bytes .../__pycache__/payment.cpython-312.pyc | Bin 2848 -> 2870 bytes .../__pycache__/rate_plan.cpython-312.pyc | Bin 0 -> 4894 bytes .../models/__pycache__/room.cpython-312.pyc | Bin 2380 -> 2665 bytes .../room_attribute.cpython-312.pyc | Bin 0 -> 1629 bytes .../room_inspection.cpython-312.pyc | Bin 0 -> 3227 bytes .../room_maintenance.cpython-312.pyc | Bin 0 -> 3103 bytes .../__pycache__/room_type.cpython-312.pyc | Bin 1371 -> 1478 bytes .../security_event.cpython-312.pyc | Bin 0 -> 7008 bytes .../__pycache__/workflow.cpython-312.pyc | Bin 0 -> 7116 bytes Backend/src/models/booking.py | 6 +- Backend/src/models/email_campaign.py | 285 + Backend/src/models/gdpr_compliance.py | 87 + Backend/src/models/group_booking.py | 183 + Backend/src/models/housekeeping_task.py | 64 + Backend/src/models/notification.py | 125 + Backend/src/models/package.py | 90 + Backend/src/models/payment.py | 1 + Backend/src/models/rate_plan.py | 107 + Backend/src/models/room.py | 6 +- Backend/src/models/room_attribute.py | 30 + Backend/src/models/room_inspection.py | 68 + Backend/src/models/room_maintenance.py | 62 + Backend/src/models/room_type.py | 4 +- Backend/src/models/security_event.py | 135 + Backend/src/models/workflow.py | 127 + .../advanced_room_routes.cpython-312.pyc | Bin 0 -> 43079 bytes .../analytics_routes.cpython-312.pyc | Bin 0 -> 13731 bytes .../booking_routes.cpython-312.pyc | Bin 97304 -> 104003 bytes .../email_campaign_routes.cpython-312.pyc | Bin 0 -> 29501 bytes .../group_booking_routes.cpython-312.pyc | Bin 0 -> 30943 bytes .../guest_profile_routes.cpython-312.pyc | Bin 28343 -> 28259 bytes .../invoice_routes.cpython-312.pyc | Bin 10413 -> 10581 bytes .../notification_routes.cpython-312.pyc | Bin 0 -> 16024 bytes .../package_routes.cpython-312.pyc | Bin 0 -> 24575 bytes .../payment_routes.cpython-312.pyc | Bin 52535 -> 71155 bytes .../rate_plan_routes.cpython-312.pyc | Bin 0 -> 27707 bytes .../__pycache__/report_routes.cpython-312.pyc | Bin 26905 -> 29506 bytes .../__pycache__/room_routes.cpython-312.pyc | Bin 40163 -> 42503 bytes .../security_routes.cpython-312.pyc | Bin 0 -> 32617 bytes .../service_booking_routes.cpython-312.pyc | Bin 16209 -> 16206 bytes .../system_settings_routes.cpython-312.pyc | Bin 56625 -> 69544 bytes .../__pycache__/task_routes.cpython-312.pyc | Bin 0 -> 20299 bytes .../__pycache__/user_routes.cpython-312.pyc | Bin 14424 -> 14562 bytes .../workflow_routes.cpython-312.pyc | Bin 0 -> 15001 bytes Backend/src/routes/advanced_room_routes.py | 859 +++ Backend/src/routes/analytics_routes.py | 301 + Backend/src/routes/booking_routes.py | 207 +- Backend/src/routes/email_campaign_routes.py | 584 ++ Backend/src/routes/group_booking_routes.py | 575 ++ Backend/src/routes/guest_profile_routes.py | 38 +- Backend/src/routes/invoice_routes.py | 24 +- Backend/src/routes/notification_routes.py | 306 + Backend/src/routes/package_routes.py | 435 ++ Backend/src/routes/payment_routes.py | 315 +- Backend/src/routes/rate_plan_routes.py | 496 ++ Backend/src/routes/report_routes.py | 52 +- Backend/src/routes/room_routes.py | 51 +- Backend/src/routes/security_routes.py | 744 +++ Backend/src/routes/service_booking_routes.py | 10 +- Backend/src/routes/system_settings_routes.py | 362 +- Backend/src/routes/task_routes.py | 418 ++ Backend/src/routes/user_routes.py | 20 +- Backend/src/routes/workflow_routes.py | 314 + .../analytics_service.cpython-312.pyc | Bin 0 -> 40144 bytes .../borica_service.cpython-312.pyc | Bin 0 -> 18549 bytes .../email_campaign_service.cpython-312.pyc | Bin 0 -> 22089 bytes .../encryption_service.cpython-312.pyc | Bin 0 -> 4880 bytes .../__pycache__/gdpr_service.cpython-312.pyc | Bin 0 -> 10300 bytes .../group_booking_service.cpython-312.pyc | Bin 0 -> 22391 bytes .../guest_profile_service.cpython-312.pyc | Bin 15484 -> 18239 bytes .../notification_service.cpython-312.pyc | Bin 0 -> 17092 bytes .../__pycache__/oauth_service.cpython-312.pyc | Bin 0 -> 9426 bytes .../room_assignment_service.cpython-312.pyc | Bin 0 -> 11015 bytes ...ecurity_monitoring_service.cpython-312.pyc | Bin 0 -> 8977 bytes .../security_scan_service.cpython-312.pyc | Bin 0 -> 14262 bytes .../__pycache__/task_service.cpython-312.pyc | Bin 0 -> 17877 bytes .../workflow_service.cpython-312.pyc | Bin 0 -> 14934 bytes Backend/src/services/analytics_service.py | 739 +++ Backend/src/services/borica_service.py | 388 ++ .../src/services/email_campaign_service.py | 517 ++ Backend/src/services/encryption_service.py | 89 + Backend/src/services/gdpr_service.py | 215 + Backend/src/services/group_booking_service.py | 574 ++ Backend/src/services/guest_profile_service.py | 51 +- Backend/src/services/notification_service.py | 373 ++ Backend/src/services/oauth_service.py | 209 + .../src/services/room_assignment_service.py | 241 + .../services/security_monitoring_service.py | 189 + Backend/src/services/security_scan_service.py | 314 + Backend/src/services/task_service.py | 338 ++ Backend/src/services/workflow_service.py | 314 + Backend/src/tasks/security_scan_task.py | 66 + .../currency_helpers.cpython-312.pyc | Bin 0 -> 796 bytes .../response_helpers.cpython-312.pyc | Bin 0 -> 1291 bytes .../__pycache__/role_helpers.cpython-312.pyc | Bin 0 -> 2795 bytes Backend/src/utils/currency_helpers.py | 24 + Backend/src/utils/response_helpers.py | 51 + Backend/src/utils/role_helpers.py | 47 + Backend/venv/bin/coverage | 7 + Backend/venv/bin/coverage-3.12 | 7 + Backend/venv/bin/coverage3 | 7 + Backend/venv/bin/httpx | 7 + Backend/venv/bin/pip | 7 +- Backend/venv/bin/pip3 | 7 +- Backend/venv/bin/pip3.12 | 7 +- Backend/venv/bin/py.test | 7 + Backend/venv/bin/pygmentize | 7 + Backend/venv/bin/pytest | 7 + .../__pycache__/py.cpython-312.pyc | Bin 0 -> 501 bytes .../typing_extensions.cpython-312.pyc | Bin 163758 -> 163758 bytes .../site-packages/_pytest/__init__.py | 13 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 489 bytes .../__pycache__/_argcomplete.cpython-312.pyc | Bin 0 -> 4839 bytes .../__pycache__/_version.cpython-312.pyc | Bin 0 -> 798 bytes .../__pycache__/cacheprovider.cpython-312.pyc | Bin 0 -> 31700 bytes .../__pycache__/capture.cpython-312.pyc | Bin 0 -> 55739 bytes .../__pycache__/compat.cpython-312.pyc | Bin 0 -> 13019 bytes .../__pycache__/debugging.cpython-312.pyc | Bin 0 -> 18297 bytes .../__pycache__/deprecated.cpython-312.pyc | Bin 0 -> 3038 bytes .../__pycache__/doctest.cpython-312.pyc | Bin 0 -> 32661 bytes .../__pycache__/faulthandler.cpython-312.pyc | Bin 0 -> 5088 bytes .../__pycache__/fixtures.cpython-312.pyc | Bin 0 -> 82899 bytes .../freeze_support.cpython-312.pyc | Bin 0 -> 1842 bytes .../__pycache__/helpconfig.cpython-312.pyc | Bin 0 -> 13162 bytes .../__pycache__/hookspec.cpython-312.pyc | Bin 0 -> 45775 bytes .../__pycache__/junitxml.cpython-312.pyc | Bin 0 -> 33356 bytes .../__pycache__/legacypath.cpython-312.pyc | Bin 0 -> 24174 bytes .../__pycache__/logging.cpython-312.pyc | Bin 0 -> 45979 bytes .../_pytest/__pycache__/main.cpython-312.pyc | Bin 0 -> 47350 bytes .../__pycache__/monkeypatch.cpython-312.pyc | Bin 0 -> 17223 bytes .../_pytest/__pycache__/nodes.cpython-312.pyc | Bin 0 -> 30852 bytes .../__pycache__/outcomes.cpython-312.pyc | Bin 0 -> 11730 bytes .../__pycache__/pastebin.cpython-312.pyc | Bin 0 -> 5897 bytes .../__pycache__/pathlib.cpython-312.pyc | Bin 0 -> 41817 bytes .../__pycache__/pytester.cpython-312.pyc | Bin 0 -> 85019 bytes .../pytester_assertions.cpython-312.pyc | Bin 0 -> 2498 bytes .../__pycache__/python.cpython-312.pyc | Bin 0 -> 75220 bytes .../__pycache__/python_api.cpython-312.pyc | Bin 0 -> 35400 bytes .../__pycache__/raises.cpython-312.pyc | Bin 0 -> 60047 bytes .../__pycache__/recwarn.cpython-312.pyc | Bin 0 -> 16110 bytes .../__pycache__/reports.cpython-312.pyc | Bin 0 -> 27013 bytes .../__pycache__/runner.cpython-312.pyc | Bin 0 -> 24219 bytes .../_pytest/__pycache__/scope.cpython-312.pyc | Bin 0 -> 3580 bytes .../__pycache__/setuponly.cpython-312.pyc | Bin 0 -> 5369 bytes .../__pycache__/setupplan.cpython-312.pyc | Bin 0 -> 1914 bytes .../__pycache__/skipping.cpython-312.pyc | Bin 0 -> 14080 bytes .../_pytest/__pycache__/stash.cpython-312.pyc | Bin 0 -> 4377 bytes .../__pycache__/stepwise.cpython-312.pyc | Bin 0 -> 9625 bytes .../__pycache__/subtests.cpython-312.pyc | Bin 0 -> 17739 bytes .../__pycache__/terminal.cpython-312.pyc | Bin 0 -> 84511 bytes .../threadexception.cpython-312.pyc | Bin 0 -> 5762 bytes .../__pycache__/timing.cpython-312.pyc | Bin 0 -> 4796 bytes .../__pycache__/tmpdir.cpython-312.pyc | Bin 0 -> 12726 bytes .../__pycache__/tracemalloc.cpython-312.pyc | Bin 0 -> 995 bytes .../__pycache__/unittest.cpython-312.pyc | Bin 0 -> 25881 bytes .../unraisableexception.cpython-312.pyc | Bin 0 -> 6263 bytes .../__pycache__/warning_types.cpython-312.pyc | Bin 0 -> 6950 bytes .../__pycache__/warnings.cpython-312.pyc | Bin 0 -> 6860 bytes .../site-packages/_pytest/_argcomplete.py | 117 + .../site-packages/_pytest/_code/__init__.py | 26 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 695 bytes .../_code/__pycache__/code.cpython-312.pyc | Bin 0 -> 67990 bytes .../_code/__pycache__/source.cpython-312.pyc | Bin 0 -> 11955 bytes .../site-packages/_pytest/_code/code.py | 1565 +++++ .../site-packages/_pytest/_code/source.py | 225 + .../site-packages/_pytest/_io/__init__.py | 10 + .../_io/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 386 bytes .../_io/__pycache__/pprint.cpython-312.pyc | Bin 0 -> 24441 bytes .../_io/__pycache__/saferepr.cpython-312.pyc | Bin 0 -> 5832 bytes .../terminalwriter.cpython-312.pyc | Bin 0 -> 11255 bytes .../_io/__pycache__/wcwidth.cpython-312.pyc | Bin 0 -> 1697 bytes .../site-packages/_pytest/_io/pprint.py | 673 +++ .../site-packages/_pytest/_io/saferepr.py | 130 + .../_pytest/_io/terminalwriter.py | 258 + .../site-packages/_pytest/_io/wcwidth.py | 57 + .../site-packages/_pytest/_py/__init__.py | 0 .../_py/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 194 bytes .../_py/__pycache__/error.cpython-312.pyc | Bin 0 -> 4962 bytes .../_py/__pycache__/path.cpython-312.pyc | Bin 0 -> 68858 bytes .../site-packages/_pytest/_py/error.py | 119 + .../site-packages/_pytest/_py/path.py | 1475 +++++ .../site-packages/_pytest/_version.py | 34 + .../_pytest/assertion/__init__.py | 208 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 10127 bytes .../__pycache__/rewrite.cpython-312.pyc | Bin 0 -> 61306 bytes .../__pycache__/truncate.cpython-312.pyc | Bin 0 -> 4203 bytes .../__pycache__/util.cpython-312.pyc | Bin 0 -> 23857 bytes .../_pytest/assertion/rewrite.py | 1202 ++++ .../_pytest/assertion/truncate.py | 137 + .../site-packages/_pytest/assertion/util.py | 615 ++ .../site-packages/_pytest/cacheprovider.py | 646 ++ .../site-packages/_pytest/capture.py | 1144 ++++ .../site-packages/_pytest/compat.py | 314 + .../site-packages/_pytest/config/__init__.py | 2166 +++++++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 89859 bytes .../__pycache__/argparsing.cpython-312.pyc | Bin 0 -> 26422 bytes .../config/__pycache__/compat.cpython-312.pyc | Bin 0 -> 3592 bytes .../__pycache__/exceptions.cpython-312.pyc | Bin 0 -> 822 bytes .../__pycache__/findpaths.cpython-312.pyc | Bin 0 -> 13270 bytes .../_pytest/config/argparsing.py | 578 ++ .../site-packages/_pytest/config/compat.py | 85 + .../_pytest/config/exceptions.py | 15 + .../site-packages/_pytest/config/findpaths.py | 350 ++ .../site-packages/_pytest/debugging.py | 407 ++ .../site-packages/_pytest/deprecated.py | 99 + .../site-packages/_pytest/doctest.py | 736 +++ .../site-packages/_pytest/faulthandler.py | 119 + .../site-packages/_pytest/fixtures.py | 2047 +++++++ .../site-packages/_pytest/freeze_support.py | 45 + .../site-packages/_pytest/helpconfig.py | 293 + .../site-packages/_pytest/hookspec.py | 1342 +++++ .../site-packages/_pytest/junitxml.py | 695 +++ .../site-packages/_pytest/legacypath.py | 468 ++ .../site-packages/_pytest/logging.py | 960 +++ .../python3.12/site-packages/_pytest/main.py | 1203 ++++ .../site-packages/_pytest/mark/__init__.py | 301 + .../mark/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 13083 bytes .../__pycache__/expression.cpython-312.pyc | Bin 0 -> 17367 bytes .../__pycache__/structures.cpython-312.pyc | Bin 0 -> 27505 bytes .../site-packages/_pytest/mark/expression.py | 353 ++ .../site-packages/_pytest/mark/structures.py | 664 ++ .../site-packages/_pytest/monkeypatch.py | 435 ++ .../python3.12/site-packages/_pytest/nodes.py | 772 +++ .../site-packages/_pytest/outcomes.py | 308 + .../site-packages/_pytest/pastebin.py | 117 + .../site-packages/_pytest/pathlib.py | 1063 ++++ .../python3.12/site-packages/_pytest/py.typed | 0 .../site-packages/_pytest/pytester.py | 1791 ++++++ .../_pytest/pytester_assertions.py | 74 + .../site-packages/_pytest/python.py | 1772 ++++++ .../site-packages/_pytest/python_api.py | 820 +++ .../site-packages/_pytest/raises.py | 1517 +++++ .../site-packages/_pytest/recwarn.py | 367 ++ .../site-packages/_pytest/reports.py | 694 +++ .../site-packages/_pytest/runner.py | 580 ++ .../python3.12/site-packages/_pytest/scope.py | 91 + .../site-packages/_pytest/setuponly.py | 98 + .../site-packages/_pytest/setupplan.py | 39 + .../site-packages/_pytest/skipping.py | 321 + .../python3.12/site-packages/_pytest/stash.py | 116 + .../site-packages/_pytest/stepwise.py | 209 + .../site-packages/_pytest/subtests.py | 411 ++ .../site-packages/_pytest/terminal.py | 1770 ++++++ .../site-packages/_pytest/threadexception.py | 152 + .../site-packages/_pytest/timing.py | 95 + .../site-packages/_pytest/tmpdir.py | 312 + .../site-packages/_pytest/tracemalloc.py | 24 + .../site-packages/_pytest/unittest.py | 614 ++ .../_pytest/unraisableexception.py | 163 + .../site-packages/_pytest/warning_types.py | 172 + .../site-packages/_pytest/warnings.py | 151 + .../anyio-3.7.1.dist-info/RECORD | 1 + .../anyio-3.7.1.dist-info/REQUESTED | 0 .../__init__.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 3640 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 3526 -> 3526 bytes .../from_thread.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 22982 bytes .../__pycache__/from_thread.cpython-312.pyc | Bin 22868 -> 22868 bytes .../lowlevel.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 7385 bytes .../__pycache__/lowlevel.cpython-312.pyc | Bin 7272 -> 7272 bytes ...pytest_plugin.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 7688 bytes .../__pycache__/pytest_plugin.cpython-312.pyc | Bin 7574 -> 7574 bytes .../__pycache__/to_process.cpython-312.pyc | Bin 11477 -> 11477 bytes .../to_thread.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 2933 bytes .../__pycache__/to_thread.cpython-312.pyc | Bin 2819 -> 2819 bytes .../__init__.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 198 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 198 -> 198 bytes .../_asyncio.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 102923 bytes .../__pycache__/_asyncio.cpython-312.pyc | Bin 101195 -> 101195 bytes .../__pycache__/_trio.cpython-312.pyc | Bin 54026 -> 54026 bytes .../__init__.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 194 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 194 -> 194 bytes .../_compat.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 9966 bytes .../_core/__pycache__/_compat.cpython-312.pyc | Bin 9852 -> 9852 bytes .../_eventloop.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 6027 bytes .../__pycache__/_eventloop.cpython-312.pyc | Bin 5914 -> 5914 bytes .../_exceptions.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 5654 bytes .../__pycache__/_exceptions.cpython-312.pyc | Bin 5540 -> 5540 bytes .../_fileio.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 35007 bytes .../_core/__pycache__/_fileio.cpython-312.pyc | Bin 34894 -> 34894 bytes .../_resources.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 1061 bytes .../__pycache__/_resources.cpython-312.pyc | Bin 947 -> 947 bytes .../_signals.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 1345 bytes .../__pycache__/_signals.cpython-312.pyc | Bin 1231 -> 1231 bytes .../_sockets.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 24567 bytes .../__pycache__/_sockets.cpython-312.pyc | Bin 24454 -> 24454 bytes .../_streams.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 2177 bytes .../__pycache__/_streams.cpython-312.pyc | Bin 2064 -> 2064 bytes ..._subprocesses.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 6731 bytes .../__pycache__/_subprocesses.cpython-312.pyc | Bin 6617 -> 6617 bytes ...nchronization.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 26158 bytes .../_synchronization.cpython-312.pyc | Bin 25208 -> 25208 bytes .../_tasks.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 7902 bytes .../_core/__pycache__/_tasks.cpython-312.pyc | Bin 7789 -> 7789 bytes .../_testing.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 3855 bytes .../__pycache__/_testing.cpython-312.pyc | Bin 3741 -> 3741 bytes .../_typedattr.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 4007 bytes .../__pycache__/_typedattr.cpython-312.pyc | Bin 3894 -> 3894 bytes .../__init__.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 2201 bytes .../abc/__pycache__/__init__.cpython-312.pyc | Bin 2087 -> 2087 bytes .../_resources.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 1728 bytes .../__pycache__/_resources.cpython-312.pyc | Bin 1614 -> 1614 bytes .../_sockets.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 8259 bytes .../abc/__pycache__/_sockets.cpython-312.pyc | Bin 8146 -> 8146 bytes .../_streams.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 8531 bytes .../abc/__pycache__/_streams.cpython-312.pyc | Bin 8417 -> 8417 bytes ..._subprocesses.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 3363 bytes .../__pycache__/_subprocesses.cpython-312.pyc | Bin 3249 -> 3249 bytes .../_tasks.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 4987 bytes .../abc/__pycache__/_tasks.cpython-312.pyc | Bin 4874 -> 4874 bytes .../_testing.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 3146 bytes .../abc/__pycache__/_testing.cpython-312.pyc | Bin 3033 -> 3033 bytes .../__init__.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 196 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 196 -> 196 bytes .../__pycache__/buffered.cpython-312.pyc | Bin 6098 -> 6098 bytes .../streams/__pycache__/file.cpython-312.pyc | Bin 7507 -> 7507 bytes .../memory.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 13036 bytes .../__pycache__/memory.cpython-312.pyc | Bin 12922 -> 12922 bytes .../stapled.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 7638 bytes .../__pycache__/stapled.cpython-312.pyc | Bin 7524 -> 7524 bytes .../streams/__pycache__/text.cpython-312.pyc | Bin 8250 -> 8250 bytes .../tls.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 17052 bytes .../streams/__pycache__/tls.cpython-312.pyc | Bin 16939 -> 16939 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 317 -> 317 bytes .../__pycache__/__main__.cpython-312.pyc | Bin 632 -> 632 bytes .../certifi/__pycache__/core.cpython-312.pyc | Bin 2064 -> 2064 bytes .../INSTALLER | 0 .../coverage-7.12.0.dist-info/METADATA | 221 + .../coverage-7.12.0.dist-info/RECORD | 106 + .../coverage-7.12.0.dist-info/WHEEL | 7 + .../entry_points.txt | 4 + .../licenses/LICENSE.txt | 177 + .../coverage-7.12.0.dist-info/top_level.txt | 1 + .../__pycache__/fernet.cpython-312.pyc | Bin 11141 -> 11141 bytes .../kdf/__pycache__/__init__.cpython-312.pyc | Bin 1260 -> 1260 bytes .../kdf/__pycache__/pbkdf2.cpython-312.pyc | Bin 2739 -> 2739 bytes .../__pycache__/testclient.cpython-312.pyc | Bin 250 -> 250 bytes .../h11-0.14.0.dist-info/INSTALLER | 1 + .../LICENSE.txt | 0 .../METADATA | 21 +- .../site-packages/h11-0.14.0.dist-info/RECORD | 52 + .../site-packages/h11-0.14.0.dist-info/WHEEL | 5 + .../top_level.txt | 0 .../site-packages/h11-0.16.0.dist-info/RECORD | 29 - .../h11/__pycache__/__init__.cpython-312.pyc | Bin 1077 -> 1077 bytes .../h11/__pycache__/_abnf.cpython-312.pyc | Bin 1783 -> 1783 bytes .../__pycache__/_connection.cpython-312.pyc | Bin 23133 -> 22604 bytes .../h11/__pycache__/_events.cpython-312.pyc | Bin 13228 -> 13288 bytes .../h11/__pycache__/_headers.cpython-312.pyc | Bin 8004 -> 7869 bytes .../h11/__pycache__/_readers.cpython-312.pyc | Bin 9660 -> 9422 bytes .../_receivebuffer.cpython-312.pyc | Bin 4706 -> 4706 bytes .../h11/__pycache__/_state.cpython-312.pyc | Bin 8470 -> 8541 bytes .../h11/__pycache__/_util.cpython-312.pyc | Bin 4721 -> 4721 bytes .../h11/__pycache__/_version.cpython-312.pyc | Bin 215 -> 215 bytes .../h11/__pycache__/_writers.cpython-312.pyc | Bin 6297 -> 6297 bytes .../site-packages/h11/_connection.py | 32 +- .../python3.12/site-packages/h11/_events.py | 4 +- .../python3.12/site-packages/h11/_headers.py | 4 - .../python3.12/site-packages/h11/_readers.py | 23 +- .../python3.12/site-packages/h11/_state.py | 6 +- .../python3.12/site-packages/h11/_version.py | 2 +- .../site-packages/h11/tests/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 192 bytes .../tests/__pycache__/helpers.cpython-312.pyc | Bin 0 -> 4393 bytes .../test_against_stdlib_http.cpython-312.pyc | Bin 0 -> 6958 bytes .../test_connection.cpython-312.pyc | Bin 0 -> 58358 bytes .../__pycache__/test_events.cpython-312.pyc | Bin 0 -> 5547 bytes .../__pycache__/test_headers.cpython-312.pyc | Bin 0 -> 7134 bytes .../__pycache__/test_helpers.cpython-312.pyc | Bin 0 -> 1200 bytes .../tests/__pycache__/test_io.cpython-312.pyc | Bin 0 -> 20029 bytes .../test_receivebuffer.cpython-312.pyc | Bin 0 -> 3980 bytes .../__pycache__/test_state.cpython-312.pyc | Bin 0 -> 12630 bytes .../__pycache__/test_util.cpython-312.pyc | Bin 0 -> 6182 bytes .../site-packages/h11/tests/data/test-file | 1 + .../site-packages/h11/tests/helpers.py | 101 + .../h11/tests/test_against_stdlib_http.py | 115 + .../h11/tests/test_connection.py | 1122 ++++ .../site-packages/h11/tests/test_events.py | 150 + .../site-packages/h11/tests/test_headers.py | 157 + .../site-packages/h11/tests/test_helpers.py | 32 + .../site-packages/h11/tests/test_io.py | 572 ++ .../h11/tests/test_receivebuffer.py | 135 + .../site-packages/h11/tests/test_state.py | 271 + .../site-packages/h11/tests/test_util.py | 112 + .../httpcore-0.17.3.dist-info/INSTALLER | 1 + .../httpcore-0.17.3.dist-info/LICENSE.md | 27 + .../httpcore-0.17.3.dist-info/METADATA | 542 ++ .../httpcore-0.17.3.dist-info/RECORD | 69 + .../httpcore-0.17.3.dist-info/WHEEL | 5 + .../httpcore-0.17.3.dist-info/top_level.txt | 4 + .../site-packages/httpcore/__init__.py | 139 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 3145 bytes .../httpcore/__pycache__/_api.cpython-312.pyc | Bin 0 -> 3789 bytes .../__pycache__/_exceptions.cpython-312.pyc | Bin 0 -> 3125 bytes .../__pycache__/_models.cpython-312.pyc | Bin 0 -> 21753 bytes .../httpcore/__pycache__/_ssl.cpython-312.pyc | Bin 0 -> 612 bytes .../_synchronization.cpython-312.pyc | Bin 0 -> 12565 bytes .../__pycache__/_trace.cpython-312.pyc | Bin 0 -> 5764 bytes .../__pycache__/_utils.cpython-312.pyc | Bin 0 -> 1331 bytes .../python3.12/site-packages/httpcore/_api.py | 92 + .../site-packages/httpcore/_async/__init__.py | 39 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1625 bytes .../__pycache__/connection.cpython-312.pyc | Bin 0 -> 11870 bytes .../connection_pool.cpython-312.pyc | Bin 0 -> 18281 bytes .../_async/__pycache__/http11.cpython-312.pyc | Bin 0 -> 18032 bytes .../_async/__pycache__/http2.cpython-312.pyc | Bin 0 -> 31868 bytes .../__pycache__/http_proxy.cpython-312.pyc | Bin 0 -> 18145 bytes .../__pycache__/interfaces.cpython-312.pyc | Bin 0 -> 5861 bytes .../__pycache__/socks_proxy.cpython-312.pyc | Bin 0 -> 16894 bytes .../httpcore/_async/connection.py | 215 + .../httpcore/_async/connection_pool.py | 356 ++ .../site-packages/httpcore/_async/http11.py | 331 + .../site-packages/httpcore/_async/http2.py | 589 ++ .../httpcore/_async/http_proxy.py | 350 ++ .../httpcore/_async/interfaces.py | 135 + .../httpcore/_async/socks_proxy.py | 340 ++ .../httpcore/_backends/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 201 bytes .../__pycache__/anyio.cpython-312.pyc | Bin 0 -> 8902 bytes .../__pycache__/auto.cpython-312.pyc | Bin 0 -> 2779 bytes .../__pycache__/base.cpython-312.pyc | Bin 0 -> 5854 bytes .../__pycache__/mock.cpython-312.pyc | Bin 0 -> 8105 bytes .../__pycache__/sync.cpython-312.pyc | Bin 0 -> 6947 bytes .../__pycache__/trio.cpython-312.pyc | Bin 0 -> 9428 bytes .../site-packages/httpcore/_backends/anyio.py | 145 + .../site-packages/httpcore/_backends/auto.py | 52 + .../site-packages/httpcore/_backends/base.py | 103 + .../site-packages/httpcore/_backends/mock.py | 142 + .../site-packages/httpcore/_backends/sync.py | 133 + .../site-packages/httpcore/_backends/trio.py | 161 + .../site-packages/httpcore/_exceptions.py | 81 + .../site-packages/httpcore/_models.py | 483 ++ .../python3.12/site-packages/httpcore/_ssl.py | 9 + .../site-packages/httpcore/_sync/__init__.py | 39 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1579 bytes .../__pycache__/connection.cpython-312.pyc | Bin 0 -> 10493 bytes .../connection_pool.cpython-312.pyc | Bin 0 -> 16482 bytes .../_sync/__pycache__/http11.cpython-312.pyc | Bin 0 -> 15831 bytes .../_sync/__pycache__/http2.cpython-312.pyc | Bin 0 -> 27866 bytes .../__pycache__/http_proxy.cpython-312.pyc | Bin 0 -> 17326 bytes .../__pycache__/interfaces.cpython-312.pyc | Bin 0 -> 5411 bytes .../__pycache__/socks_proxy.cpython-312.pyc | Bin 0 -> 15912 bytes .../httpcore/_sync/connection.py | 215 + .../httpcore/_sync/connection_pool.py | 356 ++ .../site-packages/httpcore/_sync/http11.py | 331 + .../site-packages/httpcore/_sync/http2.py | 589 ++ .../httpcore/_sync/http_proxy.py | 350 ++ .../httpcore/_sync/interfaces.py | 135 + .../httpcore/_sync/socks_proxy.py | 340 ++ .../httpcore/_synchronization.py | 279 + .../site-packages/httpcore/_trace.py | 105 + .../site-packages/httpcore/_utils.py | 36 + .../site-packages/httpcore/py.typed | 0 .../httpx-0.24.1.dist-info/INSTALLER | 1 + .../httpx-0.24.1.dist-info/METADATA | 212 + .../httpx-0.24.1.dist-info/RECORD | 57 + .../httpx-0.24.1.dist-info/REQUESTED | 0 .../httpx-0.24.1.dist-info/WHEEL | 4 + .../httpx-0.24.1.dist-info/entry_points.txt | 2 + .../licenses/LICENSE.md | 12 + .../site-packages/httpx/__init__.py | 138 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 3221 bytes .../__pycache__/__version__.cpython-312.pyc | Bin 0 -> 317 bytes .../httpx/__pycache__/_api.cpython-312.pyc | Bin 0 -> 12698 bytes .../httpx/__pycache__/_auth.cpython-312.pyc | Bin 0 -> 16693 bytes .../httpx/__pycache__/_client.cpython-312.pyc | Bin 0 -> 72309 bytes .../httpx/__pycache__/_compat.cpython-312.pyc | Bin 0 -> 1739 bytes .../httpx/__pycache__/_config.cpython-312.pyc | Bin 0 -> 16556 bytes .../__pycache__/_content.cpython-312.pyc | Bin 0 -> 10553 bytes .../__pycache__/_decoders.cpython-312.pyc | Bin 0 -> 15117 bytes .../__pycache__/_exceptions.cpython-312.pyc | Bin 0 -> 12208 bytes .../httpx/__pycache__/_main.cpython-312.pyc | Bin 0 -> 21469 bytes .../httpx/__pycache__/_models.cpython-312.pyc | Bin 0 -> 58008 bytes .../__pycache__/_multipart.cpython-312.pyc | Bin 0 -> 12506 bytes .../__pycache__/_status_codes.cpython-312.pyc | Bin 0 -> 7268 bytes .../httpx/__pycache__/_types.cpython-312.pyc | Bin 0 -> 4163 bytes .../__pycache__/_urlparse.cpython-312.pyc | Bin 0 -> 15361 bytes .../httpx/__pycache__/_urls.cpython-312.pyc | Bin 0 -> 29286 bytes .../httpx/__pycache__/_utils.cpython-312.pyc | Bin 0 -> 20635 bytes .../site-packages/httpx/__version__.py | 3 + .../python3.12/site-packages/httpx/_api.py | 445 ++ .../python3.12/site-packages/httpx/_auth.py | 347 ++ .../python3.12/site-packages/httpx/_client.py | 2006 +++++++ .../python3.12/site-packages/httpx/_compat.py | 43 + .../python3.12/site-packages/httpx/_config.py | 369 ++ .../site-packages/httpx/_content.py | 238 + .../site-packages/httpx/_decoders.py | 324 + .../site-packages/httpx/_exceptions.py | 343 ++ .../python3.12/site-packages/httpx/_main.py | 506 ++ .../python3.12/site-packages/httpx/_models.py | 1209 ++++ .../site-packages/httpx/_multipart.py | 267 + .../site-packages/httpx/_status_codes.py | 158 + .../httpx/_transports/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 200 bytes .../__pycache__/asgi.cpython-312.pyc | Bin 0 -> 7728 bytes .../__pycache__/base.cpython-312.pyc | Bin 0 -> 4132 bytes .../__pycache__/default.cpython-312.pyc | Bin 0 -> 16210 bytes .../__pycache__/mock.cpython-312.pyc | Bin 0 -> 1917 bytes .../__pycache__/wsgi.cpython-312.pyc | Bin 0 -> 7138 bytes .../site-packages/httpx/_transports/asgi.py | 173 + .../site-packages/httpx/_transports/base.py | 82 + .../httpx/_transports/default.py | 365 ++ .../site-packages/httpx/_transports/mock.py | 38 + .../site-packages/httpx/_transports/wsgi.py | 143 + .../python3.12/site-packages/httpx/_types.py | 132 + .../site-packages/httpx/_urlparse.py | 462 ++ .../python3.12/site-packages/httpx/_urls.py | 642 ++ .../python3.12/site-packages/httpx/_utils.py | 477 ++ .../python3.12/site-packages/httpx/py.typed | 0 .../idna/__pycache__/__init__.cpython-312.pyc | Bin 885 -> 885 bytes .../idna/__pycache__/codec.cpython-312.pyc | Bin 4985 -> 4985 bytes .../idna/__pycache__/compat.cpython-312.pyc | Bin 889 -> 889 bytes .../idna/__pycache__/core.cpython-312.pyc | Bin 16197 -> 16197 bytes .../idna/__pycache__/idnadata.cpython-312.pyc | Bin 100914 -> 100914 bytes .../__pycache__/intranges.cpython-312.pyc | Bin 2637 -> 2637 bytes .../__pycache__/package_data.cpython-312.pyc | Bin 216 -> 216 bytes .../__pycache__/uts46data.cpython-312.pyc | Bin 161844 -> 161844 bytes .../iniconfig-2.3.0.dist-info/INSTALLER | 1 + .../iniconfig-2.3.0.dist-info/METADATA | 79 + .../iniconfig-2.3.0.dist-info/RECORD | 15 + .../WHEEL | 2 +- .../licenses/LICENSE | 21 + .../iniconfig-2.3.0.dist-info/top_level.txt | 1 + .../site-packages/iniconfig/__init__.py | 249 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 10885 bytes .../__pycache__/_parse.cpython-312.pyc | Bin 0 -> 5585 bytes .../__pycache__/_version.cpython-312.pyc | Bin 0 -> 800 bytes .../__pycache__/exceptions.cpython-312.pyc | Bin 0 -> 1204 bytes .../site-packages/iniconfig/_parse.py | 163 + .../site-packages/iniconfig/_version.py | 34 + .../site-packages/iniconfig/exceptions.py | 16 + .../site-packages/iniconfig/py.typed | 0 .../site-packages/pip-25.3.dist-info/RECORD | 6 +- .../pip/__pycache__/__init__.cpython-312.pyc | Bin 659 -> 659 bytes .../pip/__pycache__/__main__.cpython-312.pyc | Bin 851 -> 851 bytes .../__pip-runner__.cpython-312.pyc | Bin 2214 -> 2214 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 761 -> 761 bytes .../__pycache__/build_env.cpython-312.pyc | Bin 18234 -> 18234 bytes .../__pycache__/cache.cpython-312.pyc | Bin 12376 -> 12376 bytes .../__pycache__/configuration.cpython-312.pyc | Bin 18287 -> 18287 bytes .../__pycache__/exceptions.cpython-312.pyc | Bin 40608 -> 40608 bytes .../__pycache__/main.cpython-312.pyc | Bin 644 -> 644 bytes .../__pycache__/pyproject.cpython-312.pyc | Bin 4107 -> 4107 bytes .../self_outdated_check.cpython-312.pyc | Bin 10277 -> 10277 bytes .../__pycache__/wheel_builder.cpython-312.pyc | Bin 10724 -> 10724 bytes .../cli/__pycache__/__init__.cpython-312.pyc | Bin 285 -> 285 bytes .../autocompletion.cpython-312.pyc | Bin 9135 -> 9135 bytes .../__pycache__/base_command.cpython-312.pyc | Bin 10602 -> 10602 bytes .../__pycache__/cmdoptions.cpython-312.pyc | Bin 30353 -> 30353 bytes .../command_context.cpython-312.pyc | Bin 1830 -> 1830 bytes .../__pycache__/index_command.cpython-312.pyc | Bin 7218 -> 7218 bytes .../cli/__pycache__/main.cpython-312.pyc | Bin 2269 -> 2269 bytes .../__pycache__/main_parser.cpython-312.pyc | Bin 4843 -> 4843 bytes .../cli/__pycache__/parser.cpython-312.pyc | Bin 14869 -> 14869 bytes .../__pycache__/progress_bars.cpython-312.pyc | Bin 6114 -> 6114 bytes .../__pycache__/req_command.cpython-312.pyc | Bin 13865 -> 13865 bytes .../cli/__pycache__/spinners.cpython-312.pyc | Bin 11272 -> 11272 bytes .../__pycache__/status_codes.cpython-312.pyc | Bin 385 -> 385 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 4127 -> 4127 bytes .../__pycache__/cache.cpython-312.pyc | Bin 10182 -> 10182 bytes .../__pycache__/check.cpython-312.pyc | Bin 2590 -> 2590 bytes .../__pycache__/completion.cpython-312.pyc | Bin 5435 -> 5435 bytes .../__pycache__/configuration.cpython-312.pyc | Bin 13414 -> 13414 bytes .../__pycache__/debug.cpython-312.pyc | Bin 10017 -> 10017 bytes .../__pycache__/download.cpython-312.pyc | Bin 7271 -> 7271 bytes .../__pycache__/freeze.cpython-312.pyc | Bin 4300 -> 4300 bytes .../commands/__pycache__/hash.cpython-312.pyc | Bin 2958 -> 2958 bytes .../commands/__pycache__/help.cpython-312.pyc | Bin 1648 -> 1648 bytes .../__pycache__/index.cpython-312.pyc | Bin 7270 -> 7270 bytes .../__pycache__/inspect.cpython-312.pyc | Bin 3955 -> 3955 bytes .../__pycache__/install.cpython-312.pyc | Bin 29802 -> 29802 bytes .../commands/__pycache__/list.cpython-312.pyc | Bin 17034 -> 17034 bytes .../commands/__pycache__/lock.cpython-312.pyc | Bin 7907 -> 7907 bytes .../__pycache__/search.cpython-312.pyc | Bin 7643 -> 7643 bytes .../commands/__pycache__/show.cpython-312.pyc | Bin 11213 -> 11213 bytes .../__pycache__/uninstall.cpython-312.pyc | Bin 4708 -> 4708 bytes .../__pycache__/wheel.cpython-312.pyc | Bin 8367 -> 8367 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 951 -> 951 bytes .../__pycache__/base.cpython-312.pyc | Bin 2929 -> 2929 bytes .../__pycache__/installed.cpython-312.pyc | Bin 1779 -> 1779 bytes .../__pycache__/sdist.cpython-312.pyc | Bin 8387 -> 8387 bytes .../__pycache__/wheel.cpython-312.pyc | Bin 2328 -> 2328 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 239 -> 239 bytes .../__pycache__/collector.cpython-312.pyc | Bin 21236 -> 21236 bytes .../package_finder.cpython-312.pyc | Bin 42145 -> 42145 bytes .../index/__pycache__/sources.cpython-312.pyc | Bin 12333 -> 12333 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 15327 -> 15327 bytes .../__pycache__/_distutils.cpython-312.pyc | Bin 6784 -> 6784 bytes .../__pycache__/_sysconfig.cpython-312.pyc | Bin 7935 -> 7935 bytes .../__pycache__/base.cpython-312.pyc | Bin 3718 -> 3718 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 6784 -> 6784 bytes .../__pycache__/_json.cpython-312.pyc | Bin 2887 -> 2887 bytes .../metadata/__pycache__/base.cpython-312.pyc | Bin 34471 -> 34471 bytes .../__pycache__/pkg_resources.cpython-312.pyc | Bin 15815 -> 15815 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 368 -> 368 bytes .../__pycache__/_compat.cpython-312.pyc | Bin 4247 -> 4247 bytes .../__pycache__/_dists.cpython-312.pyc | Bin 12827 -> 12827 bytes .../__pycache__/_envs.cpython-312.pyc | Bin 8052 -> 8052 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 273 -> 273 bytes .../__pycache__/candidate.cpython-312.pyc | Bin 1614 -> 1614 bytes .../__pycache__/direct_url.cpython-312.pyc | Bin 10508 -> 10508 bytes .../format_control.cpython-312.pyc | Bin 4138 -> 4138 bytes .../models/__pycache__/index.cpython-312.pyc | Bin 1704 -> 1704 bytes .../installation_report.cpython-312.pyc | Bin 2295 -> 2295 bytes .../models/__pycache__/link.cpython-312.pyc | Bin 26653 -> 26653 bytes .../models/__pycache__/pylock.cpython-312.pyc | Bin 7869 -> 7869 bytes .../models/__pycache__/scheme.cpython-312.pyc | Bin 1033 -> 1033 bytes .../__pycache__/search_scope.cpython-312.pyc | Bin 4971 -> 4971 bytes .../selection_prefs.cpython-312.pyc | Bin 1888 -> 1888 bytes .../__pycache__/target_python.cpython-312.pyc | Bin 4865 -> 4865 bytes .../models/__pycache__/wheel.cpython-312.pyc | Bin 4678 -> 4678 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 261 -> 261 bytes .../network/__pycache__/auth.cpython-312.pyc | Bin 21482 -> 21482 bytes .../network/__pycache__/cache.cpython-312.pyc | Bin 8012 -> 8012 bytes .../__pycache__/download.cpython-312.pyc | Bin 16099 -> 16099 bytes .../__pycache__/lazy_wheel.cpython-312.pyc | Bin 11624 -> 11624 bytes .../__pycache__/session.cpython-312.pyc | Bin 19216 -> 19216 bytes .../network/__pycache__/utils.cpython-312.pyc | Bin 2267 -> 2267 bytes .../__pycache__/xmlrpc.cpython-312.pyc | Bin 2940 -> 2940 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 207 -> 207 bytes .../__pycache__/check.cpython-312.pyc | Bin 7216 -> 7216 bytes .../__pycache__/freeze.cpython-312.pyc | Bin 10258 -> 10258 bytes .../__pycache__/prepare.cpython-312.pyc | Bin 26671 -> 26671 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 273 -> 273 bytes .../install/__pycache__/wheel.cpython-312.pyc | Bin 34225 -> 34225 bytes .../req/__pycache__/__init__.cpython-312.pyc | Bin 4029 -> 4029 bytes .../__pycache__/constructors.cpython-312.pyc | Bin 21694 -> 21694 bytes .../req_dependency_group.cpython-312.pyc | Bin 4025 -> 4025 bytes .../req/__pycache__/req_file.cpython-312.pyc | Bin 23816 -> 23816 bytes .../__pycache__/req_install.cpython-312.pyc | Bin 34675 -> 34675 bytes .../req/__pycache__/req_set.cpython-312.pyc | Bin 5434 -> 5434 bytes .../__pycache__/req_uninstall.cpython-312.pyc | Bin 31768 -> 31768 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 207 -> 207 bytes .../__pycache__/base.cpython-312.pyc | Bin 1178 -> 1178 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 214 -> 214 bytes .../__pycache__/resolver.cpython-312.pyc | Bin 22562 -> 22562 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 218 -> 218 bytes .../__pycache__/base.cpython-312.pyc | Bin 7992 -> 7992 bytes .../__pycache__/candidates.cpython-312.pyc | Bin 29463 -> 29463 bytes .../__pycache__/factory.cpython-312.pyc | Bin 33851 -> 33851 bytes .../found_candidates.cpython-312.pyc | Bin 6755 -> 6755 bytes .../__pycache__/provider.cpython-312.pyc | Bin 11648 -> 11648 bytes .../__pycache__/reporter.cpython-312.pyc | Bin 5815 -> 5815 bytes .../__pycache__/requirements.cpython-312.pyc | Bin 14762 -> 14762 bytes .../__pycache__/resolver.cpython-312.pyc | Bin 12401 -> 12401 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 202 -> 202 bytes .../__pycache__/_jaraco_text.cpython-312.pyc | Bin 4542 -> 4542 bytes .../utils/__pycache__/_log.cpython-312.pyc | Bin 1873 -> 1873 bytes .../utils/__pycache__/appdirs.cpython-312.pyc | Bin 2462 -> 2462 bytes .../utils/__pycache__/compat.cpython-312.pyc | Bin 3029 -> 3029 bytes .../compatibility_tags.cpython-312.pyc | Bin 6648 -> 6648 bytes .../__pycache__/datetime.cpython-312.pyc | Bin 686 -> 686 bytes .../__pycache__/deprecation.cpython-312.pyc | Bin 4215 -> 4215 bytes .../direct_url_helpers.cpython-312.pyc | Bin 3546 -> 3546 bytes .../__pycache__/egg_link.cpython-312.pyc | Bin 3171 -> 3171 bytes .../__pycache__/entrypoints.cpython-312.pyc | Bin 4062 -> 4062 bytes .../__pycache__/filesystem.cpython-312.pyc | Bin 7996 -> 7996 bytes .../__pycache__/filetypes.cpython-312.pyc | Bin 1136 -> 1136 bytes .../utils/__pycache__/glibc.cpython-312.pyc | Bin 2357 -> 2357 bytes .../utils/__pycache__/hashes.cpython-312.pyc | Bin 7475 -> 7475 bytes .../utils/__pycache__/logging.cpython-312.pyc | Bin 13870 -> 13870 bytes .../utils/__pycache__/misc.cpython-312.pyc | Bin 32707 -> 32707 bytes .../__pycache__/packaging.cpython-312.pyc | Bin 1892 -> 1892 bytes .../utils/__pycache__/retry.cpython-312.pyc | Bin 1979 -> 1979 bytes .../__pycache__/subprocess.cpython-312.pyc | Bin 8539 -> 8539 bytes .../__pycache__/temp_dir.cpython-312.pyc | Bin 11954 -> 11954 bytes .../__pycache__/unpacking.cpython-312.pyc | Bin 14351 -> 14351 bytes .../utils/__pycache__/urls.cpython-312.pyc | Bin 2088 -> 2088 bytes .../__pycache__/virtualenv.cpython-312.pyc | Bin 4401 -> 4401 bytes .../utils/__pycache__/wheel.cpython-312.pyc | Bin 5869 -> 5869 bytes .../vcs/__pycache__/__init__.cpython-312.pyc | Bin 541 -> 541 bytes .../vcs/__pycache__/bazaar.cpython-312.pyc | Bin 5169 -> 5169 bytes .../vcs/__pycache__/git.cpython-312.pyc | Bin 19928 -> 19928 bytes .../vcs/__pycache__/mercurial.cpython-312.pyc | Bin 7774 -> 7774 bytes .../__pycache__/subversion.cpython-312.pyc | Bin 12348 -> 12348 bytes .../versioncontrol.cpython-312.pyc | Bin 28748 -> 28748 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 4604 -> 4604 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 913 -> 913 bytes .../__pycache__/_cmd.cpython-312.pyc | Bin 2657 -> 2657 bytes .../__pycache__/adapter.cpython-312.pyc | Bin 6722 -> 6722 bytes .../__pycache__/cache.cpython-312.pyc | Bin 3820 -> 3820 bytes .../__pycache__/controller.cpython-312.pyc | Bin 16464 -> 16464 bytes .../__pycache__/filewrapper.cpython-312.pyc | Bin 4358 -> 4358 bytes .../__pycache__/heuristics.cpython-312.pyc | Bin 6708 -> 6708 bytes .../__pycache__/serialize.cpython-312.pyc | Bin 5276 -> 5276 bytes .../__pycache__/wrapper.cpython-312.pyc | Bin 1685 -> 1685 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 446 -> 446 bytes .../__pycache__/file_cache.cpython-312.pyc | Bin 7063 -> 7063 bytes .../__pycache__/redis_cache.cpython-312.pyc | Bin 2749 -> 2749 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 329 -> 329 bytes .../__pycache__/__main__.cpython-312.pyc | Bin 656 -> 656 bytes .../certifi/__pycache__/core.cpython-312.pyc | Bin 2093 -> 2093 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 388 -> 388 bytes .../__pycache__/__main__.cpython-312.pyc | Bin 2710 -> 2710 bytes .../_implementation.cpython-312.pyc | Bin 9662 -> 9662 bytes .../_lint_dependency_groups.cpython-312.pyc | Bin 2875 -> 2875 bytes .../__pycache__/_pip_wrapper.cpython-312.pyc | Bin 3442 -> 3442 bytes .../__pycache__/_toml_compat.cpython-312.pyc | Bin 489 -> 489 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 1280 -> 1280 bytes .../__pycache__/compat.cpython-312.pyc | Bin 45611 -> 45611 bytes .../__pycache__/resources.cpython-312.pyc | Bin 17336 -> 17336 bytes .../__pycache__/scripts.cpython-312.pyc | Bin 19791 -> 19791 bytes .../distlib/__pycache__/util.cpython-312.pyc | Bin 88285 -> 88285 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 971 -> 971 bytes .../__pycache__/__main__.cpython-312.pyc | Bin 303 -> 303 bytes .../distro/__pycache__/distro.cpython-312.pyc | Bin 53856 -> 53856 bytes .../idna/__pycache__/__init__.cpython-312.pyc | Bin 897 -> 897 bytes .../idna/__pycache__/codec.cpython-312.pyc | Bin 4997 -> 4997 bytes .../idna/__pycache__/compat.cpython-312.pyc | Bin 901 -> 901 bytes .../idna/__pycache__/core.cpython-312.pyc | Bin 16187 -> 16187 bytes .../idna/__pycache__/idnadata.cpython-312.pyc | Bin 99487 -> 99487 bytes .../__pycache__/intranges.cpython-312.pyc | Bin 2649 -> 2649 bytes .../__pycache__/package_data.cpython-312.pyc | Bin 228 -> 228 bytes .../__pycache__/uts46data.cpython-312.pyc | Bin 158857 -> 158857 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 1750 -> 1750 bytes .../__pycache__/exceptions.cpython-312.pyc | Bin 2036 -> 2036 bytes .../msgpack/__pycache__/ext.cpython-312.pyc | Bin 8304 -> 8304 bytes .../__pycache__/fallback.cpython-312.pyc | Bin 41530 -> 41530 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 569 -> 569 bytes .../__pycache__/_elffile.cpython-312.pyc | Bin 5034 -> 5034 bytes .../__pycache__/_manylinux.cpython-312.pyc | Bin 9762 -> 9762 bytes .../__pycache__/_musllinux.cpython-312.pyc | Bin 4580 -> 4580 bytes .../__pycache__/_parser.cpython-312.pyc | Bin 14009 -> 14009 bytes .../__pycache__/_structures.cpython-312.pyc | Bin 3252 -> 3252 bytes .../__pycache__/_tokenizer.cpython-312.pyc | Bin 7959 -> 7959 bytes .../__pycache__/markers.cpython-312.pyc | Bin 12776 -> 12776 bytes .../__pycache__/metadata.cpython-312.pyc | Bin 27261 -> 27261 bytes .../__pycache__/requirements.cpython-312.pyc | Bin 4421 -> 4421 bytes .../__pycache__/specifiers.cpython-312.pyc | Bin 39075 -> 39075 bytes .../__pycache__/tags.cpython-312.pyc | Bin 24827 -> 24827 bytes .../__pycache__/utils.cpython-312.pyc | Bin 6646 -> 6646 bytes .../__pycache__/version.cpython-312.pyc | Bin 20491 -> 20491 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 4140 -> 4140 bytes .../__pycache__/_spdx.cpython-312.pyc | Bin 47375 -> 47375 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 161524 -> 161524 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 19858 -> 19858 bytes .../__pycache__/__main__.cpython-312.pyc | Bin 1979 -> 1979 bytes .../__pycache__/android.cpython-312.pyc | Bin 10706 -> 10706 bytes .../__pycache__/api.cpython-312.pyc | Bin 13371 -> 13371 bytes .../__pycache__/macos.cpython-312.pyc | Bin 9014 -> 9014 bytes .../__pycache__/unix.cpython-312.pyc | Bin 14757 -> 14757 bytes .../__pycache__/version.cpython-312.pyc | Bin 814 -> 814 bytes .../__pycache__/windows.cpython-312.pyc | Bin 13684 -> 13684 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 3500 -> 3500 bytes .../__pycache__/__main__.cpython-312.pyc | Bin 746 -> 746 bytes .../__pycache__/console.cpython-312.pyc | Bin 2645 -> 2645 bytes .../__pycache__/filter.cpython-312.pyc | Bin 3238 -> 3238 bytes .../__pycache__/formatter.cpython-312.pyc | Bin 4732 -> 4732 bytes .../__pycache__/lexer.cpython-312.pyc | Bin 38477 -> 38477 bytes .../__pycache__/modeline.cpython-312.pyc | Bin 1581 -> 1581 bytes .../__pycache__/plugin.cpython-312.pyc | Bin 2640 -> 2640 bytes .../__pycache__/regexopt.cpython-312.pyc | Bin 4093 -> 4093 bytes .../__pycache__/scanner.cpython-312.pyc | Bin 4768 -> 4768 bytes .../__pycache__/sphinxext.cpython-312.pyc | Bin 12153 -> 12153 bytes .../__pycache__/style.cpython-312.pyc | Bin 6729 -> 6729 bytes .../__pycache__/token.cpython-312.pyc | Bin 8206 -> 8206 bytes .../__pycache__/unistring.cpython-312.pyc | Bin 33023 -> 33023 bytes .../pygments/__pycache__/util.cpython-312.pyc | Bin 14095 -> 14095 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 37991 -> 37991 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 6965 -> 6965 bytes .../__pycache__/_mapping.cpython-312.pyc | Bin 4227 -> 4227 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 14747 -> 14747 bytes .../__pycache__/_mapping.cpython-312.pyc | Bin 69856 -> 69856 bytes .../lexers/__pycache__/python.cpython-312.pyc | Bin 42986 -> 42986 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 2683 -> 2683 bytes .../__pycache__/_mapping.cpython-312.pyc | Bin 3660 -> 3660 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 759 -> 759 bytes .../__pycache__/_impl.cpython-312.pyc | Bin 18091 -> 18091 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 1088 -> 1088 bytes .../__pycache__/_in_process.cpython-312.pyc | Bin 15370 -> 15370 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 5265 -> 5265 bytes .../__pycache__/__version__.cpython-312.pyc | Bin 596 -> 596 bytes .../_internal_utils.cpython-312.pyc | Bin 2036 -> 2036 bytes .../__pycache__/adapters.cpython-312.pyc | Bin 27884 -> 27884 bytes .../requests/__pycache__/api.cpython-312.pyc | Bin 7216 -> 7216 bytes .../requests/__pycache__/auth.cpython-312.pyc | Bin 13935 -> 13935 bytes .../__pycache__/certs.cpython-312.pyc | Bin 690 -> 690 bytes .../__pycache__/compat.cpython-312.pyc | Bin 1997 -> 1997 bytes .../__pycache__/cookies.cpython-312.pyc | Bin 25288 -> 25288 bytes .../__pycache__/exceptions.cpython-312.pyc | Bin 7610 -> 7610 bytes .../requests/__pycache__/help.cpython-312.pyc | Bin 4240 -> 4240 bytes .../__pycache__/hooks.cpython-312.pyc | Bin 1064 -> 1064 bytes .../__pycache__/models.cpython-312.pyc | Bin 35585 -> 35585 bytes .../__pycache__/packages.cpython-312.pyc | Bin 1299 -> 1299 bytes .../__pycache__/sessions.cpython-312.pyc | Bin 27902 -> 27902 bytes .../__pycache__/status_codes.cpython-312.pyc | Bin 6043 -> 6043 bytes .../__pycache__/structures.cpython-312.pyc | Bin 5629 -> 5629 bytes .../__pycache__/utils.cpython-312.pyc | Bin 36202 -> 36202 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 646 -> 646 bytes .../__pycache__/providers.cpython-312.pyc | Bin 10135 -> 10135 bytes .../__pycache__/reporters.cpython-312.pyc | Bin 3301 -> 3301 bytes .../__pycache__/structs.cpython-312.pyc | Bin 12462 -> 12462 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 751 -> 751 bytes .../__pycache__/abstract.cpython-312.pyc | Bin 2457 -> 2457 bytes .../__pycache__/criterion.cpython-312.pyc | Bin 3282 -> 3282 bytes .../__pycache__/exceptions.cpython-312.pyc | Bin 4092 -> 4092 bytes .../__pycache__/resolution.cpython-312.pyc | Bin 25178 -> 25178 bytes .../rich/__pycache__/__init__.cpython-312.pyc | Bin 7027 -> 7027 bytes .../rich/__pycache__/__main__.cpython-312.pyc | Bin 9562 -> 9562 bytes .../__pycache__/_cell_widths.cpython-312.pyc | Bin 7884 -> 7884 bytes .../__pycache__/_emoji_codes.cpython-312.pyc | Bin 205988 -> 205988 bytes .../_emoji_replace.cpython-312.pyc | Bin 1741 -> 1741 bytes .../_export_format.cpython-312.pyc | Bin 2361 -> 2361 bytes .../__pycache__/_extension.cpython-312.pyc | Bin 549 -> 549 bytes .../rich/__pycache__/_fileno.cpython-312.pyc | Bin 867 -> 867 bytes .../rich/__pycache__/_inspect.cpython-312.pyc | Bin 12039 -> 12039 bytes .../__pycache__/_log_render.cpython-312.pyc | Bin 4159 -> 4159 bytes .../rich/__pycache__/_loop.cpython-312.pyc | Bin 1897 -> 1897 bytes .../__pycache__/_null_file.cpython-312.pyc | Bin 3641 -> 3641 bytes .../__pycache__/_palettes.cpython-312.pyc | Bin 5172 -> 5172 bytes .../rich/__pycache__/_pick.cpython-312.pyc | Bin 738 -> 738 bytes .../rich/__pycache__/_ratio.cpython-312.pyc | Bin 6442 -> 6442 bytes .../__pycache__/_spinners.cpython-312.pyc | Bin 13191 -> 13191 bytes .../rich/__pycache__/_stack.cpython-312.pyc | Bin 977 -> 977 bytes .../rich/__pycache__/_timer.cpython-312.pyc | Bin 877 -> 877 bytes .../_win32_console.cpython-312.pyc | Bin 28823 -> 28823 bytes .../rich/__pycache__/_windows.cpython-312.pyc | Bin 2502 -> 2502 bytes .../_windows_renderer.cpython-312.pyc | Bin 3585 -> 3585 bytes .../rich/__pycache__/_wrap.cpython-312.pyc | Bin 3348 -> 3348 bytes .../rich/__pycache__/abc.cpython-312.pyc | Bin 1620 -> 1620 bytes .../rich/__pycache__/align.cpython-312.pyc | Bin 12291 -> 12291 bytes .../rich/__pycache__/ansi.cpython-312.pyc | Bin 9133 -> 9133 bytes .../rich/__pycache__/bar.cpython-312.pyc | Bin 4284 -> 4284 bytes .../rich/__pycache__/box.cpython-312.pyc | Bin 11723 -> 11723 bytes .../rich/__pycache__/cells.cpython-312.pyc | Bin 5590 -> 5590 bytes .../rich/__pycache__/color.cpython-312.pyc | Bin 26565 -> 26565 bytes .../__pycache__/color_triplet.cpython-312.pyc | Bin 1713 -> 1713 bytes .../rich/__pycache__/columns.cpython-312.pyc | Bin 8599 -> 8599 bytes .../rich/__pycache__/console.cpython-312.pyc | Bin 115312 -> 115312 bytes .../__pycache__/constrain.cpython-312.pyc | Bin 2270 -> 2270 bytes .../__pycache__/containers.cpython-312.pyc | Bin 9243 -> 9243 bytes .../rich/__pycache__/control.cpython-312.pyc | Bin 10796 -> 10796 bytes .../default_styles.cpython-312.pyc | Bin 10540 -> 10540 bytes .../rich/__pycache__/diagnose.cpython-312.pyc | Bin 1532 -> 1532 bytes .../rich/__pycache__/emoji.cpython-312.pyc | Bin 4090 -> 4090 bytes .../rich/__pycache__/errors.cpython-312.pyc | Bin 1857 -> 1857 bytes .../__pycache__/file_proxy.cpython-312.pyc | Bin 3589 -> 3589 bytes .../rich/__pycache__/filesize.cpython-312.pyc | Bin 3069 -> 3069 bytes .../__pycache__/highlighter.cpython-312.pyc | Bin 9912 -> 9912 bytes .../rich/__pycache__/json.cpython-312.pyc | Bin 6047 -> 6047 bytes .../rich/__pycache__/jupyter.cpython-312.pyc | Bin 5221 -> 5221 bytes .../rich/__pycache__/layout.cpython-312.pyc | Bin 20232 -> 20232 bytes .../rich/__pycache__/live.cpython-312.pyc | Bin 20144 -> 20144 bytes .../__pycache__/live_render.cpython-312.pyc | Bin 4762 -> 4762 bytes .../rich/__pycache__/logging.cpython-312.pyc | Bin 14086 -> 14086 bytes .../rich/__pycache__/markup.cpython-312.pyc | Bin 9602 -> 9602 bytes .../rich/__pycache__/measure.cpython-312.pyc | Bin 6388 -> 6388 bytes .../rich/__pycache__/padding.cpython-312.pyc | Bin 6955 -> 6955 bytes .../rich/__pycache__/pager.cpython-312.pyc | Bin 1832 -> 1832 bytes .../rich/__pycache__/palette.cpython-312.pyc | Bin 5326 -> 5326 bytes .../rich/__pycache__/panel.cpython-312.pyc | Bin 12741 -> 12741 bytes .../rich/__pycache__/pretty.cpython-312.pyc | Bin 40645 -> 40645 bytes .../rich/__pycache__/progress.cpython-312.pyc | Bin 75135 -> 75135 bytes .../__pycache__/progress_bar.cpython-312.pyc | Bin 10401 -> 10401 bytes .../rich/__pycache__/prompt.cpython-312.pyc | Bin 16009 -> 16009 bytes .../rich/__pycache__/protocol.cpython-312.pyc | Bin 1804 -> 1804 bytes .../rich/__pycache__/region.cpython-312.pyc | Bin 579 -> 579 bytes .../rich/__pycache__/repr.cpython-312.pyc | Bin 6636 -> 6636 bytes .../rich/__pycache__/rule.cpython-312.pyc | Bin 6580 -> 6580 bytes .../rich/__pycache__/scope.cpython-312.pyc | Bin 3842 -> 3842 bytes .../rich/__pycache__/screen.cpython-312.pyc | Bin 2496 -> 2496 bytes .../rich/__pycache__/segment.cpython-312.pyc | Bin 28595 -> 28595 bytes .../rich/__pycache__/spinner.cpython-312.pyc | Bin 5937 -> 5937 bytes .../rich/__pycache__/status.cpython-312.pyc | Bin 6080 -> 6080 bytes .../rich/__pycache__/style.cpython-312.pyc | Bin 33443 -> 33443 bytes .../rich/__pycache__/styled.cpython-312.pyc | Bin 2151 -> 2151 bytes .../rich/__pycache__/syntax.cpython-312.pyc | Bin 41004 -> 41004 bytes .../rich/__pycache__/table.cpython-312.pyc | Bin 43894 -> 43894 bytes .../terminal_theme.cpython-312.pyc | Bin 3360 -> 3360 bytes .../rich/__pycache__/text.cpython-312.pyc | Bin 61266 -> 61266 bytes .../rich/__pycache__/theme.cpython-312.pyc | Bin 6344 -> 6344 bytes .../rich/__pycache__/themes.cpython-312.pyc | Bin 326 -> 326 bytes .../__pycache__/traceback.cpython-312.pyc | Bin 36252 -> 36252 bytes .../rich/__pycache__/tree.cpython-312.pyc | Bin 11808 -> 11808 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 350 -> 350 bytes .../tomli/__pycache__/_parser.cpython-312.pyc | Bin 29453 -> 29453 bytes .../tomli/__pycache__/_re.cpython-312.pyc | Bin 4088 -> 4088 bytes .../tomli/__pycache__/_types.cpython-312.pyc | Bin 378 -> 378 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 339 -> 339 bytes .../__pycache__/_writer.cpython-312.pyc | Bin 10381 -> 10381 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 1464 -> 1464 bytes .../__pycache__/_api.cpython-312.pyc | Bin 17557 -> 17557 bytes .../__pycache__/_macos.cpython-312.pyc | Bin 19004 -> 19004 bytes .../__pycache__/_openssl.cpython-312.pyc | Bin 2278 -> 2278 bytes .../_ssl_constants.cpython-312.pyc | Bin 1111 -> 1111 bytes .../__pycache__/_windows.cpython-312.pyc | Bin 15787 -> 15787 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 3417 -> 3417 bytes .../__pycache__/_collections.cpython-312.pyc | Bin 16500 -> 16500 bytes .../__pycache__/_version.cpython-312.pyc | Bin 230 -> 230 bytes .../__pycache__/connection.cpython-312.pyc | Bin 20419 -> 20419 bytes .../connectionpool.cpython-312.pyc | Bin 36555 -> 36555 bytes .../__pycache__/exceptions.cpython-312.pyc | Bin 13505 -> 13505 bytes .../__pycache__/fields.cpython-312.pyc | Bin 10425 -> 10425 bytes .../__pycache__/filepost.cpython-312.pyc | Bin 4030 -> 4030 bytes .../__pycache__/poolmanager.cpython-312.pyc | Bin 20484 -> 20484 bytes .../__pycache__/request.cpython-312.pyc | Bin 7306 -> 7306 bytes .../__pycache__/response.cpython-312.pyc | Bin 33980 -> 33980 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 210 -> 210 bytes .../_appengine_environ.cpython-312.pyc | Bin 1860 -> 1860 bytes .../__pycache__/appengine.cpython-312.pyc | Bin 11576 -> 11576 bytes .../__pycache__/ntlmpool.cpython-312.pyc | Bin 5731 -> 5731 bytes .../__pycache__/pyopenssl.cpython-312.pyc | Bin 24462 -> 24462 bytes .../securetransport.cpython-312.pyc | Bin 35565 -> 35565 bytes .../contrib/__pycache__/socks.cpython-312.pyc | Bin 7523 -> 7523 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 227 -> 227 bytes .../__pycache__/bindings.cpython-312.pyc | Bin 17439 -> 17439 bytes .../__pycache__/low_level.cpython-312.pyc | Bin 14813 -> 14813 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 211 -> 211 bytes .../packages/__pycache__/six.cpython-312.pyc | Bin 41331 -> 41331 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 221 -> 221 bytes .../__pycache__/makefile.cpython-312.pyc | Bin 1837 -> 1837 bytes .../weakref_finalize.cpython-312.pyc | Bin 7343 -> 7343 bytes .../util/__pycache__/__init__.cpython-312.pyc | Bin 1158 -> 1158 bytes .../__pycache__/connection.cpython-312.pyc | Bin 4768 -> 4768 bytes .../util/__pycache__/proxy.cpython-312.pyc | Bin 1564 -> 1564 bytes .../util/__pycache__/queue.cpython-312.pyc | Bin 1364 -> 1364 bytes .../util/__pycache__/request.cpython-312.pyc | Bin 4195 -> 4195 bytes .../util/__pycache__/response.cpython-312.pyc | Bin 3001 -> 3001 bytes .../util/__pycache__/retry.cpython-312.pyc | Bin 21730 -> 21730 bytes .../util/__pycache__/ssl_.cpython-312.pyc | Bin 15389 -> 15389 bytes .../ssl_match_hostname.cpython-312.pyc | Bin 5083 -> 5083 bytes .../__pycache__/ssltransport.cpython-312.pyc | Bin 10784 -> 10784 bytes .../util/__pycache__/timeout.cpython-312.pyc | Bin 11151 -> 11151 bytes .../util/__pycache__/url.cpython-312.pyc | Bin 15807 -> 15807 bytes .../util/__pycache__/wait.cpython-312.pyc | Bin 4415 -> 4415 bytes .../pluggy-1.6.0.dist-info/INSTALLER | 1 + .../pluggy-1.6.0.dist-info/METADATA | 152 + .../pluggy-1.6.0.dist-info/RECORD | 23 + .../pluggy-1.6.0.dist-info/WHEEL | 5 + .../pluggy-1.6.0.dist-info/licenses/LICENSE | 21 + .../pluggy-1.6.0.dist-info/top_level.txt | 1 + .../site-packages/pluggy/__init__.py | 30 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 865 bytes .../__pycache__/_callers.cpython-312.pyc | Bin 0 -> 7037 bytes .../pluggy/__pycache__/_hooks.cpython-312.pyc | Bin 0 -> 27440 bytes .../__pycache__/_manager.cpython-312.pyc | Bin 0 -> 25638 bytes .../__pycache__/_result.cpython-312.pyc | Bin 0 -> 4152 bytes .../__pycache__/_tracing.cpython-312.pyc | Bin 0 -> 4029 bytes .../__pycache__/_version.cpython-312.pyc | Bin 0 -> 650 bytes .../__pycache__/_warnings.cpython-312.pyc | Bin 0 -> 1303 bytes .../site-packages/pluggy/_callers.py | 169 + .../python3.12/site-packages/pluggy/_hooks.py | 714 +++ .../site-packages/pluggy/_manager.py | 523 ++ .../site-packages/pluggy/_result.py | 107 + .../site-packages/pluggy/_tracing.py | 72 + .../site-packages/pluggy/_version.py | 21 + .../site-packages/pluggy/_warnings.py | 27 + .../python3.12/site-packages/pluggy/py.typed | 0 .../venv/lib/python3.12/site-packages/py.py | 15 + .../functional_validators.cpython-312.pyc | Bin 26752 -> 26752 bytes .../pygments-2.19.2.dist-info/INSTALLER | 1 + .../pygments-2.19.2.dist-info/METADATA | 58 + .../pygments-2.19.2.dist-info/RECORD | 684 +++ .../pygments-2.19.2.dist-info/WHEEL | 4 + .../entry_points.txt | 2 + .../licenses/AUTHORS | 291 + .../licenses/LICENSE | 25 + .../site-packages/pygments/__init__.py | 82 + .../site-packages/pygments/__main__.py | 17 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 3464 bytes .../__pycache__/__main__.cpython-312.pyc | Bin 0 -> 783 bytes .../__pycache__/cmdline.cpython-312.pyc | Bin 0 -> 26548 bytes .../__pycache__/console.cpython-312.pyc | Bin 0 -> 2633 bytes .../__pycache__/filter.cpython-312.pyc | Bin 0 -> 3226 bytes .../__pycache__/formatter.cpython-312.pyc | Bin 0 -> 4696 bytes .../__pycache__/lexer.cpython-312.pyc | Bin 0 -> 38760 bytes .../__pycache__/modeline.cpython-312.pyc | Bin 0 -> 1569 bytes .../__pycache__/plugin.cpython-312.pyc | Bin 0 -> 2628 bytes .../__pycache__/regexopt.cpython-312.pyc | Bin 0 -> 4081 bytes .../__pycache__/scanner.cpython-312.pyc | Bin 0 -> 4756 bytes .../__pycache__/sphinxext.cpython-312.pyc | Bin 0 -> 12111 bytes .../__pycache__/style.cpython-312.pyc | Bin 0 -> 6705 bytes .../__pycache__/token.cpython-312.pyc | Bin 0 -> 8194 bytes .../__pycache__/unistring.cpython-312.pyc | Bin 0 -> 33011 bytes .../pygments/__pycache__/util.cpython-312.pyc | Bin 0 -> 14083 bytes .../site-packages/pygments/cmdline.py | 668 +++ .../site-packages/pygments/console.py | 70 + .../site-packages/pygments/filter.py | 70 + .../pygments/filters/__init__.py | 940 +++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 37931 bytes .../site-packages/pygments/formatter.py | 129 + .../pygments/formatters/__init__.py | 157 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 6917 bytes .../__pycache__/_mapping.cpython-312.pyc | Bin 0 -> 4215 bytes .../__pycache__/bbcode.cpython-312.pyc | Bin 0 -> 4208 bytes .../__pycache__/groff.cpython-312.pyc | Bin 0 -> 7308 bytes .../__pycache__/html.cpython-312.pyc | Bin 0 -> 41377 bytes .../__pycache__/img.cpython-312.pyc | Bin 0 -> 28689 bytes .../__pycache__/irc.cpython-312.pyc | Bin 0 -> 6029 bytes .../__pycache__/latex.cpython-312.pyc | Bin 0 -> 20123 bytes .../__pycache__/other.cpython-312.pyc | Bin 0 -> 6851 bytes .../__pycache__/pangomarkup.cpython-312.pyc | Bin 0 -> 2956 bytes .../__pycache__/rtf.cpython-312.pyc | Bin 0 -> 13788 bytes .../__pycache__/svg.cpython-312.pyc | Bin 0 -> 9115 bytes .../__pycache__/terminal.cpython-312.pyc | Bin 0 -> 5781 bytes .../__pycache__/terminal256.cpython-312.pyc | Bin 0 -> 15121 bytes .../pygments/formatters/_mapping.py | 23 + .../pygments/formatters/bbcode.py | 108 + .../pygments/formatters/groff.py | 170 + .../site-packages/pygments/formatters/html.py | 995 +++ .../site-packages/pygments/formatters/img.py | 686 +++ .../site-packages/pygments/formatters/irc.py | 154 + .../pygments/formatters/latex.py | 518 ++ .../pygments/formatters/other.py | 160 + .../pygments/formatters/pangomarkup.py | 83 + .../site-packages/pygments/formatters/rtf.py | 349 ++ .../site-packages/pygments/formatters/svg.py | 185 + .../pygments/formatters/terminal.py | 127 + .../pygments/formatters/terminal256.py | 338 ++ .../site-packages/pygments/lexer.py | 961 +++ .../site-packages/pygments/lexers/__init__.py | 362 ++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 14687 bytes .../__pycache__/_ada_builtins.cpython-312.pyc | Bin 0 -> 1245 bytes .../__pycache__/_asy_builtins.cpython-312.pyc | Bin 0 -> 17638 bytes .../__pycache__/_cl_builtins.cpython-312.pyc | Bin 0 -> 11674 bytes .../_cocoa_builtins.cpython-312.pyc | Bin 0 -> 97578 bytes .../_csound_builtins.cpython-312.pyc | Bin 0 -> 16386 bytes .../__pycache__/_css_builtins.cpython-312.pyc | Bin 0 -> 9395 bytes .../_googlesql_builtins.cpython-312.pyc | Bin 0 -> 10833 bytes .../_julia_builtins.cpython-312.pyc | Bin 0 -> 8260 bytes .../_lasso_builtins.cpython-312.pyc | Bin 0 -> 76738 bytes .../_lilypond_builtins.cpython-312.pyc | Bin 0 -> 88416 bytes .../__pycache__/_lua_builtins.cpython-312.pyc | Bin 0 -> 8397 bytes .../_luau_builtins.cpython-312.pyc | Bin 0 -> 1059 bytes .../__pycache__/_mapping.cpython-312.pyc | Bin 0 -> 66961 bytes .../__pycache__/_mql_builtins.cpython-312.pyc | Bin 0 -> 18019 bytes .../_mysql_builtins.cpython-312.pyc | Bin 0 -> 19565 bytes .../_openedge_builtins.cpython-312.pyc | Bin 0 -> 34101 bytes .../__pycache__/_php_builtins.cpython-312.pyc | Bin 0 -> 65534 bytes .../_postgres_builtins.cpython-312.pyc | Bin 0 -> 11306 bytes .../_qlik_builtins.cpython-312.pyc | Bin 0 -> 6386 bytes .../_scheme_builtins.cpython-312.pyc | Bin 0 -> 23183 bytes .../_scilab_builtins.cpython-312.pyc | Bin 0 -> 35230 bytes .../_sourcemod_builtins.cpython-312.pyc | Bin 0 -> 21848 bytes .../__pycache__/_sql_builtins.cpython-312.pyc | Bin 0 -> 5577 bytes .../_stan_builtins.cpython-312.pyc | Bin 0 -> 9962 bytes .../_stata_builtins.cpython-312.pyc | Bin 0 -> 21246 bytes .../_tsql_builtins.cpython-312.pyc | Bin 0 -> 8878 bytes .../__pycache__/_usd_builtins.cpython-312.pyc | Bin 0 -> 1408 bytes .../_vbscript_builtins.cpython-312.pyc | Bin 0 -> 2937 bytes .../__pycache__/_vim_builtins.cpython-312.pyc | Bin 0 -> 30740 bytes .../__pycache__/actionscript.cpython-312.pyc | Bin 0 -> 11125 bytes .../lexers/__pycache__/ada.cpython-312.pyc | Bin 0 -> 5521 bytes .../lexers/__pycache__/agile.cpython-312.pyc | Bin 0 -> 1316 bytes .../__pycache__/algebra.cpython-312.pyc | Bin 0 -> 11005 bytes .../__pycache__/ambient.cpython-312.pyc | Bin 0 -> 3133 bytes .../lexers/__pycache__/amdgpu.cpython-312.pyc | Bin 0 -> 2256 bytes .../lexers/__pycache__/ampl.cpython-312.pyc | Bin 0 -> 4104 bytes .../__pycache__/apdlexer.cpython-312.pyc | Bin 0 -> 19045 bytes .../lexers/__pycache__/apl.cpython-312.pyc | Bin 0 -> 2525 bytes .../__pycache__/archetype.cpython-312.pyc | Bin 0 -> 9197 bytes .../lexers/__pycache__/arrow.cpython-312.pyc | Bin 0 -> 3575 bytes .../lexers/__pycache__/arturo.cpython-312.pyc | Bin 0 -> 9752 bytes .../lexers/__pycache__/asc.cpython-312.pyc | Bin 0 -> 2055 bytes .../lexers/__pycache__/asm.cpython-312.pyc | Bin 0 -> 36019 bytes .../lexers/__pycache__/asn1.cpython-312.pyc | Bin 0 -> 4496 bytes .../__pycache__/automation.cpython-312.pyc | Bin 0 -> 18423 bytes .../lexers/__pycache__/bare.cpython-312.pyc | Bin 0 -> 2882 bytes .../lexers/__pycache__/basic.cpython-312.pyc | Bin 0 -> 26979 bytes .../lexers/__pycache__/bdd.cpython-312.pyc | Bin 0 -> 2091 bytes .../lexers/__pycache__/berry.cpython-312.pyc | Bin 0 -> 3565 bytes .../lexers/__pycache__/bibtex.cpython-312.pyc | Bin 0 -> 5231 bytes .../__pycache__/blueprint.cpython-312.pyc | Bin 0 -> 5319 bytes .../lexers/__pycache__/boa.cpython-312.pyc | Bin 0 -> 3541 bytes .../lexers/__pycache__/bqn.cpython-312.pyc | Bin 0 -> 2555 bytes .../__pycache__/business.cpython-312.pyc | Bin 0 -> 22072 bytes .../lexers/__pycache__/c_cpp.cpython-312.pyc | Bin 0 -> 16103 bytes .../lexers/__pycache__/c_like.cpython-312.pyc | Bin 0 -> 27575 bytes .../__pycache__/capnproto.cpython-312.pyc | Bin 0 -> 2414 bytes .../lexers/__pycache__/carbon.cpython-312.pyc | Bin 0 -> 3552 bytes .../lexers/__pycache__/cddl.cpython-312.pyc | Bin 0 -> 4244 bytes .../lexers/__pycache__/chapel.cpython-312.pyc | Bin 0 -> 4278 bytes .../lexers/__pycache__/clean.cpython-312.pyc | Bin 0 -> 6087 bytes .../lexers/__pycache__/codeql.cpython-312.pyc | Bin 0 -> 2760 bytes .../lexers/__pycache__/comal.cpython-312.pyc | Bin 0 -> 3234 bytes .../__pycache__/compiled.cpython-312.pyc | Bin 0 -> 2007 bytes .../__pycache__/configs.cpython-312.pyc | Bin 0 -> 44525 bytes .../__pycache__/console.cpython-312.pyc | Bin 0 -> 4257 bytes .../lexers/__pycache__/cplint.cpython-312.pyc | Bin 0 -> 1769 bytes .../__pycache__/crystal.cpython-312.pyc | Bin 0 -> 15191 bytes .../lexers/__pycache__/csound.cpython-312.pyc | Bin 0 -> 14128 bytes .../lexers/__pycache__/css.cpython-312.pyc | Bin 0 -> 22114 bytes .../lexers/__pycache__/d.cpython-312.pyc | Bin 0 -> 8343 bytes .../lexers/__pycache__/dalvik.cpython-312.pyc | Bin 0 -> 4553 bytes .../lexers/__pycache__/data.cpython-312.pyc | Bin 0 -> 21177 bytes .../lexers/__pycache__/dax.cpython-312.pyc | Bin 0 -> 6254 bytes .../__pycache__/devicetree.cpython-312.pyc | Bin 0 -> 4054 bytes .../lexers/__pycache__/diff.cpython-312.pyc | Bin 0 -> 5689 bytes .../lexers/__pycache__/dns.cpython-312.pyc | Bin 0 -> 3788 bytes .../lexers/__pycache__/dotnet.cpython-312.pyc | Bin 0 -> 35236 bytes .../lexers/__pycache__/dsls.cpython-312.pyc | Bin 0 -> 33799 bytes .../lexers/__pycache__/dylan.cpython-312.pyc | Bin 0 -> 9743 bytes .../lexers/__pycache__/ecl.cpython-312.pyc | Bin 0 -> 5597 bytes .../lexers/__pycache__/eiffel.cpython-312.pyc | Bin 0 -> 3004 bytes .../lexers/__pycache__/elm.cpython-312.pyc | Bin 0 -> 3248 bytes .../lexers/__pycache__/elpi.cpython-312.pyc | Bin 0 -> 7246 bytes .../lexers/__pycache__/email.cpython-312.pyc | Bin 0 -> 5994 bytes .../lexers/__pycache__/erlang.cpython-312.pyc | Bin 0 -> 20559 bytes .../__pycache__/esoteric.cpython-312.pyc | Bin 0 -> 9776 bytes .../lexers/__pycache__/ezhil.cpython-312.pyc | Bin 0 -> 3870 bytes .../lexers/__pycache__/factor.cpython-312.pyc | Bin 0 -> 16937 bytes .../lexers/__pycache__/fantom.cpython-312.pyc | Bin 0 -> 8006 bytes .../lexers/__pycache__/felix.cpython-312.pyc | Bin 0 -> 8261 bytes .../lexers/__pycache__/fift.cpython-312.pyc | Bin 0 -> 1973 bytes .../__pycache__/floscript.cpython-312.pyc | Bin 0 -> 2991 bytes .../lexers/__pycache__/forth.cpython-312.pyc | Bin 0 -> 5367 bytes .../__pycache__/fortran.cpython-312.pyc | Bin 0 -> 8728 bytes .../lexers/__pycache__/foxpro.cpython-312.pyc | Bin 0 -> 20821 bytes .../__pycache__/freefem.cpython-312.pyc | Bin 0 -> 12796 bytes .../lexers/__pycache__/func.cpython-312.pyc | Bin 0 -> 3347 bytes .../__pycache__/functional.cpython-312.pyc | Bin 0 -> 1057 bytes .../__pycache__/futhark.cpython-312.pyc | Bin 0 -> 4089 bytes .../__pycache__/gcodelexer.cpython-312.pyc | Bin 0 -> 1329 bytes .../__pycache__/gdscript.cpython-312.pyc | Bin 0 -> 7223 bytes .../lexers/__pycache__/gleam.cpython-312.pyc | Bin 0 -> 2721 bytes .../lexers/__pycache__/go.cpython-312.pyc | Bin 0 -> 3396 bytes .../grammar_notation.cpython-312.pyc | Bin 0 -> 7726 bytes .../lexers/__pycache__/graph.cpython-312.pyc | Bin 0 -> 3823 bytes .../__pycache__/graphics.cpython-312.pyc | Bin 0 -> 29712 bytes .../__pycache__/graphql.cpython-312.pyc | Bin 0 -> 4456 bytes .../__pycache__/graphviz.cpython-312.pyc | Bin 0 -> 2230 bytes .../lexers/__pycache__/gsql.cpython-312.pyc | Bin 0 -> 3790 bytes .../lexers/__pycache__/hare.cpython-312.pyc | Bin 0 -> 2963 bytes .../__pycache__/haskell.cpython-312.pyc | Bin 0 -> 30509 bytes .../lexers/__pycache__/haxe.cpython-312.pyc | Bin 0 -> 22293 bytes .../lexers/__pycache__/hdl.cpython-312.pyc | Bin 0 -> 17488 bytes .../__pycache__/hexdump.cpython-312.pyc | Bin 0 -> 3664 bytes .../lexers/__pycache__/html.cpython-312.pyc | Bin 0 -> 20742 bytes .../lexers/__pycache__/idl.cpython-312.pyc | Bin 0 -> 12482 bytes .../lexers/__pycache__/igor.cpython-312.pyc | Bin 0 -> 25707 bytes .../__pycache__/inferno.cpython-312.pyc | Bin 0 -> 3260 bytes .../__pycache__/installers.cpython-312.pyc | Bin 0 -> 13789 bytes .../__pycache__/int_fiction.cpython-312.pyc | Bin 0 -> 48112 bytes .../lexers/__pycache__/iolang.cpython-312.pyc | Bin 0 -> 2224 bytes .../lexers/__pycache__/j.cpython-312.pyc | Bin 0 -> 4316 bytes .../__pycache__/javascript.cpython-312.pyc | Bin 0 -> 57008 bytes .../__pycache__/jmespath.cpython-312.pyc | Bin 0 -> 2414 bytes .../lexers/__pycache__/jslt.cpython-312.pyc | Bin 0 -> 3772 bytes .../lexers/__pycache__/json5.cpython-312.pyc | Bin 0 -> 2894 bytes .../__pycache__/jsonnet.cpython-312.pyc | Bin 0 -> 4870 bytes .../lexers/__pycache__/jsx.cpython-312.pyc | Bin 0 -> 2926 bytes .../lexers/__pycache__/julia.cpython-312.pyc | Bin 0 -> 10948 bytes .../lexers/__pycache__/jvm.cpython-312.pyc | Bin 0 -> 63885 bytes .../lexers/__pycache__/kuin.cpython-312.pyc | Bin 0 -> 9894 bytes .../lexers/__pycache__/kusto.cpython-312.pyc | Bin 0 -> 2856 bytes .../lexers/__pycache__/ldap.cpython-312.pyc | Bin 0 -> 6440 bytes .../lexers/__pycache__/lean.cpython-312.pyc | Bin 0 -> 8005 bytes .../__pycache__/lilypond.cpython-312.pyc | Bin 0 -> 8389 bytes .../lexers/__pycache__/lisp.cpython-312.pyc | Bin 0 -> 121654 bytes .../__pycache__/macaulay2.cpython-312.pyc | Bin 0 -> 23169 bytes .../lexers/__pycache__/make.cpython-312.pyc | Bin 0 -> 6683 bytes .../lexers/__pycache__/maple.cpython-312.pyc | Bin 0 -> 4992 bytes .../lexers/__pycache__/markup.cpython-312.pyc | Bin 0 -> 60430 bytes .../lexers/__pycache__/math.cpython-312.pyc | Bin 0 -> 1053 bytes .../lexers/__pycache__/matlab.cpython-312.pyc | Bin 0 -> 55752 bytes .../lexers/__pycache__/maxima.cpython-312.pyc | Bin 0 -> 3201 bytes .../lexers/__pycache__/meson.cpython-312.pyc | Bin 0 -> 3534 bytes .../lexers/__pycache__/mime.cpython-312.pyc | Bin 0 -> 10147 bytes .../__pycache__/minecraft.cpython-312.pyc | Bin 0 -> 10709 bytes .../lexers/__pycache__/mips.cpython-312.pyc | Bin 0 -> 3448 bytes .../lexers/__pycache__/ml.cpython-312.pyc | Bin 0 -> 26179 bytes .../__pycache__/modeling.cpython-312.pyc | Bin 0 -> 12083 bytes .../__pycache__/modula2.cpython-312.pyc | Bin 0 -> 26513 bytes .../lexers/__pycache__/mojo.cpython-312.pyc | Bin 0 -> 14374 bytes .../lexers/__pycache__/monte.cpython-312.pyc | Bin 0 -> 5126 bytes .../lexers/__pycache__/mosel.cpython-312.pyc | Bin 0 -> 6975 bytes .../lexers/__pycache__/ncl.cpython-312.pyc | Bin 0 -> 45927 bytes .../lexers/__pycache__/nimrod.cpython-312.pyc | Bin 0 -> 6473 bytes .../lexers/__pycache__/nit.cpython-312.pyc | Bin 0 -> 2769 bytes .../lexers/__pycache__/nix.cpython-312.pyc | Bin 0 -> 5478 bytes .../__pycache__/numbair.cpython-312.pyc | Bin 0 -> 2144 bytes .../lexers/__pycache__/oberon.cpython-312.pyc | Bin 0 -> 3758 bytes .../__pycache__/objective.cpython-312.pyc | Bin 0 -> 19482 bytes .../lexers/__pycache__/ooc.cpython-312.pyc | Bin 0 -> 3129 bytes .../__pycache__/openscad.cpython-312.pyc | Bin 0 -> 3741 bytes .../lexers/__pycache__/other.cpython-312.pyc | Bin 0 -> 2463 bytes .../__pycache__/parasail.cpython-312.pyc | Bin 0 -> 2898 bytes .../__pycache__/parsers.cpython-312.pyc | Bin 0 -> 24413 bytes .../lexers/__pycache__/pascal.cpython-312.pyc | Bin 0 -> 23953 bytes .../lexers/__pycache__/pawn.cpython-312.pyc | Bin 0 -> 7876 bytes .../lexers/__pycache__/pddl.cpython-312.pyc | Bin 0 -> 2824 bytes .../lexers/__pycache__/perl.cpython-312.pyc | Bin 0 -> 39029 bytes .../lexers/__pycache__/phix.cpython-312.pyc | Bin 0 -> 18435 bytes .../lexers/__pycache__/php.cpython-312.pyc | Bin 0 -> 14334 bytes .../__pycache__/pointless.cpython-312.pyc | Bin 0 -> 2308 bytes .../lexers/__pycache__/pony.cpython-312.pyc | Bin 0 -> 3437 bytes .../lexers/__pycache__/praat.cpython-312.pyc | Bin 0 -> 10299 bytes .../__pycache__/procfile.cpython-312.pyc | Bin 0 -> 1642 bytes .../lexers/__pycache__/prolog.cpython-312.pyc | Bin 0 -> 10544 bytes .../lexers/__pycache__/promql.cpython-312.pyc | Bin 0 -> 3347 bytes .../lexers/__pycache__/prql.cpython-312.pyc | Bin 0 -> 8395 bytes .../lexers/__pycache__/ptx.cpython-312.pyc | Bin 0 -> 3783 bytes .../lexers/__pycache__/python.cpython-312.pyc | Bin 0 -> 42926 bytes .../lexers/__pycache__/q.cpython-312.pyc | Bin 0 -> 5857 bytes .../lexers/__pycache__/qlik.cpython-312.pyc | Bin 0 -> 3523 bytes .../lexers/__pycache__/qvt.cpython-312.pyc | Bin 0 -> 5391 bytes .../lexers/__pycache__/r.cpython-312.pyc | Bin 0 -> 6101 bytes .../lexers/__pycache__/rdf.cpython-312.pyc | Bin 0 -> 12271 bytes .../lexers/__pycache__/rebol.cpython-312.pyc | Bin 0 -> 19262 bytes .../lexers/__pycache__/rego.cpython-312.pyc | Bin 0 -> 1887 bytes .../__pycache__/resource.cpython-312.pyc | Bin 0 -> 3599 bytes .../lexers/__pycache__/ride.cpython-312.pyc | Bin 0 -> 4501 bytes .../lexers/__pycache__/rita.cpython-312.pyc | Bin 0 -> 1482 bytes .../lexers/__pycache__/rnc.cpython-312.pyc | Bin 0 -> 2024 bytes .../__pycache__/roboconf.cpython-312.pyc | Bin 0 -> 2375 bytes .../robotframework.cpython-312.pyc | Bin 0 -> 29618 bytes .../lexers/__pycache__/ruby.cpython-312.pyc | Bin 0 -> 22602 bytes .../lexers/__pycache__/rust.cpython-312.pyc | Bin 0 -> 7317 bytes .../lexers/__pycache__/sas.cpython-312.pyc | Bin 0 -> 7052 bytes .../lexers/__pycache__/savi.cpython-312.pyc | Bin 0 -> 3993 bytes .../lexers/__pycache__/scdoc.cpython-312.pyc | Bin 0 -> 2831 bytes .../__pycache__/scripting.cpython-312.pyc | Bin 0 -> 71852 bytes .../lexers/__pycache__/sgf.cpython-312.pyc | Bin 0 -> 2092 bytes .../lexers/__pycache__/shell.cpython-312.pyc | Bin 0 -> 37113 bytes .../lexers/__pycache__/sieve.cpython-312.pyc | Bin 0 -> 2762 bytes .../lexers/__pycache__/slash.cpython-312.pyc | Bin 0 -> 8405 bytes .../__pycache__/smalltalk.cpython-312.pyc | Bin 0 -> 6714 bytes .../lexers/__pycache__/smithy.cpython-312.pyc | Bin 0 -> 3139 bytes .../lexers/__pycache__/smv.cpython-312.pyc | Bin 0 -> 2821 bytes .../lexers/__pycache__/snobol.cpython-312.pyc | Bin 0 -> 2517 bytes .../__pycache__/solidity.cpython-312.pyc | Bin 0 -> 3420 bytes .../lexers/__pycache__/soong.cpython-312.pyc | Bin 0 -> 2285 bytes .../lexers/__pycache__/sophia.cpython-312.pyc | Bin 0 -> 3865 bytes .../__pycache__/special.cpython-312.pyc | Bin 0 -> 5442 bytes .../lexers/__pycache__/spice.cpython-312.pyc | Bin 0 -> 3190 bytes .../lexers/__pycache__/sql.cpython-312.pyc | Bin 0 -> 40711 bytes .../__pycache__/srcinfo.cpython-312.pyc | Bin 0 -> 2020 bytes .../lexers/__pycache__/stata.cpython-312.pyc | Bin 0 -> 5172 bytes .../__pycache__/supercollider.cpython-312.pyc | Bin 0 -> 3922 bytes .../__pycache__/tablegen.cpython-312.pyc | Bin 0 -> 3364 bytes .../lexers/__pycache__/tact.cpython-312.pyc | Bin 0 -> 9049 bytes .../lexers/__pycache__/tal.cpython-312.pyc | Bin 0 -> 2986 bytes .../lexers/__pycache__/tcl.cpython-312.pyc | Bin 0 -> 5160 bytes .../lexers/__pycache__/teal.cpython-312.pyc | Bin 0 -> 3569 bytes .../__pycache__/templates.cpython-312.pyc | Bin 0 -> 83666 bytes .../__pycache__/teraterm.cpython-312.pyc | Bin 0 -> 5577 bytes .../__pycache__/testing.cpython-312.pyc | Bin 0 -> 10085 bytes .../lexers/__pycache__/text.cpython-312.pyc | Bin 0 -> 1567 bytes .../__pycache__/textedit.cpython-312.pyc | Bin 0 -> 8529 bytes .../__pycache__/textfmts.cpython-312.pyc | Bin 0 -> 15612 bytes .../__pycache__/theorem.cpython-312.pyc | Bin 0 -> 14920 bytes .../__pycache__/thingsdb.cpython-312.pyc | Bin 0 -> 5631 bytes .../lexers/__pycache__/tlb.cpython-312.pyc | Bin 0 -> 1884 bytes .../lexers/__pycache__/tls.cpython-312.pyc | Bin 0 -> 1942 bytes .../lexers/__pycache__/tnt.cpython-312.pyc | Bin 0 -> 13605 bytes .../__pycache__/trafficscript.cpython-312.pyc | Bin 0 -> 1861 bytes .../__pycache__/typoscript.cpython-312.pyc | Bin 0 -> 7377 bytes .../lexers/__pycache__/typst.cpython-312.pyc | Bin 0 -> 6922 bytes .../lexers/__pycache__/ul4.cpython-312.pyc | Bin 0 -> 8158 bytes .../lexers/__pycache__/unicon.cpython-312.pyc | Bin 0 -> 12525 bytes .../lexers/__pycache__/urbi.cpython-312.pyc | Bin 0 -> 5927 bytes .../lexers/__pycache__/usd.cpython-312.pyc | Bin 0 -> 4039 bytes .../__pycache__/varnish.cpython-312.pyc | Bin 0 -> 6962 bytes .../__pycache__/verification.cpython-312.pyc | Bin 0 -> 4046 bytes .../__pycache__/verifpal.cpython-312.pyc | Bin 0 -> 2988 bytes .../lexers/__pycache__/vip.cpython-312.pyc | Bin 0 -> 5711 bytes .../lexers/__pycache__/vyper.cpython-312.pyc | Bin 0 -> 4941 bytes .../lexers/__pycache__/web.cpython-312.pyc | Bin 0 -> 1331 bytes .../__pycache__/webassembly.cpython-312.pyc | Bin 0 -> 5837 bytes .../lexers/__pycache__/webidl.cpython-312.pyc | Bin 0 -> 8121 bytes .../__pycache__/webmisc.cpython-312.pyc | Bin 0 -> 43558 bytes .../lexers/__pycache__/wgsl.cpython-312.pyc | Bin 0 -> 10867 bytes .../lexers/__pycache__/whiley.cpython-312.pyc | Bin 0 -> 3652 bytes .../lexers/__pycache__/wowtoc.cpython-312.pyc | Bin 0 -> 3257 bytes .../lexers/__pycache__/wren.cpython-312.pyc | Bin 0 -> 3122 bytes .../lexers/__pycache__/x10.cpython-312.pyc | Bin 0 -> 2413 bytes .../lexers/__pycache__/xorg.cpython-312.pyc | Bin 0 -> 1402 bytes .../lexers/__pycache__/yang.cpython-312.pyc | Bin 0 -> 4166 bytes .../lexers/__pycache__/yara.cpython-312.pyc | Bin 0 -> 2748 bytes .../lexers/__pycache__/zig.cpython-312.pyc | Bin 0 -> 3911 bytes .../pygments/lexers/_ada_builtins.py | 103 + .../pygments/lexers/_asy_builtins.py | 1644 +++++ .../pygments/lexers/_cl_builtins.py | 231 + .../pygments/lexers/_cocoa_builtins.py | 75 + .../pygments/lexers/_csound_builtins.py | 1780 ++++++ .../pygments/lexers/_css_builtins.py | 558 ++ .../pygments/lexers/_googlesql_builtins.py | 918 +++ .../pygments/lexers/_julia_builtins.py | 411 ++ .../pygments/lexers/_lasso_builtins.py | 5326 +++++++++++++++++ .../pygments/lexers/_lilypond_builtins.py | 4932 +++++++++++++++ .../pygments/lexers/_lua_builtins.py | 285 + .../pygments/lexers/_luau_builtins.py | 62 + .../site-packages/pygments/lexers/_mapping.py | 602 ++ .../pygments/lexers/_mql_builtins.py | 1171 ++++ .../pygments/lexers/_mysql_builtins.py | 1335 +++++ .../pygments/lexers/_openedge_builtins.py | 2600 ++++++++ .../pygments/lexers/_php_builtins.py | 3325 ++++++++++ .../pygments/lexers/_postgres_builtins.py | 739 +++ .../pygments/lexers/_qlik_builtins.py | 666 +++ .../pygments/lexers/_scheme_builtins.py | 1609 +++++ .../pygments/lexers/_scilab_builtins.py | 3093 ++++++++++ .../pygments/lexers/_sourcemod_builtins.py | 1151 ++++ .../pygments/lexers/_sql_builtins.py | 106 + .../pygments/lexers/_stan_builtins.py | 648 ++ .../pygments/lexers/_stata_builtins.py | 457 ++ .../pygments/lexers/_tsql_builtins.py | 1003 ++++ .../pygments/lexers/_usd_builtins.py | 112 + .../pygments/lexers/_vbscript_builtins.py | 279 + .../pygments/lexers/_vim_builtins.py | 1938 ++++++ .../pygments/lexers/actionscript.py | 243 + .../site-packages/pygments/lexers/ada.py | 144 + .../site-packages/pygments/lexers/agile.py | 25 + .../site-packages/pygments/lexers/algebra.py | 299 + .../site-packages/pygments/lexers/ambient.py | 75 + .../site-packages/pygments/lexers/amdgpu.py | 54 + .../site-packages/pygments/lexers/ampl.py | 87 + .../site-packages/pygments/lexers/apdlexer.py | 593 ++ .../site-packages/pygments/lexers/apl.py | 103 + .../pygments/lexers/archetype.py | 315 + .../site-packages/pygments/lexers/arrow.py | 116 + .../site-packages/pygments/lexers/arturo.py | 249 + .../site-packages/pygments/lexers/asc.py | 55 + .../site-packages/pygments/lexers/asm.py | 1051 ++++ .../site-packages/pygments/lexers/asn1.py | 178 + .../pygments/lexers/automation.py | 379 ++ .../site-packages/pygments/lexers/bare.py | 101 + .../site-packages/pygments/lexers/basic.py | 656 ++ .../site-packages/pygments/lexers/bdd.py | 57 + .../site-packages/pygments/lexers/berry.py | 99 + .../site-packages/pygments/lexers/bibtex.py | 159 + .../pygments/lexers/blueprint.py | 173 + .../site-packages/pygments/lexers/boa.py | 97 + .../site-packages/pygments/lexers/bqn.py | 112 + .../site-packages/pygments/lexers/business.py | 625 ++ .../site-packages/pygments/lexers/c_cpp.py | 414 ++ .../site-packages/pygments/lexers/c_like.py | 738 +++ .../pygments/lexers/capnproto.py | 74 + .../site-packages/pygments/lexers/carbon.py | 95 + .../site-packages/pygments/lexers/cddl.py | 172 + .../site-packages/pygments/lexers/chapel.py | 139 + .../site-packages/pygments/lexers/clean.py | 180 + .../site-packages/pygments/lexers/codeql.py | 80 + .../site-packages/pygments/lexers/comal.py | 81 + .../site-packages/pygments/lexers/compiled.py | 35 + .../site-packages/pygments/lexers/configs.py | 1433 +++++ .../site-packages/pygments/lexers/console.py | 114 + .../site-packages/pygments/lexers/cplint.py | 43 + .../site-packages/pygments/lexers/crystal.py | 364 ++ .../site-packages/pygments/lexers/csound.py | 466 ++ .../site-packages/pygments/lexers/css.py | 602 ++ .../site-packages/pygments/lexers/d.py | 259 + .../site-packages/pygments/lexers/dalvik.py | 126 + .../site-packages/pygments/lexers/data.py | 763 +++ .../site-packages/pygments/lexers/dax.py | 135 + .../pygments/lexers/devicetree.py | 108 + .../site-packages/pygments/lexers/diff.py | 169 + .../site-packages/pygments/lexers/dns.py | 109 + .../site-packages/pygments/lexers/dotnet.py | 873 +++ .../site-packages/pygments/lexers/dsls.py | 970 +++ .../site-packages/pygments/lexers/dylan.py | 279 + .../site-packages/pygments/lexers/ecl.py | 144 + .../site-packages/pygments/lexers/eiffel.py | 68 + .../site-packages/pygments/lexers/elm.py | 123 + .../site-packages/pygments/lexers/elpi.py | 175 + .../site-packages/pygments/lexers/email.py | 132 + .../site-packages/pygments/lexers/erlang.py | 526 ++ .../site-packages/pygments/lexers/esoteric.py | 300 + .../site-packages/pygments/lexers/ezhil.py | 76 + .../site-packages/pygments/lexers/factor.py | 363 ++ .../site-packages/pygments/lexers/fantom.py | 251 + .../site-packages/pygments/lexers/felix.py | 275 + .../site-packages/pygments/lexers/fift.py | 68 + .../pygments/lexers/floscript.py | 81 + .../site-packages/pygments/lexers/forth.py | 178 + .../site-packages/pygments/lexers/fortran.py | 212 + .../site-packages/pygments/lexers/foxpro.py | 427 ++ .../site-packages/pygments/lexers/freefem.py | 893 +++ .../site-packages/pygments/lexers/func.py | 110 + .../pygments/lexers/functional.py | 21 + .../site-packages/pygments/lexers/futhark.py | 105 + .../pygments/lexers/gcodelexer.py | 35 + .../site-packages/pygments/lexers/gdscript.py | 189 + .../site-packages/pygments/lexers/gleam.py | 74 + .../site-packages/pygments/lexers/go.py | 97 + .../pygments/lexers/grammar_notation.py | 262 + .../site-packages/pygments/lexers/graph.py | 108 + .../site-packages/pygments/lexers/graphics.py | 794 +++ .../site-packages/pygments/lexers/graphql.py | 176 + .../site-packages/pygments/lexers/graphviz.py | 58 + .../site-packages/pygments/lexers/gsql.py | 103 + .../site-packages/pygments/lexers/hare.py | 73 + .../site-packages/pygments/lexers/haskell.py | 866 +++ .../site-packages/pygments/lexers/haxe.py | 935 +++ .../site-packages/pygments/lexers/hdl.py | 466 ++ .../site-packages/pygments/lexers/hexdump.py | 102 + .../site-packages/pygments/lexers/html.py | 670 +++ .../site-packages/pygments/lexers/idl.py | 284 + .../site-packages/pygments/lexers/igor.py | 435 ++ .../site-packages/pygments/lexers/inferno.py | 95 + .../pygments/lexers/installers.py | 352 ++ .../pygments/lexers/int_fiction.py | 1370 +++++ .../site-packages/pygments/lexers/iolang.py | 61 + .../site-packages/pygments/lexers/j.py | 151 + .../pygments/lexers/javascript.py | 1591 +++++ .../site-packages/pygments/lexers/jmespath.py | 69 + .../site-packages/pygments/lexers/jslt.py | 94 + .../site-packages/pygments/lexers/json5.py | 83 + .../site-packages/pygments/lexers/jsonnet.py | 169 + .../site-packages/pygments/lexers/jsx.py | 100 + .../site-packages/pygments/lexers/julia.py | 294 + .../site-packages/pygments/lexers/jvm.py | 1802 ++++++ .../site-packages/pygments/lexers/kuin.py | 332 + .../site-packages/pygments/lexers/kusto.py | 93 + .../site-packages/pygments/lexers/ldap.py | 155 + .../site-packages/pygments/lexers/lean.py | 241 + .../site-packages/pygments/lexers/lilypond.py | 225 + .../site-packages/pygments/lexers/lisp.py | 3146 ++++++++++ .../pygments/lexers/macaulay2.py | 1814 ++++++ .../site-packages/pygments/lexers/make.py | 212 + .../site-packages/pygments/lexers/maple.py | 291 + .../site-packages/pygments/lexers/markup.py | 1654 +++++ .../site-packages/pygments/lexers/math.py | 21 + .../site-packages/pygments/lexers/matlab.py | 3307 ++++++++++ .../site-packages/pygments/lexers/maxima.py | 84 + .../site-packages/pygments/lexers/meson.py | 139 + .../site-packages/pygments/lexers/mime.py | 210 + .../pygments/lexers/minecraft.py | 391 ++ .../site-packages/pygments/lexers/mips.py | 130 + .../site-packages/pygments/lexers/ml.py | 958 +++ .../site-packages/pygments/lexers/modeling.py | 366 ++ .../site-packages/pygments/lexers/modula2.py | 1579 +++++ .../site-packages/pygments/lexers/mojo.py | 707 +++ .../site-packages/pygments/lexers/monte.py | 203 + .../site-packages/pygments/lexers/mosel.py | 447 ++ .../site-packages/pygments/lexers/ncl.py | 894 +++ .../site-packages/pygments/lexers/nimrod.py | 199 + .../site-packages/pygments/lexers/nit.py | 63 + .../site-packages/pygments/lexers/nix.py | 144 + .../site-packages/pygments/lexers/numbair.py | 63 + .../site-packages/pygments/lexers/oberon.py | 120 + .../pygments/lexers/objective.py | 513 ++ .../site-packages/pygments/lexers/ooc.py | 84 + .../site-packages/pygments/lexers/openscad.py | 96 + .../site-packages/pygments/lexers/other.py | 41 + .../site-packages/pygments/lexers/parasail.py | 78 + .../site-packages/pygments/lexers/parsers.py | 798 +++ .../site-packages/pygments/lexers/pascal.py | 644 ++ .../site-packages/pygments/lexers/pawn.py | 202 + .../site-packages/pygments/lexers/pddl.py | 82 + .../site-packages/pygments/lexers/perl.py | 733 +++ .../site-packages/pygments/lexers/phix.py | 363 ++ .../site-packages/pygments/lexers/php.py | 334 ++ .../pygments/lexers/pointless.py | 70 + .../site-packages/pygments/lexers/pony.py | 93 + .../site-packages/pygments/lexers/praat.py | 303 + .../site-packages/pygments/lexers/procfile.py | 41 + .../site-packages/pygments/lexers/prolog.py | 318 + .../site-packages/pygments/lexers/promql.py | 176 + .../site-packages/pygments/lexers/prql.py | 251 + .../site-packages/pygments/lexers/ptx.py | 119 + .../site-packages/pygments/lexers/python.py | 1201 ++++ .../site-packages/pygments/lexers/q.py | 187 + .../site-packages/pygments/lexers/qlik.py | 117 + .../site-packages/pygments/lexers/qvt.py | 153 + .../site-packages/pygments/lexers/r.py | 196 + .../site-packages/pygments/lexers/rdf.py | 468 ++ .../site-packages/pygments/lexers/rebol.py | 419 ++ .../site-packages/pygments/lexers/rego.py | 57 + .../site-packages/pygments/lexers/resource.py | 83 + .../site-packages/pygments/lexers/ride.py | 138 + .../site-packages/pygments/lexers/rita.py | 42 + .../site-packages/pygments/lexers/rnc.py | 66 + .../site-packages/pygments/lexers/roboconf.py | 81 + .../pygments/lexers/robotframework.py | 551 ++ .../site-packages/pygments/lexers/ruby.py | 518 ++ .../site-packages/pygments/lexers/rust.py | 222 + .../site-packages/pygments/lexers/sas.py | 227 + .../site-packages/pygments/lexers/savi.py | 171 + .../site-packages/pygments/lexers/scdoc.py | 85 + .../pygments/lexers/scripting.py | 1616 +++++ .../site-packages/pygments/lexers/sgf.py | 59 + .../site-packages/pygments/lexers/shell.py | 902 +++ .../site-packages/pygments/lexers/sieve.py | 78 + .../site-packages/pygments/lexers/slash.py | 183 + .../pygments/lexers/smalltalk.py | 194 + .../site-packages/pygments/lexers/smithy.py | 77 + .../site-packages/pygments/lexers/smv.py | 78 + .../site-packages/pygments/lexers/snobol.py | 82 + .../site-packages/pygments/lexers/solidity.py | 87 + .../site-packages/pygments/lexers/soong.py | 78 + .../site-packages/pygments/lexers/sophia.py | 102 + .../site-packages/pygments/lexers/special.py | 122 + .../site-packages/pygments/lexers/spice.py | 70 + .../site-packages/pygments/lexers/sql.py | 1109 ++++ .../site-packages/pygments/lexers/srcinfo.py | 62 + .../site-packages/pygments/lexers/stata.py | 170 + .../pygments/lexers/supercollider.py | 94 + .../site-packages/pygments/lexers/tablegen.py | 177 + .../site-packages/pygments/lexers/tact.py | 303 + .../site-packages/pygments/lexers/tal.py | 77 + .../site-packages/pygments/lexers/tcl.py | 148 + .../site-packages/pygments/lexers/teal.py | 88 + .../pygments/lexers/templates.py | 2355 ++++++++ .../site-packages/pygments/lexers/teraterm.py | 325 + .../site-packages/pygments/lexers/testing.py | 209 + .../site-packages/pygments/lexers/text.py | 27 + .../site-packages/pygments/lexers/textedit.py | 205 + .../site-packages/pygments/lexers/textfmts.py | 436 ++ .../site-packages/pygments/lexers/theorem.py | 410 ++ .../site-packages/pygments/lexers/thingsdb.py | 140 + .../site-packages/pygments/lexers/tlb.py | 59 + .../site-packages/pygments/lexers/tls.py | 54 + .../site-packages/pygments/lexers/tnt.py | 270 + .../pygments/lexers/trafficscript.py | 51 + .../pygments/lexers/typoscript.py | 216 + .../site-packages/pygments/lexers/typst.py | 160 + .../site-packages/pygments/lexers/ul4.py | 309 + .../site-packages/pygments/lexers/unicon.py | 413 ++ .../site-packages/pygments/lexers/urbi.py | 145 + .../site-packages/pygments/lexers/usd.py | 85 + .../site-packages/pygments/lexers/varnish.py | 189 + .../pygments/lexers/verification.py | 113 + .../site-packages/pygments/lexers/verifpal.py | 65 + .../site-packages/pygments/lexers/vip.py | 150 + .../site-packages/pygments/lexers/vyper.py | 140 + .../site-packages/pygments/lexers/web.py | 24 + .../pygments/lexers/webassembly.py | 119 + .../site-packages/pygments/lexers/webidl.py | 298 + .../site-packages/pygments/lexers/webmisc.py | 1006 ++++ .../site-packages/pygments/lexers/wgsl.py | 406 ++ .../site-packages/pygments/lexers/whiley.py | 115 + .../site-packages/pygments/lexers/wowtoc.py | 120 + .../site-packages/pygments/lexers/wren.py | 98 + .../site-packages/pygments/lexers/x10.py | 66 + .../site-packages/pygments/lexers/xorg.py | 38 + .../site-packages/pygments/lexers/yang.py | 103 + .../site-packages/pygments/lexers/yara.py | 69 + .../site-packages/pygments/lexers/zig.py | 125 + .../site-packages/pygments/modeline.py | 43 + .../site-packages/pygments/plugin.py | 72 + .../site-packages/pygments/regexopt.py | 91 + .../site-packages/pygments/scanner.py | 104 + .../site-packages/pygments/sphinxext.py | 247 + .../site-packages/pygments/style.py | 203 + .../site-packages/pygments/styles/__init__.py | 61 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 2635 bytes .../__pycache__/_mapping.cpython-312.pyc | Bin 0 -> 3648 bytes .../styles/__pycache__/abap.cpython-312.pyc | Bin 0 -> 1083 bytes .../styles/__pycache__/algol.cpython-312.pyc | Bin 0 -> 2603 bytes .../__pycache__/algol_nu.cpython-312.pyc | Bin 0 -> 2618 bytes .../__pycache__/arduino.cpython-312.pyc | Bin 0 -> 4277 bytes .../styles/__pycache__/autumn.cpython-312.pyc | Bin 0 -> 2859 bytes .../__pycache__/borland.cpython-312.pyc | Bin 0 -> 2228 bytes .../styles/__pycache__/bw.cpython-312.pyc | Bin 0 -> 1905 bytes .../styles/__pycache__/coffee.cpython-312.pyc | Bin 0 -> 3449 bytes .../__pycache__/colorful.cpython-312.pyc | Bin 0 -> 3717 bytes .../__pycache__/default.cpython-312.pyc | Bin 0 -> 3219 bytes .../__pycache__/dracula.cpython-312.pyc | Bin 0 -> 3032 bytes .../styles/__pycache__/emacs.cpython-312.pyc | Bin 0 -> 3259 bytes .../__pycache__/friendly.cpython-312.pyc | Bin 0 -> 3355 bytes .../friendly_grayscale.cpython-312.pyc | Bin 0 -> 3565 bytes .../styles/__pycache__/fruity.cpython-312.pyc | Bin 0 -> 1934 bytes .../__pycache__/gh_dark.cpython-312.pyc | Bin 0 -> 4023 bytes .../__pycache__/gruvbox.cpython-312.pyc | Bin 0 -> 4371 bytes .../styles/__pycache__/igor.cpython-312.pyc | Bin 0 -> 1128 bytes .../styles/__pycache__/inkpot.cpython-312.pyc | Bin 0 -> 2990 bytes .../__pycache__/lightbulb.cpython-312.pyc | Bin 0 -> 4361 bytes .../__pycache__/lilypond.cpython-312.pyc | Bin 0 -> 3502 bytes .../__pycache__/lovelace.cpython-312.pyc | Bin 0 -> 4255 bytes .../styles/__pycache__/manni.cpython-312.pyc | Bin 0 -> 3490 bytes .../__pycache__/material.cpython-312.pyc | Bin 0 -> 4803 bytes .../__pycache__/monokai.cpython-312.pyc | Bin 0 -> 4784 bytes .../styles/__pycache__/murphy.cpython-312.pyc | Bin 0 -> 3675 bytes .../styles/__pycache__/native.cpython-312.pyc | Bin 0 -> 2952 bytes .../styles/__pycache__/nord.cpython-312.pyc | Bin 0 -> 5581 bytes .../__pycache__/onedark.cpython-312.pyc | Bin 0 -> 2258 bytes .../__pycache__/paraiso_dark.cpython-312.pyc | Bin 0 -> 5124 bytes .../__pycache__/paraiso_light.cpython-312.pyc | Bin 0 -> 5130 bytes .../styles/__pycache__/pastie.cpython-312.pyc | Bin 0 -> 3464 bytes .../__pycache__/perldoc.cpython-312.pyc | Bin 0 -> 3050 bytes .../__pycache__/rainbow_dash.cpython-312.pyc | Bin 0 -> 3696 bytes .../styles/__pycache__/rrt.cpython-312.pyc | Bin 0 -> 1458 bytes .../styles/__pycache__/sas.cpython-312.pyc | Bin 0 -> 1803 bytes .../__pycache__/solarized.cpython-312.pyc | Bin 0 -> 5755 bytes .../__pycache__/staroffice.cpython-312.pyc | Bin 0 -> 1106 bytes .../__pycache__/stata_dark.cpython-312.pyc | Bin 0 -> 1673 bytes .../__pycache__/stata_light.cpython-312.pyc | Bin 0 -> 1674 bytes .../styles/__pycache__/tango.cpython-312.pyc | Bin 0 -> 6127 bytes .../styles/__pycache__/trac.cpython-312.pyc | Bin 0 -> 2599 bytes .../styles/__pycache__/vim.cpython-312.pyc | Bin 0 -> 2482 bytes .../styles/__pycache__/vs.cpython-312.pyc | Bin 0 -> 1487 bytes .../styles/__pycache__/xcode.cpython-312.pyc | Bin 0 -> 1825 bytes .../__pycache__/zenburn.cpython-312.pyc | Bin 0 -> 3334 bytes .../site-packages/pygments/styles/_mapping.py | 54 + .../site-packages/pygments/styles/abap.py | 32 + .../site-packages/pygments/styles/algol.py | 65 + .../site-packages/pygments/styles/algol_nu.py | 65 + .../site-packages/pygments/styles/arduino.py | 100 + .../site-packages/pygments/styles/autumn.py | 67 + .../site-packages/pygments/styles/borland.py | 53 + .../site-packages/pygments/styles/bw.py | 52 + .../site-packages/pygments/styles/coffee.py | 80 + .../site-packages/pygments/styles/colorful.py | 83 + .../site-packages/pygments/styles/default.py | 76 + .../site-packages/pygments/styles/dracula.py | 90 + .../site-packages/pygments/styles/emacs.py | 75 + .../site-packages/pygments/styles/friendly.py | 76 + .../pygments/styles/friendly_grayscale.py | 80 + .../site-packages/pygments/styles/fruity.py | 47 + .../site-packages/pygments/styles/gh_dark.py | 113 + .../site-packages/pygments/styles/gruvbox.py | 118 + .../site-packages/pygments/styles/igor.py | 32 + .../site-packages/pygments/styles/inkpot.py | 72 + .../pygments/styles/lightbulb.py | 110 + .../site-packages/pygments/styles/lilypond.py | 62 + .../site-packages/pygments/styles/lovelace.py | 100 + .../site-packages/pygments/styles/manni.py | 79 + .../site-packages/pygments/styles/material.py | 124 + .../site-packages/pygments/styles/monokai.py | 112 + .../site-packages/pygments/styles/murphy.py | 82 + .../site-packages/pygments/styles/native.py | 70 + .../site-packages/pygments/styles/nord.py | 156 + .../site-packages/pygments/styles/onedark.py | 63 + .../pygments/styles/paraiso_dark.py | 124 + .../pygments/styles/paraiso_light.py | 124 + .../site-packages/pygments/styles/pastie.py | 78 + .../site-packages/pygments/styles/perldoc.py | 73 + .../pygments/styles/rainbow_dash.py | 95 + .../site-packages/pygments/styles/rrt.py | 40 + .../site-packages/pygments/styles/sas.py | 46 + .../pygments/styles/solarized.py | 144 + .../pygments/styles/staroffice.py | 31 + .../pygments/styles/stata_dark.py | 42 + .../pygments/styles/stata_light.py | 42 + .../site-packages/pygments/styles/tango.py | 143 + .../site-packages/pygments/styles/trac.py | 66 + .../site-packages/pygments/styles/vim.py | 67 + .../site-packages/pygments/styles/vs.py | 41 + .../site-packages/pygments/styles/xcode.py | 53 + .../site-packages/pygments/styles/zenburn.py | 83 + .../site-packages/pygments/token.py | 214 + .../site-packages/pygments/unistring.py | 153 + .../python3.12/site-packages/pygments/util.py | 324 + .../pytest-9.0.1.dist-info/INSTALLER | 1 + .../pytest-9.0.1.dist-info/METADATA | 212 + .../pytest-9.0.1.dist-info/RECORD | 158 + .../pytest-9.0.1.dist-info/REQUESTED | 0 .../pytest-9.0.1.dist-info/WHEEL | 5 + .../pytest-9.0.1.dist-info/entry_points.txt | 3 + .../pytest-9.0.1.dist-info/licenses/LICENSE | 21 + .../pytest-9.0.1.dist-info/top_level.txt | 3 + .../site-packages/pytest/__init__.py | 186 + .../site-packages/pytest/__main__.py | 9 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 4502 bytes .../__pycache__/__main__.cpython-312.pyc | Bin 0 -> 438 bytes .../python3.12/site-packages/pytest/py.typed | 0 .../pytest_asyncio-1.3.0.dist-info/INSTALLER | 1 + .../pytest_asyncio-1.3.0.dist-info/METADATA | 91 + .../pytest_asyncio-1.3.0.dist-info/RECORD | 13 + .../pytest_asyncio-1.3.0.dist-info/REQUESTED | 0 .../pytest_asyncio-1.3.0.dist-info/WHEEL | 5 + .../entry_points.txt | 2 + .../licenses/LICENSE | 201 + .../top_level.txt | 1 + .../site-packages/pytest_asyncio/__init__.py | 11 + .../__init__.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 614 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 500 bytes .../plugin.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 43205 bytes .../__pycache__/plugin.cpython-312.pyc | Bin 0 -> 39403 bytes .../site-packages/pytest_asyncio/plugin.py | 896 +++ .../site-packages/pytest_asyncio/py.typed | 0 .../pytest_cov-7.0.0.dist-info/INSTALLER | 1 + .../pytest_cov-7.0.0.dist-info/METADATA | 673 +++ .../pytest_cov-7.0.0.dist-info/RECORD | 14 + .../pytest_cov-7.0.0.dist-info/REQUESTED | 0 .../pytest_cov-7.0.0.dist-info/WHEEL | 4 + .../entry_points.txt | 2 + .../licenses/AUTHORS.rst | 67 + .../licenses/LICENSE | 21 + .../site-packages/pytest_cov/__init__.py | 41 + .../__init__.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 1834 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1834 bytes .../__pycache__/engine.cpython-312.pyc | Bin 0 -> 23173 bytes .../plugin.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 22974 bytes .../__pycache__/plugin.cpython-312.pyc | Bin 0 -> 22046 bytes .../site-packages/pytest_cov/engine.py | 460 ++ .../site-packages/pytest_cov/plugin.py | 468 ++ .../pytest_mock-3.15.1.dist-info/INSTALLER | 1 + .../pytest_mock-3.15.1.dist-info/METADATA | 105 + .../pytest_mock-3.15.1.dist-info/RECORD | 17 + .../pytest_mock-3.15.1.dist-info/REQUESTED | 0 .../pytest_mock-3.15.1.dist-info/WHEEL | 5 + .../entry_points.txt | 2 + .../licenses/LICENSE | 21 + .../top_level.txt | 1 + .../site-packages/pytest_mock/__init__.py | 28 + .../__init__.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 844 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 730 bytes .../_util.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 1455 bytes .../__pycache__/_util.cpython-312.pyc | Bin 0 -> 1341 bytes .../__pycache__/_version.cpython-312.pyc | Bin 0 -> 803 bytes .../plugin.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 32761 bytes .../__pycache__/plugin.cpython-312.pyc | Bin 0 -> 30367 bytes .../site-packages/pytest_mock/_util.py | 36 + .../site-packages/pytest_mock/_version.py | 34 + .../site-packages/pytest_mock/plugin.py | 725 +++ .../site-packages/pytest_mock/py.typed | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 466 -> 466 bytes .../sniffio/__pycache__/_impl.cpython-312.pyc | Bin 3177 -> 3177 bytes .../__pycache__/_version.cpython-312.pyc | Bin 218 -> 218 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 197 -> 197 bytes .../__pycache__/test_sniffio.cpython-312.pyc | Bin 3926 -> 3926 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 1069 -> 1069 bytes .../__pycache__/aiosqlite.cpython-312.pyc | Bin 17661 -> 17661 bytes .../sqlite/__pycache__/base.cpython-312.pyc | Bin 100318 -> 100318 bytes .../sqlite/__pycache__/dml.cpython-312.pyc | Bin 9245 -> 9245 bytes .../sqlite/__pycache__/json.cpython-312.pyc | Bin 3781 -> 3781 bytes .../__pycache__/pysqlcipher.cpython-312.pyc | Bin 6213 -> 6213 bytes .../__pycache__/pysqlite.cpython-312.pyc | Bin 31273 -> 31273 bytes .../__pycache__/testclient.cpython-312.pyc | Bin 40653 -> 40653 bytes .../__pycache__/__main__.cpython-312.pyc | Bin 308 -> 308 bytes ENTERPRISE_FEATURES_ROADMAP.md | 698 --- Frontend/package-lock.json | 2931 ++++++++- Frontend/src/App.tsx | 108 +- .../analytics/CustomReportBuilder.tsx | 377 ++ .../src/components/analytics/SimpleChart.tsx | 224 + .../src/components/auth/AccountantRoute.tsx | 6 + Frontend/src/components/auth/AdminRoute.tsx | 6 + .../{ => auth}/ResetPasswordRouteHandler.tsx | 2 +- Frontend/src/components/auth/StaffRoute.tsx | 6 + Frontend/src/components/auth/index.ts | 1 + .../components/chat/StaffChatNotification.tsx | 10 + .../common/PaymentMethodSelector.tsx | 59 +- Frontend/src/components/common/Preloader.tsx | 87 + Frontend/src/components/layout/Header.tsx | 222 +- .../components/layout/SidebarAccountant.tsx | 20 +- .../src/components/layout/SidebarAdmin.tsx | 536 +- .../src/components/layout/SidebarStaff.tsx | 28 +- .../notifications/InAppNotificationBell.tsx | 139 + .../notifications/NotificationPreferences.tsx | 302 + .../NotificationTemplatesModal.tsx | 210 + .../notifications/SendNotificationModal.tsx | 241 + .../payments/BoricaPaymentModal.tsx | 227 + .../{admin => shared}/CreateBookingModal.tsx | 0 .../shared/CreateGroupBookingModal.tsx | 632 ++ .../shared/HousekeepingManagement.tsx | 729 +++ .../shared/InspectionManagement.tsx | 768 +++ .../shared/MaintenanceManagement.tsx | 704 +++ Frontend/src/components/shared/index.ts | 12 + .../src/components/tasks/CreateTaskModal.tsx | 187 + .../src/components/tasks/TaskDetailModal.tsx | 255 + Frontend/src/components/tasks/TaskFilters.tsx | 95 + .../components/workflows/WorkflowBuilder.tsx | 308 + .../workflows/WorkflowDetailModal.tsx | 97 + .../src/contexts/NavigationLoadingContext.tsx | 56 + Frontend/src/contexts/ResponsiveContext.tsx | 52 + Frontend/src/hooks/index.ts | 2 + Frontend/src/hooks/useResponsive.ts | 129 + Frontend/src/pages/AccountantLayout.tsx | 5 +- Frontend/src/pages/AdminLayout.tsx | 141 +- Frontend/src/pages/StaffLayout.tsx | 5 +- .../accountant/AnalyticsDashboardPage.tsx | 1803 ++++++ .../accountant/InvoiceManagementPage.tsx | 353 ++ .../accountant/PaymentManagementPage.tsx | 317 + Frontend/src/pages/accountant/index.ts | 11 + .../src/pages/admin/AdvancedAnalyticsPage.tsx | 679 +++ .../admin/AdvancedRoomManagementPage.tsx | 1475 +++++ .../pages/admin/AnalyticsDashboardPage.tsx | 874 ++- .../src/pages/admin/BookingManagementPage.tsx | 32 +- .../src/pages/admin/BusinessDashboardPage.tsx | 106 +- Frontend/src/pages/admin/DashboardPage.tsx | 224 +- .../admin/EmailCampaignManagementPage.tsx | 947 +++ .../admin/GroupBookingManagementPage.tsx | 538 ++ .../src/pages/admin/InvoiceManagementPage.tsx | 15 +- .../admin/NotificationManagementPage.tsx | 271 + .../src/pages/admin/PackageManagementPage.tsx | 708 +++ .../src/pages/admin/PageContentDashboard.tsx | 88 +- .../src/pages/admin/PaymentManagementPage.tsx | 65 +- .../pages/admin/RatePlanManagementPage.tsx | 774 +++ .../pages/admin/ReceptionDashboardPage.tsx | 1296 +--- .../src/pages/admin/RoomManagementPage.tsx | 24 +- .../pages/admin/SecurityManagementPage.tsx | 1563 +++++ .../src/pages/admin/ServiceManagementPage.tsx | 18 +- Frontend/src/pages/admin/SettingsPage.tsx | 1516 +++-- .../src/pages/admin/TaskManagementPage.tsx | 398 ++ .../src/pages/admin/UserManagementPage.tsx | 18 +- .../pages/admin/WorkflowManagementPage.tsx | 215 + Frontend/src/pages/admin/index.ts | 36 +- .../src/pages/customer/BoricaReturnPage.tsx | 163 + .../src/pages/customer/GroupBookingPage.tsx | 179 + Frontend/src/pages/customer/index.ts | 24 + .../staff/AdvancedRoomManagementPage.tsx | 394 ++ .../pages/staff/AnalyticsDashboardPage.tsx | 1803 ++++++ .../src/pages/staff/BookingManagementPage.tsx | 707 +++ Frontend/src/pages/staff/GuestProfilePage.tsx | 1319 ++++ .../src/pages/staff/LoyaltyManagementPage.tsx | 1161 ++++ .../src/pages/staff/PaymentManagementPage.tsx | 317 + .../pages/staff/ReceptionDashboardPage.tsx | 3262 ++++++++++ Frontend/src/pages/staff/index.ts | 16 + Frontend/src/routes/accountantRoutes.tsx | 29 + Frontend/src/routes/adminRoutes.tsx | 65 + Frontend/src/routes/customerRoutes.tsx | 42 + Frontend/src/routes/index.ts | 13 + Frontend/src/routes/publicRoutes.tsx | 50 + Frontend/src/routes/staffRoutes.tsx | 39 + .../src/services/api/advancedRoomService.ts | 268 + Frontend/src/services/api/analyticsService.ts | 327 + Frontend/src/services/api/bookingService.ts | 80 +- .../src/services/api/emailCampaignService.ts | 192 + .../src/services/api/groupBookingService.ts | 286 + Frontend/src/services/api/index.ts | 12 + Frontend/src/services/api/invoiceService.ts | 69 +- .../src/services/api/notificationService.ts | 125 + Frontend/src/services/api/packageService.ts | 190 + Frontend/src/services/api/paymentService.ts | 77 +- Frontend/src/services/api/ratePlanService.ts | 198 + Frontend/src/services/api/reportService.ts | 22 +- Frontend/src/services/api/securityService.ts | 249 + .../src/services/api/systemSettingsService.ts | 72 + Frontend/src/services/api/taskService.ts | 141 + Frontend/src/services/api/userService.ts | 49 +- Frontend/src/services/api/workflowService.ts | 123 + Frontend/src/services/responsiveService.ts | 94 + Frontend/src/store/useFavoritesStore.ts | 37 + Frontend/src/styles/index.css | 10 + logs/app.log | 408 ++ 1840 files changed, 236564 insertions(+), 3475 deletions(-) create mode 100644 Backend/alembic/versions/__pycache__/add_group_booking_tables.cpython-312.pyc create mode 100644 Backend/alembic/versions/__pycache__/add_rate_plan_id_to_bookings.cpython-312.pyc create mode 100644 Backend/alembic/versions/add_borica_payment_method.py create mode 100644 Backend/alembic/versions/add_group_booking_tables.py create mode 100644 Backend/alembic/versions/add_rate_plan_id_to_bookings.py create mode 100644 Backend/pytest.ini create mode 100644 Backend/src/middleware/ip_whitelist.py create mode 100644 Backend/src/models/__pycache__/email_campaign.cpython-312.pyc create mode 100644 Backend/src/models/__pycache__/gdpr_compliance.cpython-312.pyc create mode 100644 Backend/src/models/__pycache__/group_booking.cpython-312.pyc create mode 100644 Backend/src/models/__pycache__/housekeeping_task.cpython-312.pyc create mode 100644 Backend/src/models/__pycache__/notification.cpython-312.pyc create mode 100644 Backend/src/models/__pycache__/package.cpython-312.pyc create mode 100644 Backend/src/models/__pycache__/rate_plan.cpython-312.pyc create mode 100644 Backend/src/models/__pycache__/room_attribute.cpython-312.pyc create mode 100644 Backend/src/models/__pycache__/room_inspection.cpython-312.pyc create mode 100644 Backend/src/models/__pycache__/room_maintenance.cpython-312.pyc create mode 100644 Backend/src/models/__pycache__/security_event.cpython-312.pyc create mode 100644 Backend/src/models/__pycache__/workflow.cpython-312.pyc create mode 100644 Backend/src/models/email_campaign.py create mode 100644 Backend/src/models/gdpr_compliance.py create mode 100644 Backend/src/models/group_booking.py create mode 100644 Backend/src/models/housekeeping_task.py create mode 100644 Backend/src/models/notification.py create mode 100644 Backend/src/models/package.py create mode 100644 Backend/src/models/rate_plan.py create mode 100644 Backend/src/models/room_attribute.py create mode 100644 Backend/src/models/room_inspection.py create mode 100644 Backend/src/models/room_maintenance.py create mode 100644 Backend/src/models/security_event.py create mode 100644 Backend/src/models/workflow.py create mode 100644 Backend/src/routes/__pycache__/advanced_room_routes.cpython-312.pyc create mode 100644 Backend/src/routes/__pycache__/analytics_routes.cpython-312.pyc create mode 100644 Backend/src/routes/__pycache__/email_campaign_routes.cpython-312.pyc create mode 100644 Backend/src/routes/__pycache__/group_booking_routes.cpython-312.pyc create mode 100644 Backend/src/routes/__pycache__/notification_routes.cpython-312.pyc create mode 100644 Backend/src/routes/__pycache__/package_routes.cpython-312.pyc create mode 100644 Backend/src/routes/__pycache__/rate_plan_routes.cpython-312.pyc create mode 100644 Backend/src/routes/__pycache__/security_routes.cpython-312.pyc create mode 100644 Backend/src/routes/__pycache__/task_routes.cpython-312.pyc create mode 100644 Backend/src/routes/__pycache__/workflow_routes.cpython-312.pyc create mode 100644 Backend/src/routes/advanced_room_routes.py create mode 100644 Backend/src/routes/analytics_routes.py create mode 100644 Backend/src/routes/email_campaign_routes.py create mode 100644 Backend/src/routes/group_booking_routes.py create mode 100644 Backend/src/routes/notification_routes.py create mode 100644 Backend/src/routes/package_routes.py create mode 100644 Backend/src/routes/rate_plan_routes.py create mode 100644 Backend/src/routes/security_routes.py create mode 100644 Backend/src/routes/task_routes.py create mode 100644 Backend/src/routes/workflow_routes.py create mode 100644 Backend/src/services/__pycache__/analytics_service.cpython-312.pyc create mode 100644 Backend/src/services/__pycache__/borica_service.cpython-312.pyc create mode 100644 Backend/src/services/__pycache__/email_campaign_service.cpython-312.pyc create mode 100644 Backend/src/services/__pycache__/encryption_service.cpython-312.pyc create mode 100644 Backend/src/services/__pycache__/gdpr_service.cpython-312.pyc create mode 100644 Backend/src/services/__pycache__/group_booking_service.cpython-312.pyc create mode 100644 Backend/src/services/__pycache__/notification_service.cpython-312.pyc create mode 100644 Backend/src/services/__pycache__/oauth_service.cpython-312.pyc create mode 100644 Backend/src/services/__pycache__/room_assignment_service.cpython-312.pyc create mode 100644 Backend/src/services/__pycache__/security_monitoring_service.cpython-312.pyc create mode 100644 Backend/src/services/__pycache__/security_scan_service.cpython-312.pyc create mode 100644 Backend/src/services/__pycache__/task_service.cpython-312.pyc create mode 100644 Backend/src/services/__pycache__/workflow_service.cpython-312.pyc create mode 100644 Backend/src/services/analytics_service.py create mode 100644 Backend/src/services/borica_service.py create mode 100644 Backend/src/services/email_campaign_service.py create mode 100644 Backend/src/services/encryption_service.py create mode 100644 Backend/src/services/gdpr_service.py create mode 100644 Backend/src/services/group_booking_service.py create mode 100644 Backend/src/services/notification_service.py create mode 100644 Backend/src/services/oauth_service.py create mode 100644 Backend/src/services/room_assignment_service.py create mode 100644 Backend/src/services/security_monitoring_service.py create mode 100644 Backend/src/services/security_scan_service.py create mode 100644 Backend/src/services/task_service.py create mode 100644 Backend/src/services/workflow_service.py create mode 100644 Backend/src/tasks/security_scan_task.py create mode 100644 Backend/src/utils/__pycache__/currency_helpers.cpython-312.pyc create mode 100644 Backend/src/utils/__pycache__/response_helpers.cpython-312.pyc create mode 100644 Backend/src/utils/__pycache__/role_helpers.cpython-312.pyc create mode 100644 Backend/src/utils/currency_helpers.py create mode 100644 Backend/src/utils/response_helpers.py create mode 100644 Backend/src/utils/role_helpers.py create mode 100755 Backend/venv/bin/coverage create mode 100755 Backend/venv/bin/coverage-3.12 create mode 100755 Backend/venv/bin/coverage3 create mode 100755 Backend/venv/bin/httpx create mode 100755 Backend/venv/bin/py.test create mode 100755 Backend/venv/bin/pygmentize create mode 100755 Backend/venv/bin/pytest create mode 100644 Backend/venv/lib/python3.12/site-packages/__pycache__/py.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/_argcomplete.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/_version.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/cacheprovider.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/capture.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/compat.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/debugging.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/deprecated.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/doctest.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/faulthandler.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/fixtures.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/freeze_support.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/helpconfig.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/hookspec.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/junitxml.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/legacypath.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/logging.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/main.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/monkeypatch.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/nodes.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/outcomes.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/pastebin.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/pathlib.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/pytester.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/pytester_assertions.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/python.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/python_api.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/raises.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/recwarn.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/reports.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/runner.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/scope.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/setuponly.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/setupplan.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/skipping.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/stash.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/stepwise.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/subtests.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/terminal.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/threadexception.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/timing.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/tmpdir.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/tracemalloc.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/unittest.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/unraisableexception.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/warning_types.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/warnings.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_argcomplete.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_code/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/code.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/source.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_code/code.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_code/source.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_io/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/pprint.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/saferepr.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/terminalwriter.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/wcwidth.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_io/pprint.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_io/saferepr.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_io/terminalwriter.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_io/wcwidth.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_py/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_py/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_py/__pycache__/error.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_py/__pycache__/path.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_py/error.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_py/path.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/_version.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/assertion/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/rewrite.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/truncate.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/util.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/assertion/rewrite.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/assertion/truncate.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/assertion/util.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/cacheprovider.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/capture.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/compat.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/config/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/argparsing.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/compat.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/exceptions.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/findpaths.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/config/argparsing.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/config/compat.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/config/exceptions.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/config/findpaths.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/debugging.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/deprecated.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/doctest.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/faulthandler.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/fixtures.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/freeze_support.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/helpconfig.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/hookspec.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/junitxml.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/legacypath.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/logging.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/main.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/mark/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/expression.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/structures.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/mark/expression.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/mark/structures.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/monkeypatch.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/nodes.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/outcomes.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/pastebin.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/pathlib.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/py.typed create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/pytester.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/pytester_assertions.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/python.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/python_api.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/raises.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/recwarn.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/reports.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/runner.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/scope.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/setuponly.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/setupplan.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/skipping.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/stash.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/stepwise.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/subtests.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/terminal.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/threadexception.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/timing.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/tmpdir.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/tracemalloc.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/unittest.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/unraisableexception.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/warning_types.py create mode 100644 Backend/venv/lib/python3.12/site-packages/_pytest/warnings.py create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio-3.7.1.dist-info/REQUESTED create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/__init__.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/from_thread.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/lowlevel.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/pytest_plugin.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/to_thread.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/__init__.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/__init__.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_compat.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_eventloop.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_exceptions.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_fileio.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_resources.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_signals.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_sockets.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_streams.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_subprocesses.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_synchronization.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tasks.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_testing.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_typedattr.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/__init__.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_resources.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_sockets.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_streams.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_subprocesses.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_tasks.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_testing.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/__init__.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/memory.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/stapled.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/tls.cpython-312-pytest-9.0.1.pyc rename Backend/venv/lib/python3.12/site-packages/{h11-0.16.0.dist-info => coverage-7.12.0.dist-info}/INSTALLER (100%) create mode 100644 Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/METADATA create mode 100644 Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/RECORD create mode 100644 Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/WHEEL create mode 100644 Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/entry_points.txt create mode 100644 Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/licenses/LICENSE.txt create mode 100644 Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/top_level.txt create mode 100644 Backend/venv/lib/python3.12/site-packages/h11-0.14.0.dist-info/INSTALLER rename Backend/venv/lib/python3.12/site-packages/{h11-0.16.0.dist-info/licenses => h11-0.14.0.dist-info}/LICENSE.txt (100%) rename Backend/venv/lib/python3.12/site-packages/{h11-0.16.0.dist-info => h11-0.14.0.dist-info}/METADATA (95%) create mode 100644 Backend/venv/lib/python3.12/site-packages/h11-0.14.0.dist-info/RECORD create mode 100644 Backend/venv/lib/python3.12/site-packages/h11-0.14.0.dist-info/WHEEL rename Backend/venv/lib/python3.12/site-packages/{h11-0.16.0.dist-info => h11-0.14.0.dist-info}/top_level.txt (100%) delete mode 100644 Backend/venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/RECORD create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/helpers.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_against_stdlib_http.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_connection.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_events.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_headers.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_helpers.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_io.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_receivebuffer.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_state.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_util.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/data/test-file create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/helpers.py create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/test_against_stdlib_http.py create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/test_connection.py create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/test_events.py create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/test_headers.py create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/test_helpers.py create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/test_io.py create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/test_receivebuffer.py create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/test_state.py create mode 100644 Backend/venv/lib/python3.12/site-packages/h11/tests/test_util.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/INSTALLER create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/LICENSE.md create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/METADATA create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/RECORD create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/WHEEL create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/top_level.txt create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_api.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_exceptions.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_models.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_ssl.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_synchronization.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_trace.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_utils.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_api.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_async/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/connection.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/connection_pool.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/http11.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/http2.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/http_proxy.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/interfaces.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/socks_proxy.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_async/connection.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_async/connection_pool.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_async/http11.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_async/http2.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_async/http_proxy.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_async/interfaces.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_async/socks_proxy.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/anyio.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/auto.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/base.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/mock.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/sync.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/trio.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_backends/anyio.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_backends/auto.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_backends/base.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_backends/mock.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_backends/sync.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_backends/trio.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_exceptions.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_models.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_ssl.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/connection.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/connection_pool.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/http11.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/http2.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/http_proxy.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/interfaces.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/socks_proxy.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_sync/connection.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_sync/connection_pool.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_sync/http11.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_sync/http2.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_sync/http_proxy.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_sync/interfaces.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_sync/socks_proxy.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_synchronization.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_trace.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/_utils.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpcore/py.typed create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/INSTALLER create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/METADATA create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/RECORD create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/REQUESTED create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/WHEEL create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/entry_points.txt create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/licenses/LICENSE.md create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/__version__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_api.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_auth.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_client.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_compat.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_config.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_content.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_decoders.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_exceptions.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_main.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_models.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_multipart.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_status_codes.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_types.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_urlparse.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_urls.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_utils.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/__version__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_api.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_auth.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_client.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_compat.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_config.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_content.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_decoders.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_exceptions.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_main.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_models.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_multipart.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_status_codes.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_transports/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_transports/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_transports/__pycache__/asgi.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_transports/__pycache__/base.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_transports/__pycache__/default.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_transports/__pycache__/mock.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_transports/__pycache__/wsgi.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_transports/asgi.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_transports/base.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_transports/default.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_transports/mock.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_transports/wsgi.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_types.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_urlparse.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_urls.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/_utils.py create mode 100644 Backend/venv/lib/python3.12/site-packages/httpx/py.typed create mode 100644 Backend/venv/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/INSTALLER create mode 100644 Backend/venv/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/METADATA create mode 100644 Backend/venv/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/RECORD rename Backend/venv/lib/python3.12/site-packages/{h11-0.16.0.dist-info => iniconfig-2.3.0.dist-info}/WHEEL (65%) create mode 100644 Backend/venv/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/licenses/LICENSE create mode 100644 Backend/venv/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/top_level.txt create mode 100644 Backend/venv/lib/python3.12/site-packages/iniconfig/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/iniconfig/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/iniconfig/__pycache__/_parse.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/iniconfig/__pycache__/_version.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/iniconfig/__pycache__/exceptions.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/iniconfig/_parse.py create mode 100644 Backend/venv/lib/python3.12/site-packages/iniconfig/_version.py create mode 100644 Backend/venv/lib/python3.12/site-packages/iniconfig/exceptions.py create mode 100644 Backend/venv/lib/python3.12/site-packages/iniconfig/py.typed create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/INSTALLER create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/METADATA create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/RECORD create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/WHEEL create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/licenses/LICENSE create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/top_level.txt create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/_callers.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/_hooks.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/_manager.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/_result.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/_tracing.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/_version.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/_warnings.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy/_callers.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy/_hooks.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy/_manager.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy/_result.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy/_tracing.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy/_version.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy/_warnings.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pluggy/py.typed create mode 100644 Backend/venv/lib/python3.12/site-packages/py.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/INSTALLER create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/METADATA create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/RECORD create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/WHEEL create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/entry_points.txt create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/licenses/AUTHORS create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/licenses/LICENSE create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__main__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/__main__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/cmdline.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/console.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/filter.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/formatter.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/lexer.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/modeline.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/plugin.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/regexopt.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/scanner.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/sphinxext.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/style.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/token.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/unistring.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/util.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/cmdline.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/console.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/filter.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/filters/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/filters/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatter.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/_mapping.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/bbcode.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/groff.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/html.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/img.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/irc.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/latex.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/other.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/pangomarkup.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/rtf.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/svg.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/terminal.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/terminal256.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/_mapping.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/bbcode.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/groff.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/html.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/img.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/irc.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/latex.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/other.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/pangomarkup.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/rtf.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/svg.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/terminal.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/formatters/terminal256.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexer.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_ada_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_asy_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_cl_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_cocoa_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_csound_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_css_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_googlesql_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_julia_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_lasso_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_lilypond_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_lua_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_luau_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_mapping.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_mql_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_mysql_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_openedge_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_php_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_postgres_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_qlik_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_scheme_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_scilab_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_sourcemod_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_sql_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_stan_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_stata_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_tsql_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_usd_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_vbscript_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/_vim_builtins.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/actionscript.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/ada.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/agile.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/algebra.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/ambient.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/amdgpu.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/ampl.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/apdlexer.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/apl.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/archetype.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/arrow.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/arturo.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/asc.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/asm.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/asn1.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/automation.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/bare.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/basic.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/bdd.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/berry.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/bibtex.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/blueprint.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/boa.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/bqn.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/business.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/c_cpp.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/c_like.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/capnproto.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/carbon.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/cddl.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/chapel.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/clean.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/codeql.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/comal.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/compiled.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/configs.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/console.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/cplint.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/crystal.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/csound.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/css.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/d.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/dalvik.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/data.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/dax.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/devicetree.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/diff.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/dns.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/dotnet.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/dsls.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/dylan.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/ecl.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/eiffel.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/elm.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/elpi.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/email.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/erlang.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/esoteric.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/ezhil.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/factor.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/fantom.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/felix.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/fift.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/floscript.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/forth.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/fortran.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/foxpro.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/freefem.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/func.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/functional.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/futhark.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/gcodelexer.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/gdscript.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/gleam.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/go.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/grammar_notation.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/graph.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/graphics.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/graphql.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/graphviz.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/gsql.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/hare.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/haskell.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/haxe.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/hdl.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/hexdump.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/html.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/idl.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/igor.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/inferno.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/installers.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/int_fiction.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/iolang.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/j.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/javascript.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/jmespath.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/jslt.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/json5.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/jsonnet.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/jsx.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/julia.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/jvm.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/kuin.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/kusto.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/ldap.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/lean.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/lilypond.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/lisp.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/macaulay2.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/make.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/maple.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/markup.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/math.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/matlab.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/maxima.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/meson.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/mime.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/minecraft.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/mips.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/ml.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/modeling.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/modula2.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/mojo.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/monte.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/mosel.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/ncl.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/nimrod.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/nit.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/nix.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/numbair.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/oberon.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/objective.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/ooc.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/openscad.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/other.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/parasail.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/parsers.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/pascal.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/pawn.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/pddl.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/perl.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/phix.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/php.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/pointless.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/pony.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/praat.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/procfile.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/prolog.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/promql.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/prql.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/ptx.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/python.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/q.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/qlik.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/qvt.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/r.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/rdf.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/rebol.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/rego.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/resource.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/ride.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/rita.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/rnc.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/roboconf.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/robotframework.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/ruby.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/rust.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/sas.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/savi.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/scdoc.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/scripting.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/sgf.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/shell.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/sieve.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/slash.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/smalltalk.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/smithy.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/smv.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/snobol.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/solidity.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/soong.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/sophia.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/special.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/spice.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/sql.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/srcinfo.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/stata.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/supercollider.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/tablegen.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/tact.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/tal.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/tcl.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/teal.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/templates.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/teraterm.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/testing.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/text.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/textedit.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/textfmts.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/theorem.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/thingsdb.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/tlb.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/tls.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/tnt.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/trafficscript.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/typoscript.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/typst.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/ul4.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/unicon.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/urbi.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/usd.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/varnish.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/verification.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/verifpal.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/vip.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/vyper.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/web.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/webassembly.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/webidl.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/webmisc.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/wgsl.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/whiley.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/wowtoc.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/wren.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/x10.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/xorg.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/yang.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/yara.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/__pycache__/zig.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_ada_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_asy_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_cl_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_cocoa_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_csound_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_css_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_googlesql_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_julia_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_lasso_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_lilypond_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_lua_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_luau_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_mapping.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_mql_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_mysql_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_openedge_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_php_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_postgres_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_qlik_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_scheme_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_scilab_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_sourcemod_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_sql_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_stan_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_stata_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_tsql_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_usd_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_vbscript_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/_vim_builtins.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/actionscript.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/ada.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/agile.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/algebra.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/ambient.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/amdgpu.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/ampl.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/apdlexer.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/apl.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/archetype.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/arrow.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/arturo.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/asc.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/asm.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/asn1.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/automation.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/bare.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/basic.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/bdd.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/berry.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/bibtex.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/blueprint.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/boa.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/bqn.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/business.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/c_cpp.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/c_like.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/capnproto.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/carbon.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/cddl.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/chapel.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/clean.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/codeql.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/comal.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/compiled.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/configs.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/console.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/cplint.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/crystal.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/csound.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/css.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/d.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/dalvik.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/data.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/dax.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/devicetree.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/diff.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/dns.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/dotnet.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/dsls.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/dylan.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/ecl.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/eiffel.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/elm.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/elpi.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/email.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/erlang.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/esoteric.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/ezhil.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/factor.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/fantom.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/felix.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/fift.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/floscript.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/forth.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/fortran.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/foxpro.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/freefem.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/func.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/functional.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/futhark.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/gcodelexer.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/gdscript.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/gleam.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/go.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/grammar_notation.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/graph.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/graphics.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/graphql.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/graphviz.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/gsql.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/hare.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/haskell.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/haxe.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/hdl.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/hexdump.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/html.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/idl.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/igor.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/inferno.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/installers.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/int_fiction.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/iolang.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/j.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/javascript.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/jmespath.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/jslt.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/json5.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/jsonnet.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/jsx.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/julia.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/jvm.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/kuin.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/kusto.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/ldap.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/lean.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/lilypond.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/lisp.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/macaulay2.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/make.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/maple.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/markup.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/math.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/matlab.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/maxima.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/meson.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/mime.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/minecraft.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/mips.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/ml.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/modeling.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/modula2.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/mojo.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/monte.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/mosel.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/ncl.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/nimrod.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/nit.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/nix.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/numbair.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/oberon.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/objective.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/ooc.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/openscad.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/other.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/parasail.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/parsers.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/pascal.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/pawn.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/pddl.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/perl.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/phix.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/php.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/pointless.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/pony.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/praat.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/procfile.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/prolog.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/promql.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/prql.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/ptx.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/python.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/q.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/qlik.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/qvt.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/r.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/rdf.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/rebol.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/rego.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/resource.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/ride.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/rita.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/rnc.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/roboconf.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/robotframework.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/ruby.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/rust.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/sas.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/savi.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/scdoc.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/scripting.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/sgf.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/shell.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/sieve.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/slash.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/smalltalk.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/smithy.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/smv.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/snobol.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/solidity.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/soong.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/sophia.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/special.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/spice.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/sql.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/srcinfo.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/stata.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/supercollider.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/tablegen.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/tact.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/tal.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/tcl.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/teal.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/templates.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/teraterm.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/testing.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/text.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/textedit.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/textfmts.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/theorem.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/thingsdb.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/tlb.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/tls.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/tnt.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/trafficscript.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/typoscript.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/typst.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/ul4.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/unicon.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/urbi.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/usd.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/varnish.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/verification.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/verifpal.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/vip.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/vyper.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/web.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/webassembly.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/webidl.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/webmisc.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/wgsl.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/whiley.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/wowtoc.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/wren.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/x10.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/xorg.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/yang.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/yara.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/lexers/zig.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/modeline.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/plugin.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/regexopt.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/scanner.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/sphinxext.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/style.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/_mapping.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/abap.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/algol.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/algol_nu.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/arduino.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/autumn.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/borland.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/bw.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/coffee.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/colorful.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/default.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/dracula.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/emacs.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/friendly.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/friendly_grayscale.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/fruity.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/gh_dark.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/gruvbox.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/igor.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/inkpot.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/lightbulb.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/lilypond.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/lovelace.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/manni.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/material.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/monokai.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/murphy.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/native.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/nord.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/onedark.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/paraiso_dark.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/paraiso_light.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/pastie.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/perldoc.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/rainbow_dash.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/rrt.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/sas.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/solarized.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/staroffice.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/stata_dark.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/stata_light.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/tango.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/trac.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/vim.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/vs.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/xcode.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/__pycache__/zenburn.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/_mapping.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/abap.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/algol.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/algol_nu.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/arduino.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/autumn.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/borland.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/bw.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/coffee.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/colorful.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/default.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/dracula.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/emacs.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/friendly.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/friendly_grayscale.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/fruity.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/gh_dark.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/gruvbox.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/igor.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/inkpot.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/lightbulb.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/lilypond.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/lovelace.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/manni.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/material.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/monokai.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/murphy.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/native.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/nord.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/onedark.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/paraiso_dark.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/paraiso_light.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/pastie.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/perldoc.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/rainbow_dash.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/rrt.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/sas.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/solarized.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/staroffice.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/stata_dark.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/stata_light.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/tango.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/trac.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/vim.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/vs.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/xcode.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/styles/zenburn.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/token.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/unistring.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pygments/util.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest-9.0.1.dist-info/INSTALLER create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest-9.0.1.dist-info/METADATA create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest-9.0.1.dist-info/RECORD create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest-9.0.1.dist-info/REQUESTED create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest-9.0.1.dist-info/WHEEL create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest-9.0.1.dist-info/entry_points.txt create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest-9.0.1.dist-info/licenses/LICENSE create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest-9.0.1.dist-info/top_level.txt create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest/__main__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest/__pycache__/__main__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest/py.typed create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_asyncio-1.3.0.dist-info/INSTALLER create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_asyncio-1.3.0.dist-info/METADATA create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_asyncio-1.3.0.dist-info/RECORD create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_asyncio-1.3.0.dist-info/REQUESTED create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_asyncio-1.3.0.dist-info/WHEEL create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_asyncio-1.3.0.dist-info/entry_points.txt create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_asyncio-1.3.0.dist-info/licenses/LICENSE create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_asyncio-1.3.0.dist-info/top_level.txt create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_asyncio/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_asyncio/__pycache__/__init__.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_asyncio/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_asyncio/__pycache__/plugin.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_asyncio/__pycache__/plugin.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_asyncio/plugin.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_asyncio/py.typed create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_cov-7.0.0.dist-info/INSTALLER create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_cov-7.0.0.dist-info/METADATA create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_cov-7.0.0.dist-info/RECORD create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_cov-7.0.0.dist-info/REQUESTED create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_cov-7.0.0.dist-info/WHEEL create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_cov-7.0.0.dist-info/entry_points.txt create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_cov-7.0.0.dist-info/licenses/AUTHORS.rst create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_cov-7.0.0.dist-info/licenses/LICENSE create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_cov/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_cov/__pycache__/__init__.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_cov/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_cov/__pycache__/engine.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_cov/__pycache__/plugin.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_cov/__pycache__/plugin.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_cov/engine.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_cov/plugin.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock-3.15.1.dist-info/INSTALLER create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock-3.15.1.dist-info/METADATA create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock-3.15.1.dist-info/RECORD create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock-3.15.1.dist-info/REQUESTED create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock-3.15.1.dist-info/WHEEL create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock-3.15.1.dist-info/entry_points.txt create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock-3.15.1.dist-info/licenses/LICENSE create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock-3.15.1.dist-info/top_level.txt create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock/__init__.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock/__pycache__/__init__.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock/__pycache__/__init__.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock/__pycache__/_util.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock/__pycache__/_util.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock/__pycache__/_version.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock/__pycache__/plugin.cpython-312-pytest-9.0.1.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock/__pycache__/plugin.cpython-312.pyc create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock/_util.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock/_version.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock/plugin.py create mode 100644 Backend/venv/lib/python3.12/site-packages/pytest_mock/py.typed delete mode 100644 ENTERPRISE_FEATURES_ROADMAP.md create mode 100644 Frontend/src/components/analytics/CustomReportBuilder.tsx create mode 100644 Frontend/src/components/analytics/SimpleChart.tsx rename Frontend/src/components/{ => auth}/ResetPasswordRouteHandler.tsx (88%) create mode 100644 Frontend/src/components/common/Preloader.tsx create mode 100644 Frontend/src/components/notifications/InAppNotificationBell.tsx create mode 100644 Frontend/src/components/notifications/NotificationPreferences.tsx create mode 100644 Frontend/src/components/notifications/NotificationTemplatesModal.tsx create mode 100644 Frontend/src/components/notifications/SendNotificationModal.tsx create mode 100644 Frontend/src/components/payments/BoricaPaymentModal.tsx rename Frontend/src/components/{admin => shared}/CreateBookingModal.tsx (100%) create mode 100644 Frontend/src/components/shared/CreateGroupBookingModal.tsx create mode 100644 Frontend/src/components/shared/HousekeepingManagement.tsx create mode 100644 Frontend/src/components/shared/InspectionManagement.tsx create mode 100644 Frontend/src/components/shared/MaintenanceManagement.tsx create mode 100644 Frontend/src/components/shared/index.ts create mode 100644 Frontend/src/components/tasks/CreateTaskModal.tsx create mode 100644 Frontend/src/components/tasks/TaskDetailModal.tsx create mode 100644 Frontend/src/components/tasks/TaskFilters.tsx create mode 100644 Frontend/src/components/workflows/WorkflowBuilder.tsx create mode 100644 Frontend/src/components/workflows/WorkflowDetailModal.tsx create mode 100644 Frontend/src/contexts/NavigationLoadingContext.tsx create mode 100644 Frontend/src/contexts/ResponsiveContext.tsx create mode 100644 Frontend/src/hooks/useResponsive.ts create mode 100644 Frontend/src/pages/accountant/AnalyticsDashboardPage.tsx create mode 100644 Frontend/src/pages/accountant/InvoiceManagementPage.tsx create mode 100644 Frontend/src/pages/accountant/PaymentManagementPage.tsx create mode 100644 Frontend/src/pages/accountant/index.ts create mode 100644 Frontend/src/pages/admin/AdvancedAnalyticsPage.tsx create mode 100644 Frontend/src/pages/admin/AdvancedRoomManagementPage.tsx create mode 100644 Frontend/src/pages/admin/EmailCampaignManagementPage.tsx create mode 100644 Frontend/src/pages/admin/GroupBookingManagementPage.tsx create mode 100644 Frontend/src/pages/admin/NotificationManagementPage.tsx create mode 100644 Frontend/src/pages/admin/PackageManagementPage.tsx create mode 100644 Frontend/src/pages/admin/RatePlanManagementPage.tsx create mode 100644 Frontend/src/pages/admin/SecurityManagementPage.tsx create mode 100644 Frontend/src/pages/admin/TaskManagementPage.tsx create mode 100644 Frontend/src/pages/admin/WorkflowManagementPage.tsx create mode 100644 Frontend/src/pages/customer/BoricaReturnPage.tsx create mode 100644 Frontend/src/pages/customer/GroupBookingPage.tsx create mode 100644 Frontend/src/pages/customer/index.ts create mode 100644 Frontend/src/pages/staff/AdvancedRoomManagementPage.tsx create mode 100644 Frontend/src/pages/staff/AnalyticsDashboardPage.tsx create mode 100644 Frontend/src/pages/staff/BookingManagementPage.tsx create mode 100644 Frontend/src/pages/staff/GuestProfilePage.tsx create mode 100644 Frontend/src/pages/staff/LoyaltyManagementPage.tsx create mode 100644 Frontend/src/pages/staff/PaymentManagementPage.tsx create mode 100644 Frontend/src/pages/staff/ReceptionDashboardPage.tsx create mode 100644 Frontend/src/pages/staff/index.ts create mode 100644 Frontend/src/routes/accountantRoutes.tsx create mode 100644 Frontend/src/routes/adminRoutes.tsx create mode 100644 Frontend/src/routes/customerRoutes.tsx create mode 100644 Frontend/src/routes/index.ts create mode 100644 Frontend/src/routes/publicRoutes.tsx create mode 100644 Frontend/src/routes/staffRoutes.tsx create mode 100644 Frontend/src/services/api/advancedRoomService.ts create mode 100644 Frontend/src/services/api/analyticsService.ts create mode 100644 Frontend/src/services/api/emailCampaignService.ts create mode 100644 Frontend/src/services/api/groupBookingService.ts create mode 100644 Frontend/src/services/api/notificationService.ts create mode 100644 Frontend/src/services/api/packageService.ts create mode 100644 Frontend/src/services/api/ratePlanService.ts create mode 100644 Frontend/src/services/api/securityService.ts create mode 100644 Frontend/src/services/api/taskService.ts create mode 100644 Frontend/src/services/api/workflowService.ts create mode 100644 Frontend/src/services/responsiveService.ts create mode 100644 logs/app.log diff --git a/Backend/.env.example b/Backend/.env.example index 5aa5b0cd..a6c859cc 100644 --- a/Backend/.env.example +++ b/Backend/.env.example @@ -35,6 +35,14 @@ JWT_ALGORITHM=HS256 JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30 JWT_REFRESH_TOKEN_EXPIRE_DAYS=7 +# ============================================ +# Encryption Configuration +# ============================================ +# Base64-encoded encryption key for data encryption at rest +# Generate a new key using: python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" +# If not set, a temporary key will be generated (not recommended for production) +ENCRYPTION_KEY= + # ============================================ # CORS & Client Configuration # ============================================ diff --git a/Backend/alembic/versions/__pycache__/add_group_booking_tables.cpython-312.pyc b/Backend/alembic/versions/__pycache__/add_group_booking_tables.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2403c8aeb76b63796601d6a7e546fc72564a090f GIT binary patch literal 18482 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 diff --git a/Backend/alembic/versions/__pycache__/add_rate_plan_id_to_bookings.cpython-312.pyc b/Backend/alembic/versions/__pycache__/add_rate_plan_id_to_bookings.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e6927cd9b782d06cdf135ee88e41bde80129616a GIT binary patch literal 1699 zcmah}L1-IC6x~^^R;#t-B*wAg6pC)5*lKA?EJNx*P$`?0W-PC@+TF~q z63Z>chC&LxBoJ^5g=)Hk7H)6B4Mc#$MK4c zW0eFn1uIzoLaHR8Pe8A>W4Yw^mpaB-s(VCpOx@B9ob~K%)wUakRd-eO3b|>xhHYgp zm5Nz#)rMWQLSeq=LEPSIo!Tu&3&VruWlCW4Yzb5`yHHp-KU5mSW|N~*-29%&p00A(!J5l%o; zy2(y>f+LZs$bZle-D!MajYe4Qgm;Gh`R-%gR0pWV1qp2-mo76ULY}3DfnEb#6su*L zytM4(dLx3^9q@sv{Ro$d2HsXadT%XTzIypG54J5#OyUuC%Geqa47__E+~k&J+-Q-U zLV3-Y%3-b9W~*s2(bbu}+#4q@Sstkq$|h<7yR>zi5~FTu4bo<*&}Kl!Er!;A5Dxhb zUtY^^*iDkJTU+@OaT}iP!%K-x%MI|04dX{-pk#dgwiQd+mvI z^|>VfAx%G$rv20N`^Ed8J+%ClPyFlH*NE?IA4s2q!|m1G)q6{OrMo46`rPBm^ZT0* z7X76w{>RsUmvlM}iH@+Gvo>J)k1%)>%ZK<9@&JPRf>EC@VmCIDy`x@+ghd?h$`R|2 zaCXE?0q?|MZ(yNPrpMq6l=}iD)ZjU5mfj?E3`|T7!3L~hIqtO`qR}LGj)$zl)ONH0 zewyJC#{xqQp!BG2SRNY-4V+l?6u1O+q^G%a65F?|9-QyMoEO-=0ICn?y_vl;chC5l z`NtCr_W`620~DtoNmG7iZeM+H_CQ+YxRrOycZy$sejv>SyTi{*ro?MetDGBS2~ERx zP19H+WLtQ2_lBvPwGGm2Q%)qN(D47u()?W7U|OZRRol?uMH16xDkcta*VSw*U?Jsa zf8}Z~m5WUqw@k7~&%!GI7r5s@y@(-%{t;y){WmV4%rhbLLKadpJId4J%RA|($0tFh TPwc3NvV^MW-*E}$gB$%1=;5NQ literal 0 HcmV?d00001 diff --git a/Backend/alembic/versions/add_borica_payment_method.py b/Backend/alembic/versions/add_borica_payment_method.py new file mode 100644 index 00000000..8c5c9ade --- /dev/null +++ b/Backend/alembic/versions/add_borica_payment_method.py @@ -0,0 +1,20 @@ +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql +revision = 'add_borica_payment_method' +down_revision = 'd9aff6c5f0d4' +branch_labels = None +depends_on = None + +def upgrade() -> None: + bind = op.get_bind() + if bind.dialect.name == 'mysql': + op.execute("ALTER TABLE payments MODIFY COLUMN payment_method ENUM('cash', 'credit_card', 'debit_card', 'bank_transfer', 'e_wallet', 'stripe', 'paypal', 'borica') NOT NULL") + else: + pass + +def downgrade() -> None: + bind = op.get_bind() + if bind.dialect.name == 'mysql': + op.execute("ALTER TABLE payments MODIFY COLUMN payment_method ENUM('cash', 'credit_card', 'debit_card', 'bank_transfer', 'e_wallet', 'stripe', 'paypal') NOT NULL") + diff --git a/Backend/alembic/versions/add_group_booking_tables.py b/Backend/alembic/versions/add_group_booking_tables.py new file mode 100644 index 00000000..dd2c0f72 --- /dev/null +++ b/Backend/alembic/versions/add_group_booking_tables.py @@ -0,0 +1,193 @@ +"""add group booking tables + +Revision ID: add_group_booking_001 +Revises: add_guest_profile_crm +Create Date: 2024-01-15 00:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = 'add_group_booking_001' +down_revision = 'add_guest_profile_crm' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Create group_bookings table + op.create_table( + 'group_bookings', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('group_booking_number', sa.String(length=50), nullable=False), + sa.Column('coordinator_id', sa.Integer(), nullable=False), + sa.Column('coordinator_name', sa.String(length=100), nullable=False), + sa.Column('coordinator_email', sa.String(length=100), nullable=False), + sa.Column('coordinator_phone', sa.String(length=20), nullable=True), + sa.Column('group_name', sa.String(length=200), nullable=True), + sa.Column('group_type', sa.String(length=50), nullable=True), + sa.Column('total_rooms', sa.Integer(), nullable=False, server_default='0'), + sa.Column('total_guests', sa.Integer(), nullable=False, server_default='0'), + sa.Column('check_in_date', sa.DateTime(), nullable=False), + sa.Column('check_out_date', sa.DateTime(), nullable=False), + sa.Column('base_rate_per_room', sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column('group_discount_percentage', sa.Numeric(precision=5, scale=2), nullable=True, server_default='0'), + sa.Column('group_discount_amount', sa.Numeric(precision=10, scale=2), nullable=True, server_default='0'), + sa.Column('original_total_price', sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column('discount_amount', sa.Numeric(precision=10, scale=2), nullable=True, server_default='0'), + sa.Column('total_price', sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column('payment_option', sa.Enum('coordinator_pays_all', 'individual_payments', 'split_payment', name='paymentoption'), nullable=False, server_default='coordinator_pays_all'), + sa.Column('deposit_required', sa.Boolean(), nullable=False, server_default='0'), + sa.Column('deposit_percentage', sa.Integer(), nullable=True), + sa.Column('deposit_amount', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('amount_paid', sa.Numeric(precision=10, scale=2), nullable=False, server_default='0'), + sa.Column('balance_due', sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column('status', sa.Enum('draft', 'pending', 'confirmed', 'partially_confirmed', 'checked_in', 'checked_out', 'cancelled', name='groupbookingstatus'), nullable=False, server_default='draft'), + sa.Column('cancellation_policy', sa.Text(), nullable=True), + sa.Column('cancellation_deadline', sa.DateTime(), nullable=True), + sa.Column('cancellation_penalty_percentage', sa.Numeric(precision=5, scale=2), nullable=True, server_default='0'), + sa.Column('special_requests', sa.Text(), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('contract_terms', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('confirmed_at', sa.DateTime(), nullable=True), + sa.Column('cancelled_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['coordinator_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('group_booking_number') + ) + op.create_index(op.f('ix_group_bookings_id'), 'group_bookings', ['id'], unique=False) + op.create_index(op.f('ix_group_bookings_group_booking_number'), 'group_bookings', ['group_booking_number'], unique=True) + op.create_index(op.f('ix_group_bookings_coordinator_id'), 'group_bookings', ['coordinator_id'], unique=False) + op.create_index(op.f('ix_group_bookings_status'), 'group_bookings', ['status'], unique=False) + op.create_index(op.f('ix_group_bookings_check_in_date'), 'group_bookings', ['check_in_date'], unique=False) + op.create_index(op.f('ix_group_bookings_check_out_date'), 'group_bookings', ['check_out_date'], unique=False) + + # Create group_room_blocks table + op.create_table( + 'group_room_blocks', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('group_booking_id', sa.Integer(), nullable=False), + sa.Column('room_type_id', sa.Integer(), nullable=False), + sa.Column('rooms_blocked', sa.Integer(), nullable=False, server_default='0'), + sa.Column('rooms_confirmed', sa.Integer(), nullable=False, server_default='0'), + sa.Column('rooms_available', sa.Integer(), nullable=False, server_default='0'), + sa.Column('rate_per_room', sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column('total_block_price', sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1'), + sa.Column('block_released_at', sa.DateTime(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['group_booking_id'], ['group_bookings.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['room_type_id'], ['room_types.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_group_room_blocks_id'), 'group_room_blocks', ['id'], unique=False) + op.create_index(op.f('ix_group_room_blocks_group_booking_id'), 'group_room_blocks', ['group_booking_id'], unique=False) + op.create_index(op.f('ix_group_room_blocks_room_type_id'), 'group_room_blocks', ['room_type_id'], unique=False) + + # Create group_booking_members table + op.create_table( + 'group_booking_members', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('group_booking_id', sa.Integer(), nullable=False), + sa.Column('full_name', sa.String(length=100), nullable=False), + sa.Column('email', sa.String(length=100), nullable=True), + sa.Column('phone', sa.String(length=20), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('room_block_id', sa.Integer(), nullable=True), + sa.Column('assigned_room_id', sa.Integer(), nullable=True), + sa.Column('individual_booking_id', sa.Integer(), nullable=True), + sa.Column('special_requests', sa.Text(), nullable=True), + sa.Column('preferences', sa.JSON(), nullable=True), + sa.Column('individual_amount', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('individual_paid', sa.Numeric(precision=10, scale=2), nullable=True, server_default='0'), + sa.Column('individual_balance', sa.Numeric(precision=10, scale=2), nullable=True, server_default='0'), + sa.Column('is_checked_in', sa.Boolean(), nullable=False, server_default='0'), + sa.Column('checked_in_at', sa.DateTime(), nullable=True), + sa.Column('is_checked_out', sa.Boolean(), nullable=False, server_default='0'), + sa.Column('checked_out_at', sa.DateTime(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['group_booking_id'], ['group_bookings.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['room_block_id'], ['group_room_blocks.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['assigned_room_id'], ['rooms.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['individual_booking_id'], ['bookings.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_group_booking_members_id'), 'group_booking_members', ['id'], unique=False) + op.create_index(op.f('ix_group_booking_members_group_booking_id'), 'group_booking_members', ['group_booking_id'], unique=False) + op.create_index(op.f('ix_group_booking_members_user_id'), 'group_booking_members', ['user_id'], unique=False) + + # Create group_payments table + op.create_table( + 'group_payments', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('group_booking_id', sa.Integer(), nullable=False), + sa.Column('amount', sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column('payment_method', sa.String(length=50), nullable=False), + sa.Column('payment_type', sa.String(length=50), nullable=False, server_default='deposit'), + sa.Column('payment_status', sa.String(length=50), nullable=False, server_default='pending'), + sa.Column('transaction_id', sa.String(length=100), nullable=True), + sa.Column('payment_date', sa.DateTime(), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('paid_by_member_id', sa.Integer(), nullable=True), + sa.Column('paid_by_user_id', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['group_booking_id'], ['group_bookings.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['paid_by_member_id'], ['group_booking_members.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['paid_by_user_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_group_payments_id'), 'group_payments', ['id'], unique=False) + op.create_index(op.f('ix_group_payments_group_booking_id'), 'group_payments', ['group_booking_id'], unique=False) + op.create_index(op.f('ix_group_payments_payment_status'), 'group_payments', ['payment_status'], unique=False) + + # Add group_booking_id to bookings table + op.add_column('bookings', sa.Column('group_booking_id', sa.Integer(), nullable=True)) + op.create_foreign_key('fk_bookings_group_booking', 'bookings', 'group_bookings', ['group_booking_id'], ['id'], ondelete='SET NULL') + op.create_index(op.f('ix_bookings_group_booking_id'), 'bookings', ['group_booking_id'], unique=False) + + +def downgrade() -> None: + # Drop foreign key and column from bookings table + op.drop_index(op.f('ix_bookings_group_booking_id'), table_name='bookings') + op.drop_constraint('fk_bookings_group_booking', 'bookings', type_='foreignkey') + op.drop_column('bookings', 'group_booking_id') + + # Drop group_payments table + op.drop_index(op.f('ix_group_payments_payment_status'), table_name='group_payments') + op.drop_index(op.f('ix_group_payments_group_booking_id'), table_name='group_payments') + op.drop_index(op.f('ix_group_payments_id'), table_name='group_payments') + op.drop_table('group_payments') + + # Drop group_booking_members table + op.drop_index(op.f('ix_group_booking_members_user_id'), table_name='group_booking_members') + op.drop_index(op.f('ix_group_booking_members_group_booking_id'), table_name='group_booking_members') + op.drop_index(op.f('ix_group_booking_members_id'), table_name='group_booking_members') + op.drop_table('group_booking_members') + + # Drop group_room_blocks table + op.drop_index(op.f('ix_group_room_blocks_room_type_id'), table_name='group_room_blocks') + op.drop_index(op.f('ix_group_room_blocks_group_booking_id'), table_name='group_room_blocks') + op.drop_index(op.f('ix_group_room_blocks_id'), table_name='group_room_blocks') + op.drop_table('group_room_blocks') + + # Drop group_bookings table + op.drop_index(op.f('ix_group_bookings_check_out_date'), table_name='group_bookings') + op.drop_index(op.f('ix_group_bookings_check_in_date'), table_name='group_bookings') + op.drop_index(op.f('ix_group_bookings_status'), table_name='group_bookings') + op.drop_index(op.f('ix_group_bookings_coordinator_id'), table_name='group_bookings') + op.drop_index(op.f('ix_group_bookings_group_booking_number'), table_name='group_bookings') + op.drop_index(op.f('ix_group_bookings_id'), table_name='group_bookings') + op.drop_table('group_bookings') + + # Drop enums + op.execute("DROP TYPE IF EXISTS paymentoption") + op.execute("DROP TYPE IF EXISTS groupbookingstatus") + diff --git a/Backend/alembic/versions/add_rate_plan_id_to_bookings.py b/Backend/alembic/versions/add_rate_plan_id_to_bookings.py new file mode 100644 index 00000000..a416498b --- /dev/null +++ b/Backend/alembic/versions/add_rate_plan_id_to_bookings.py @@ -0,0 +1,30 @@ +"""add rate_plan_id to bookings + +Revision ID: add_rate_plan_id_001 +Revises: add_group_booking_001 +Create Date: 2024-01-20 00:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'add_rate_plan_id_001' +down_revision = ('add_group_booking_001', 'add_loyalty_tables_001') +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add rate_plan_id column to bookings table + op.add_column('bookings', sa.Column('rate_plan_id', sa.Integer(), nullable=True)) + op.create_foreign_key('fk_bookings_rate_plan', 'bookings', 'rate_plans', ['rate_plan_id'], ['id'], ondelete='SET NULL') + op.create_index(op.f('ix_bookings_rate_plan_id'), 'bookings', ['rate_plan_id'], unique=False) + + +def downgrade() -> None: + # Drop foreign key, index, and column from bookings table + op.drop_index(op.f('ix_bookings_rate_plan_id'), table_name='bookings') + op.drop_constraint('fk_bookings_rate_plan', 'bookings', type_='foreignkey') + op.drop_column('bookings', 'rate_plan_id') + diff --git a/Backend/pytest.ini b/Backend/pytest.ini new file mode 100644 index 00000000..741d3416 --- /dev/null +++ b/Backend/pytest.ini @@ -0,0 +1,18 @@ +[pytest] +# Pytest configuration file +testpaths = src/tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = + -v + --strict-markers + --tb=short + --disable-warnings + --color=yes +markers = + slow: marks tests as slow (deselect with '-m "not slow"') + integration: marks tests as integration tests + unit: marks tests as unit tests +asyncio_mode = auto + diff --git a/Backend/requirements.txt b/Backend/requirements.txt index 7e719c73..fd03ba5a 100644 --- a/Backend/requirements.txt +++ b/Backend/requirements.txt @@ -21,6 +21,13 @@ paypal-checkout-serversdk>=1.0.3 pyotp==2.9.0 qrcode[pil]==7.4.2 httpx==0.25.2 +cryptography>=41.0.7 + +# Testing dependencies +pytest==7.4.3 +pytest-asyncio==0.21.1 +pytest-cov==4.1.0 +pytest-mock==3.12.0 # Enterprise features (optional but recommended) # redis==5.0.1 # Uncomment if using Redis caching diff --git a/Backend/run.py b/Backend/run.py index 7413afa3..210c489d 100644 --- a/Backend/run.py +++ b/Backend/run.py @@ -10,4 +10,4 @@ if __name__ == '__main__': base_dir = Path(__file__).parent src_dir = str(base_dir / 'src') use_reload = False - uvicorn.run('src.main:app', host=settings.HOST, port=8000, reload=use_reload, log_level=settings.LOG_LEVEL.lower(), reload_dirs=[src_dir] if use_reload else None, reload_excludes=['*.log', '*.pyc', '*.pyo', '*.pyd', '__pycache__', '**/__pycache__/**', '*.db', '*.sqlite', '*.sqlite3'], reload_delay=1.0) \ No newline at end of file + uvicorn.run('src.main:app', host=settings.HOST, port=settings.PORT, reload=use_reload, log_level=settings.LOG_LEVEL.lower(), reload_dirs=[src_dir] if use_reload else None, reload_excludes=['*.log', '*.pyc', '*.pyo', '*.pyd', '__pycache__', '**/__pycache__/**', '*.db', '*.sqlite', '*.sqlite3'], reload_delay=1.0) \ No newline at end of file diff --git a/Backend/src/__pycache__/main.cpython-312.pyc b/Backend/src/__pycache__/main.cpython-312.pyc index 10b06b5888d0be570bd4d16e08769f61851887db..cbbe56823f363ef64cdedba8ad2c421d51ba6701 100644 GIT binary patch delta 965 zcmb8sZ%7ki90%}wHs_xW=XA4nTK+?8vp-f={zILbTgyTvA-Z0yYdhuIZj)^)R^YpI z|EE9yj1og3g!QI63Jax#-b5r8QXvF_uXlQMd@gilxR#cAQ_YlOGYH4k}=7+Sfa_v!fji* z7jPRfAQ_YlOGYH4M5v+@8ROi=IJ#Z|YqT)~{?tEdqjr*;a&$Qj6y;4gC`Yzynwf_B zg0|ur8O7PT8AtaG6-7NjWb+ioByc*7pK}6c3utr_j^*jn@bEM)@?pC`zqj`H#DB&2 z8{mbZ01g+@C;{fe%W`$tCN30KtC2zcVEl?kHZ`o9bq)J@yU*tJ4EQ+E%T zhEjpb&-$FHf!6Ku^S!*C_46KgD%ADi-hSIPkH^Wo`~Ie^V~};*IR_3rLMqjGSwCm% zcd?}Vl=Mb*KWlfgeOzivj*qhsczOSDD#+&qmUr3gtkBQ$eQt5}P+dBj5qFOFAjK2B zg=|chQ`_NzYsUbx&2XjH1(?F_>YpG>g!aC%T2bl<_^^loczDvq^?h})mjES?bZ0yi+9^5WI&%Ogtayr zzO?FL$+{07w(F$~Pk^^=bnd#=@KIa!PFuBpmixitI*hg>~Q delta 502 zcmZ3rlyT2AM!wU$yj%=G5O`c6bC%IWJ_)7{>5Up~oRc4LNKSswS+aR9mmC|AAv!rh z#B#DG7lfV*p*OD=e#1U_uj~x=_SE1Bj9G${CmIS*&X*Ho??mFymlG52O6^Vw$kt$B zD4LPNkiwhO6V)^Mgq*W{FB3y*a%yi%U==q5!)iv5OBfiU`hcQ6K=b54=CKE*DgoIF zlSD*VfV9#i5z)zo@)F`I5XHzOP+EPmp{~^AUGkEQeUq=qM>6yDNNhGxFy>>F+nl2J ziE*=_p%EKn?`9YC5JuM3K)nk$&$U>{%nf1|C4h(plRa(RSvLTgCpS;E(O_g;FnOP? zF6(BXz@^C_Z2vP(+5EwdpOJe4Nbw{PF=H~9Lk;7C&Fv1XOl(s?B2y<%atdc$Klzo@ zR8f%0w>a|i;#12~^Gb?91{ZCfe9SqG`zMeI@^Ntj&t`L%cov-ij_*Jb;>gMM-qxEn zJy$br-sRKFyjdmCijgsLa(s}MtR&FLBU+ADWwH$4WLe7@nZGHCl-V$UvjMRIF%FNB diff --git a/Backend/src/config/__pycache__/database.cpython-312.pyc b/Backend/src/config/__pycache__/database.cpython-312.pyc index 8bcd92efd2d1daeb2ff6d43d9d85196a4a66bfa8..54f5dd6df1618f5a42bb23988ce9085083f76169 100644 GIT binary patch delta 20 acmZ1|v`~oqG%qg~0}$MGQrgHZ#t8s4Nd$cW delta 20 acmZ1|v`~oqG%qg~0}wokP}s;V#t8s4^aPjy diff --git a/Backend/src/config/__pycache__/settings.cpython-312.pyc b/Backend/src/config/__pycache__/settings.cpython-312.pyc index 1a0c186bd890cd76b288ee901d94baefcafa0646..3fb66bc875148d2740600a57e7f3055ec7f39f0b 100644 GIT binary patch delta 2161 zcmZvc?@wD*7{~9u6etW3T7I|!*Cq$-``t9V0#?H8izpY>=%G4 zUIaVA+YEk}N$w`}JxcF^_Ziwl=nkd#!3PYzN~ne@yaqmm*%1MJw5fYYp38Z-X$P_M zko_^WxoIbLA26f4pmi-&L^z)C22VXq#g^h6WK^o+)vJV zQo>l!BP2Hr&X^*HB{2Y%WjQJ>iJsmQgLBmHaXEqkw|~UrbqohRy)=MP8Lu_;ma75)|W-x^PlUpQuV64os|M)^% zTHmJNz);a5;>x+me+-R$ABwEytp{U99p43TNEi!5vY}TXknw3)bczqVBUrpxMzNeL zwo_cq4{8U+YBs(-UW;3^mV9o(7e#5n@(PwLEG<}G#nOhQ9ZQFnSz2A_z)KgFUMxe} zO0iKJEUimU;-6Dkrm@UmnZ&gQ5ma)XJtYG=y-PU6d3+^fr z)p6VwcWkWPTm7~yEk)AN)g$9tYWTSLV*22R(iiz6Q`Mf3ZciMU)RcF}b*NJhekpSZ z$zyZPo-=Lt9ht|{y3*Z@L*3Z(teUzn3~5L3sAev$E8iVI)Xg0~$1>NX8)tvxxJ5o7 zJmI*i34zr7gX1zMgyXV`&#tFCrjN>IQbRk&L*2~JnfaQ<6h&8I*EDH5zrVlqc66m~ kEx{forR9`YRSxjHm%sb^6OJr?_X}KJ@pk8Jy@Ch!AIahtCjbBd delta 1336 zcmZ9LOH30{6ozL8up=!mY312c5h4ZJ0um^tEl^M%LQ&C3c*)~ju*!3aQHV)FK~WdR z+y#4g3LzR2*KUXlL*ipW!b%f2E+wv9dCyIHb8!}b?)=|5|D3sZ<|wdNm;Na&EmdLH z&wgjroBcBVqjr???!c_Y>!5yH-_npN%0NCb5gmL1w$>Dq0u5^l__rGcFCc})l*cHm z3W%y8grp2Ru`t>sXfw1()Il;C4JT+ETA@wCMSOq@2@DsZU7}7-I|S{7E{UGu(U%16 zh8~F)a~ct}7cMi}o_O=KJo<{jSD{a8F5%7B1a-i5iIx%*J(t`WAA}*vE#ry9qGtqd zO4P;asGwsoF41!Sya_>podc21p+Y)`$wXBgl3+>-spJQ7D}kW|q7rrU0d5O=2c{+J z;RDPFItz0W^>R8dr~?)lbtU$)ibop)S3yi_t|pmuGo>QS0rw>LJln%>DeLi7cqq9( zp7aQlNCT`9)1(ILYsp*e9v+WbjLd@3Zb>WAEFN)y5$# z9Yh&svaA~fqA|bM>q<6y(d|a*L5ZLo3$^4FtF6?VvuMIeBd!pAle6P0$GU2i6DTz( zwJ3Ec^(a0RKV8i&HivQ9iqb`Qa+|~bYIp M*603J-M|<72M{F|`2YX_ diff --git a/Backend/src/config/settings.py b/Backend/src/config/settings.py index a5e00ed4..2f05da3b 100644 --- a/Backend/src/config/settings.py +++ b/Backend/src/config/settings.py @@ -21,6 +21,7 @@ class Settings(BaseSettings): JWT_ALGORITHM: str = Field(default='HS256', description='JWT algorithm') JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=30, description='JWT access token expiration in minutes') JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = Field(default=7, description='JWT refresh token expiration in days') + ENCRYPTION_KEY: str = Field(default='', description='Base64-encoded encryption key for data encryption at rest') CLIENT_URL: str = Field(default='http://localhost:5173', description='Frontend client URL') CORS_ORIGINS: List[str] = Field(default_factory=lambda: ['http://localhost:5173', 'http://localhost:3000', 'http://127.0.0.1:5173'], description='Allowed CORS origins') RATE_LIMIT_ENABLED: bool = Field(default=True, description='Enable rate limiting') @@ -51,6 +52,12 @@ class Settings(BaseSettings): PAYPAL_CLIENT_ID: str = Field(default='', description='PayPal client ID') PAYPAL_CLIENT_SECRET: str = Field(default='', description='PayPal client secret') PAYPAL_MODE: str = Field(default='sandbox', description='PayPal mode: sandbox or live') + BORICA_TERMINAL_ID: str = Field(default='', description='Borica Terminal ID') + BORICA_MERCHANT_ID: str = Field(default='', description='Borica Merchant ID') + BORICA_PRIVATE_KEY_PATH: str = Field(default='', description='Borica private key file path') + BORICA_CERTIFICATE_PATH: str = Field(default='', description='Borica certificate file path') + BORICA_GATEWAY_URL: str = Field(default='https://3dsgate-dev.borica.bg/cgi-bin/cgi_link', description='Borica gateway URL (test or production)') + BORICA_MODE: str = Field(default='test', description='Borica mode: test or production') @property def database_url(self) -> str: diff --git a/Backend/src/main.py b/Backend/src/main.py index ad038dff..920396c4 100644 --- a/Backend/src/main.py +++ b/Backend/src/main.py @@ -95,9 +95,10 @@ async def metrics(): return {'status': 'success', 'service': settings.APP_NAME, 'version': settings.APP_VERSION, 'environment': settings.ENVIRONMENT, 'timestamp': datetime.utcnow().isoformat()} app.include_router(auth_routes.router, prefix='/api') app.include_router(auth_routes.router, prefix=settings.API_V1_PREFIX) -from .routes import room_routes, booking_routes, payment_routes, invoice_routes, banner_routes, favorite_routes, service_routes, service_booking_routes, promotion_routes, report_routes, review_routes, user_routes, audit_routes, admin_privacy_routes, system_settings_routes, contact_routes, page_content_routes, home_routes, about_routes, contact_content_routes, footer_routes, chat_routes, privacy_routes, terms_routes, refunds_routes, cancellation_routes, accessibility_routes, faq_routes, loyalty_routes, guest_profile_routes +from .routes import room_routes, booking_routes, payment_routes, invoice_routes, banner_routes, favorite_routes, service_routes, service_booking_routes, promotion_routes, report_routes, review_routes, user_routes, audit_routes, admin_privacy_routes, system_settings_routes, contact_routes, page_content_routes, home_routes, about_routes, contact_content_routes, footer_routes, chat_routes, privacy_routes, terms_routes, refunds_routes, cancellation_routes, accessibility_routes, faq_routes, loyalty_routes, guest_profile_routes, analytics_routes, workflow_routes, task_routes, notification_routes, group_booking_routes, advanced_room_routes, rate_plan_routes, package_routes, security_routes, email_campaign_routes app.include_router(room_routes.router, prefix='/api') app.include_router(booking_routes.router, prefix='/api') +app.include_router(group_booking_routes.router, prefix='/api') app.include_router(payment_routes.router, prefix='/api') app.include_router(invoice_routes.router, prefix='/api') app.include_router(banner_routes.router, prefix='/api') @@ -125,6 +126,15 @@ app.include_router(faq_routes.router, prefix='/api') app.include_router(chat_routes.router, prefix='/api') app.include_router(loyalty_routes.router, prefix='/api') app.include_router(guest_profile_routes.router, prefix='/api') +app.include_router(analytics_routes.router, prefix='/api') +app.include_router(workflow_routes.router, prefix='/api') +app.include_router(task_routes.router, prefix='/api') +app.include_router(notification_routes.router, prefix='/api') +app.include_router(advanced_room_routes.router, prefix='/api') +app.include_router(rate_plan_routes.router, prefix='/api') +app.include_router(package_routes.router, prefix='/api') +app.include_router(security_routes.router, prefix='/api') +app.include_router(email_campaign_routes.router, prefix='/api') app.include_router(room_routes.router, prefix=settings.API_V1_PREFIX) app.include_router(booking_routes.router, prefix=settings.API_V1_PREFIX) app.include_router(payment_routes.router, prefix=settings.API_V1_PREFIX) @@ -154,6 +164,13 @@ app.include_router(faq_routes.router, prefix=settings.API_V1_PREFIX) app.include_router(chat_routes.router, prefix=settings.API_V1_PREFIX) app.include_router(loyalty_routes.router, prefix=settings.API_V1_PREFIX) app.include_router(guest_profile_routes.router, prefix=settings.API_V1_PREFIX) +app.include_router(analytics_routes.router, prefix=settings.API_V1_PREFIX) +app.include_router(workflow_routes.router, prefix=settings.API_V1_PREFIX) +app.include_router(task_routes.router, prefix=settings.API_V1_PREFIX) +app.include_router(notification_routes.router, prefix=settings.API_V1_PREFIX) +app.include_router(advanced_room_routes.router, prefix=settings.API_V1_PREFIX) +app.include_router(rate_plan_routes.router, prefix=settings.API_V1_PREFIX) +app.include_router(package_routes.router, prefix=settings.API_V1_PREFIX) app.include_router(page_content_routes.router, prefix='/api') app.include_router(page_content_routes.router, prefix=settings.API_V1_PREFIX) logger.info('All routes registered successfully') diff --git a/Backend/src/middleware/ip_whitelist.py b/Backend/src/middleware/ip_whitelist.py new file mode 100644 index 00000000..1f17cfd2 --- /dev/null +++ b/Backend/src/middleware/ip_whitelist.py @@ -0,0 +1,195 @@ +from fastapi import Request, HTTPException, status +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.responses import JSONResponse +from sqlalchemy.orm import Session +from typing import List +from ..config.database import get_db +from ..config.logging_config import get_logger +from ..config.settings import settings +from ..models.security_event import IPWhitelist, IPBlacklist, SecurityEvent, SecurityEventType, SecurityEventSeverity +from datetime import datetime +import ipaddress + +logger = get_logger(__name__) + +class IPWhitelistMiddleware(BaseHTTPMiddleware): + """Middleware to enforce IP whitelisting and blacklisting""" + + def __init__(self, app, enabled: bool = True, whitelist_only: bool = False): + super().__init__(app) + self.enabled = enabled + self.whitelist_only = whitelist_only # If True, only whitelisted IPs allowed + + async def dispatch(self, request: Request, call_next): + if not self.enabled: + return await call_next(request) + + # Skip IP check for health checks and public endpoints + if request.url.path in ['/health', '/api/health', '/metrics']: + return await call_next(request) + + client_ip = self._get_client_ip(request) + + if not client_ip: + logger.warning("Could not determine client IP address") + return await call_next(request) + + # Check blacklist first + if await self._is_blacklisted(client_ip): + await self._log_security_event( + request, + SecurityEventType.ip_blocked, + SecurityEventSeverity.high, + f"Blocked request from blacklisted IP: {client_ip}" + ) + return JSONResponse( + status_code=status.HTTP_403_FORBIDDEN, + content={"status": "error", "message": "Access denied"} + ) + + # Check whitelist if whitelist_only mode is enabled + if self.whitelist_only: + if not await self._is_whitelisted(client_ip): + await self._log_security_event( + request, + SecurityEventType.permission_denied, + SecurityEventSeverity.medium, + f"Blocked request from non-whitelisted IP: {client_ip}" + ) + return JSONResponse( + status_code=status.HTTP_403_FORBIDDEN, + content={"status": "error", "message": "Access denied. IP not whitelisted."} + ) + + return await call_next(request) + + def _get_client_ip(self, request: Request) -> str: + """Extract client IP address from request""" + # Check for forwarded IP (when behind proxy/load balancer) + forwarded_for = request.headers.get("X-Forwarded-For") + if forwarded_for: + # X-Forwarded-For can contain multiple IPs, take the first one + return forwarded_for.split(",")[0].strip() + + real_ip = request.headers.get("X-Real-IP") + if real_ip: + return real_ip.strip() + + # Fallback to direct client IP + if request.client: + return request.client.host + + return None + + async def _is_blacklisted(self, ip_address: str) -> bool: + """Check if IP address is blacklisted""" + try: + db_gen = get_db() + db = next(db_gen) + try: + # Check exact match + blacklist_entry = db.query(IPBlacklist).filter( + IPBlacklist.ip_address == ip_address, + IPBlacklist.is_active == True + ).first() + + if blacklist_entry: + # Check if temporary block has expired + if blacklist_entry.blocked_until and blacklist_entry.blocked_until < datetime.utcnow(): + # Block expired, deactivate it + blacklist_entry.is_active = False + db.commit() + return False + return True + + # Check CIDR ranges (if needed) + # This is a simplified version - you might want to cache this + all_blacklist = db.query(IPBlacklist).filter( + IPBlacklist.is_active == True + ).all() + + for entry in all_blacklist: + try: + if '/' in entry.ip_address: # CIDR notation + network = ipaddress.ip_network(entry.ip_address, strict=False) + if ipaddress.ip_address(ip_address) in network: + return True + except (ValueError, ipaddress.AddressValueError): + continue + + return False + finally: + db.close() + except Exception as e: + logger.error(f"Error checking IP blacklist: {str(e)}") + return False + + async def _is_whitelisted(self, ip_address: str) -> bool: + """Check if IP address is whitelisted""" + try: + db_gen = get_db() + db = next(db_gen) + try: + # Check exact match + whitelist_entry = db.query(IPWhitelist).filter( + IPWhitelist.ip_address == ip_address, + IPWhitelist.is_active == True + ).first() + + if whitelist_entry: + return True + + # Check CIDR ranges + all_whitelist = db.query(IPWhitelist).filter( + IPWhitelist.is_active == True + ).all() + + for entry in all_whitelist: + try: + if '/' in entry.ip_address: # CIDR notation + network = ipaddress.ip_network(entry.ip_address, strict=False) + if ipaddress.ip_address(ip_address) in network: + return True + except (ValueError, ipaddress.AddressValueError): + continue + + return False + finally: + db.close() + except Exception as e: + logger.error(f"Error checking IP whitelist: {str(e)}") + return False + + async def _log_security_event( + self, + request: Request, + event_type: SecurityEventType, + severity: SecurityEventSeverity, + description: str + ): + """Log security event""" + try: + db_gen = get_db() + db = next(db_gen) + try: + client_ip = self._get_client_ip(request) + event = SecurityEvent( + event_type=event_type, + severity=severity, + ip_address=client_ip, + user_agent=request.headers.get("User-Agent"), + request_path=str(request.url.path), + request_method=request.method, + description=description, + details={ + "url": str(request.url), + "headers": dict(request.headers) + } + ) + db.add(event) + db.commit() + finally: + db.close() + except Exception as e: + logger.error(f"Error logging security event: {str(e)}") + diff --git a/Backend/src/models/__init__.py b/Backend/src/models/__init__.py index 50280bc0..426064f5 100644 --- a/Backend/src/models/__init__.py +++ b/Backend/src/models/__init__.py @@ -32,4 +32,16 @@ from .guest_note import GuestNote from .guest_tag import GuestTag, guest_tag_association from .guest_communication import GuestCommunication, CommunicationType, CommunicationDirection from .guest_segment import GuestSegment, guest_segment_association -__all__ = ['Role', 'User', 'RefreshToken', 'PasswordResetToken', 'RoomType', 'Room', 'Booking', 'Payment', 'Service', 'ServiceUsage', 'ServiceBooking', 'ServiceBookingItem', 'ServicePayment', 'ServiceBookingStatus', 'ServicePaymentStatus', 'ServicePaymentMethod', 'Promotion', 'CheckInCheckOut', 'Banner', 'Review', 'Favorite', 'AuditLog', 'CookiePolicy', 'CookieIntegrationConfig', 'SystemSettings', 'Invoice', 'InvoiceItem', 'PageContent', 'PageType', 'Chat', 'ChatMessage', 'ChatStatus', 'LoyaltyTier', 'TierLevel', 'UserLoyalty', 'LoyaltyPointTransaction', 'TransactionType', 'TransactionSource', 'LoyaltyReward', 'RewardType', 'RewardStatus', 'RewardRedemption', 'RedemptionStatus', 'Referral', 'ReferralStatus', 'GuestPreference', 'GuestNote', 'GuestTag', 'guest_tag_association', 'GuestCommunication', 'CommunicationType', 'CommunicationDirection', 'GuestSegment', 'guest_segment_association'] \ No newline at end of file +from .workflow import Workflow, WorkflowInstance, Task, TaskComment, WorkflowType, WorkflowStatus, WorkflowTrigger, TaskStatus, TaskPriority +from .notification import Notification, NotificationTemplate, NotificationPreference, NotificationDeliveryLog, NotificationChannel, NotificationStatus, NotificationType +from .group_booking import GroupBooking, GroupBookingMember, GroupRoomBlock, GroupPayment, GroupBookingStatus, PaymentOption +from .room_maintenance import RoomMaintenance, MaintenanceType, MaintenanceStatus +from .housekeeping_task import HousekeepingTask, HousekeepingStatus, HousekeepingType +from .room_inspection import RoomInspection, InspectionType, InspectionStatus +from .room_attribute import RoomAttribute +from .rate_plan import RatePlan, RatePlanRule, RatePlanType, RatePlanStatus +from .package import Package, PackageItem, PackageStatus, PackageItemType +from .security_event import SecurityEvent, SecurityEventType, SecurityEventSeverity, IPWhitelist, IPBlacklist, OAuthProvider, OAuthToken +from .gdpr_compliance import DataSubjectRequest, DataSubjectRequestType, DataSubjectRequestStatus, DataRetentionPolicy, ConsentRecord +from .email_campaign import Campaign, CampaignStatus, CampaignType, CampaignSegment, EmailTemplate, CampaignEmail, EmailStatus, EmailClick, DripSequence, DripSequenceStep, DripSequenceEnrollment, Unsubscribe +__all__ = ['Role', 'User', 'RefreshToken', 'PasswordResetToken', 'RoomType', 'Room', 'Booking', 'Payment', 'Service', 'ServiceUsage', 'ServiceBooking', 'ServiceBookingItem', 'ServicePayment', 'ServiceBookingStatus', 'ServicePaymentStatus', 'ServicePaymentMethod', 'Promotion', 'CheckInCheckOut', 'Banner', 'Review', 'Favorite', 'AuditLog', 'CookiePolicy', 'CookieIntegrationConfig', 'SystemSettings', 'Invoice', 'InvoiceItem', 'PageContent', 'PageType', 'Chat', 'ChatMessage', 'ChatStatus', 'LoyaltyTier', 'TierLevel', 'UserLoyalty', 'LoyaltyPointTransaction', 'TransactionType', 'TransactionSource', 'LoyaltyReward', 'RewardType', 'RewardStatus', 'RewardRedemption', 'RedemptionStatus', 'Referral', 'ReferralStatus', 'GuestPreference', 'GuestNote', 'GuestTag', 'guest_tag_association', 'GuestCommunication', 'CommunicationType', 'CommunicationDirection', 'GuestSegment', 'guest_segment_association', 'Workflow', 'WorkflowInstance', 'Task', 'TaskComment', 'WorkflowType', 'WorkflowStatus', 'WorkflowTrigger', 'TaskStatus', 'TaskPriority', 'Notification', 'NotificationTemplate', 'NotificationPreference', 'NotificationDeliveryLog', 'NotificationChannel', 'NotificationStatus', 'NotificationType', 'GroupBooking', 'GroupBookingMember', 'GroupRoomBlock', 'GroupPayment', 'GroupBookingStatus', 'PaymentOption', 'RoomMaintenance', 'MaintenanceType', 'MaintenanceStatus', 'HousekeepingTask', 'HousekeepingStatus', 'HousekeepingType', 'RoomInspection', 'InspectionType', 'InspectionStatus', 'RoomAttribute', 'RatePlan', 'RatePlanRule', 'RatePlanType', 'RatePlanStatus', 'Package', 'PackageItem', 'PackageStatus', 'PackageItemType', 'SecurityEvent', 'SecurityEventType', 'SecurityEventSeverity', 'IPWhitelist', 'IPBlacklist', 'OAuthProvider', 'OAuthToken', 'DataSubjectRequest', 'DataSubjectRequestType', 'DataSubjectRequestStatus', 'DataRetentionPolicy', 'ConsentRecord', 'Campaign', 'CampaignStatus', 'CampaignType', 'CampaignSegment', 'EmailTemplate', 'CampaignEmail', 'EmailStatus', 'EmailClick', 'DripSequence', 'DripSequenceStep', 'DripSequenceEnrollment', 'Unsubscribe'] \ No newline at end of file diff --git a/Backend/src/models/__pycache__/__init__.cpython-312.pyc b/Backend/src/models/__pycache__/__init__.cpython-312.pyc index 0f1848e2358f04ed4e23ba6df5b71701f79d0608..597ac016a47cbde17573d3a96cd434fbbbad8a2d 100644 GIT binary patch delta 2489 zcmbuBNplld5XVO{#EUj#2;oR&0|l~GPT7MOKmi#c$uTs`Fd99}gI>=JqmhijVOYWv zb}$h3H93%+QhbM8a-ygdRj(@LFOW-;N^#BW=aCW1fmB@OpS%D4*6uf5{%yh8CCOhC zi5^YN}c9kUwF$QezQo+oC6 z8zsWvI+yO5?hq-Ooi!}m516eIol@S6qY^u>O_*6m&g)(6)N8EDm@5ybO4HY_wC>fe zscUtJB<(26>M~R1IvLu5JdoP2_i-@XE+P^%X@&25R;li@^c>NZmlerz!xr(Dm9KO5 z*i%a-Td{DVIifRXm{p^~L@YEiK0_hXVg@9K=T5Z+)v-H8qQJ~Ly_TT{y}fx)xN5VP zpO6Ynz5+R1#4@?jam$y_qUMv+&*ce}lUTQ+?vLj@r(ua62Q9bYGgscUZNQN2Ih?EgVk35~UaFb$A+U7gUUP0T5_liV7VdpS_?Yks;ZwqA zgwF|I5LOWS2?K;d!VqC4VHIIDAw&3*u!gXfkR=Qg))8`q^@MytED(x>5yDr5QNjko zMgkBv5jGRH5VjI@!Zw0IC=pCTnZO8RgbHDtU=g+xs+u;Wd7Mln*aU~*5_S+gLXF@P z>VyVig0NF!IQ_%Dqc1&-23-?j2mVhv6>+<*U%o(wMWRBzO)t$fx7maT&&{~#_2k#D z%ZFymcT7#{sTzoR<89qY_hoPQ@^rV^s)&?Y!kTS6PnAg|RXi|SEk@N%bbGSP=(79U z&nuzmbZ45$^J*!QwXk96vJ=?;3)EJmf=b!-bW{FQz%6ypXRAF^Y>{O2-RWldvHqXp zi%wlPIM?;N+EleW7yC(OE+T(x-I1wZy7vcu@@!#XSU delta 97 zcmbQFepQ6;G%qg~0}uorSIE4;Igw9-=?mjVjW>)u=?p2#ix{IKDpfR9H#aaxFf*EM xX6EK&WcJgH+#JGtooTYHP!xAOP%9%47k4mBZW3zZ{lvt;?aJ54UK9%y1OVk`cs_mG%qg~0}!|=C}lQtZseQD#H7T$c_Gs_rg|oZRF+g$Ajz2`vzig4h=C!6 zX${9}W(Ye<6DX&YBAX(&Mrbt?R7?v=mpoQo+F)G@DU3+EbSf1!m2R;+<>zN-=B3}_ zNCGl8XR(?w22}~B7v+~0#DheDQpI|iDYtmsK|(Oa!kKv~nPr(NrHMIkRaLx2i6yD= z1v!a%U>zJmKyCn#=clQ=c`;iTqtz_|6m#Q&#`270g~i|^Cs(Z$T1$>?8DK_ z=z4)gG%qg~0}wokP{?%R*vL1LiD?_t=7m6lrBX&ycJpyoS4L((P0h`5>|KoV zMN&YeMbaQb21Llg2tFXAxOuZ9XEP)7MHZ3CoZL$ogC?)zwi1+JOknIt`N{yI!8!rK Cu^Q$8 diff --git a/Backend/src/models/__pycache__/email_campaign.cpython-312.pyc b/Backend/src/models/__pycache__/email_campaign.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0bf935ee1fa588bbd5b66a0596259ebf18c8b0f6 GIT binary patch literal 12843 zcmcIqTWlL=b|xi?_q#~yE?c%7o3Sk=wrpo($5;6#$FgI~PQq-=Qk=0&nHOb-jICt1 zp|L5DeJCo!KtjVsS9vH(Z45*$ibCy^`?7UW6kwnMVoebgL0{HyTZDaRUfS>cKW8|k zDPP*%v32;*`LA>4Kj(bs{PQm!kJE(f&$e$${qj|l=|AX3yR5as55G2>OkbD;lVDDp zGUk{$(-v#Xw8z>rmY5~e5$niUW7dowIS0PS)0i zw(e)P4Y9T!wDpSqui9&~g&EfeuHV3oFm3?cpn==PxSimJ4BT$U4TBpoaC;cH3*2r4 zx0i8y!0k0~`xv(m+rjw9}Y7wN=oN7%LaOOr}%V1Sm}B3fP3Gxr*-ibr-U7VOmZ~scG7mu4W;rs=TlZTGFZ6}9UvZ1ooR?O&0>z!h`U>YbviUN5?4>8YBo`wo3n zkKW5`=;fWIJ=AnzHBNoKe(EO%z^ktSUgZfMyazomXh^89i+Z*5;bZU)Xc1xZs6Q?I z0OuOUhZoSta_g%#js=f^7QGFtg5?LVAJXhFJokWMEKkd^2A|nt18;$qdkrIRBrg7J zg0Ep*?L7^#Ud`*-#K!^+3I2wDnw&QC77fofT;p{J0mBNmwB)|jlDoAfcUwztv?cfD zmfY>c#orDgD0FIjv|;T#pV?yMnJq?ZEsR=Y+>x*7^`QotYVYFJXVmYadbW4k4D#LG zk~_w@e5Z#SX4Br_9@Ziv?w!pxsTOXTwnudRdhKa#*Bjw}m)`#;=>Ix!QFz9DzekgQ zY(L}niaib71B`1D2WQNp6*YajZoW~g+3-ZoK~~c*?r9k55aSLAgTjXU8}(5RGwzVM zxuNZm`AyNys$FxXs;|+}%Tr73cpeaoBR)cys#Ru4Rw56py07IkfQC6Bb6{6@9)MiClkC*MrM=QP zrX{qfx^?IqPvjZ;%~Qcyd&$(!LGAn+184)UA(fC^#CE3Scw#z^J(5>LwL<*E1nRRG z2fd%DZn3%6k;=tHqORq29Y=i|*CFohuy=e)jFjE8m>FqJ-kbvumEt+vBCd)zG%( zJr72{J$m_@qn9gJrWH@Jc;Y{;rciHbxExYCM~ZK*b@tTD!R}JmmoHa#9$DRV^x>2e zyih#DTW>2*t)TruwD*P<-YHuY@6O^$)S-v%%ihY4qpL&59tuj}VsT=vYoH{Qr(c9$y%Ve3&r~kQl)m>WmhN@98@4RYtXLjQD;rK%Ed6WY zzPl$DU%hvF*`kE^6elsXFTC*fVqsZSyayl*%scbtN0nX2SGS(1ymd+Gx{L*^Mfyu` zm-9+wtauJRx(7=$%N;8pC|yUWGSF4BVQm2=upetYvX2nsn0NJEnN(=R;?x&Zgs7&HPB{6E4V(E%uF(_xL1jphQtuk+m63HcQotEqNcj-`$LZNOX z-#P)GJo+>D%zL9gX%Z8w0o_^Fte!k2jiEw%4djIM6Dl4c@&=KUAb>=5;BlI`=ZU-t zG8ye{a^}+8RHwUe=^eUdkmg+~F;DCgM2gGjDAmCos9kfM0CqA`Tg;j#5z*(lBMzn$ zGQ4#non+X88FA)f9pqw>GeK5>e7N*65%K}j&xrh-$R86SlP~>(NK-&hLSd4bK|%fp z5Hi@si@(UOWZWg0<4@(i#kieR)jjc@5^{do;UI4zE}g z{~OE%kbzxSiIu>_(>2)Frz_sAlU%dtiCSyc=f7vo!d|lpPf0L{PVDV^$nRo%+u0D< zan18I@VwNHjcw2-w5SLCMm?YUf#vmP)-Y0T}2t&9W83Ciq6b2x-os;Gn$d&#SZB$Z2d0xh{4Qfab`o(mS zM8M=gdMkei@_&a;4EKbU&e28;#Ju-Heo;{T+l!~k1x9XPEnU2Gy*v(;^u19$)uf;` zb?s362aBh_CqVIgR#$rH*6NDGi5GB$EN^DaZSagB1yZWR zyY9vs^NLJ`v5DPcJrC9)dS=O}HeDFmp^vO7EM*0?e2S*7MjlsIKl;|&ap&2{<0{+8 z}_Eut>=R2bz=`9ST`gDiR!;_jjKZ3fNJ0~YDogi|E z2=ngr61ed=Mx|3kSmg34O2~yw5rh2vWOVp>YzU8_^`#&HwNc`NLSz=fcd*6r$URD9 zv;6x{sl@C=0wvY0ho2N?v)~a?9R+W-z>}40Q74ss#*mX$vVfBz2i3(RPJZKM2!cnm ztS3hl{Pch^w9vX119GV15hW=n#WSKTBmQo-W(-tZpp{$bVVj`^y&m?CK1V@52D&dQ2*Y->JgoRfISR3t$CSV;8rZ0n z5iz}kfF`ns1y>Xd)}yIfa5AI>4`8X`zS1cKL*X%mEsedp2JT))81yc}pvYb}4%&`k z=FliWR;!4Gta5*8V)>@xJB+Zz)m5^tx;ESz=kSK4+S*?J7B$ZwI6RNNgw{FM0XVfV zLUhmeV%nyQX>{HCX@JHdI5iE1g{uQ2W*ymFji(-(rKIzdfS{jZ#Depls8@9!r<1Z{ zw1rhSQ?EErh;R$=QBc=LZWxe5!)J|QDR1_EjYluw$ZdK(wqtWJML%Ovvm_>*sekUDK@ zfz*)&xUuA{>^`}=?NsIBJC#2Ylx`8HmupV{?MTUTr@!2xI7f=d){B?#T1(PBCln>H zizx)|>rpy(XJpgj=xTV^tx3KkX-%zntz?2JVTOXhU{ z1UxMfZ_go^O(*8$$!Op6C}Lx9E&Y;)$HxSz8ThNX_#tT*g~|-5zYscCJ81DTU*Rkb ziw`~c7b0@AZjzjtuz!Psyb40PIov`wpGg8&X!fz8iR4+uiUt^>i2WN45D4AvRiQHiBCKLdZ^ow)1oRW-iC^ z!G;;QT?jGDQ@0ki(|fUN_7w=R>1qhHIqr_MZrPfP{`EkcFnXWh6@2&ox>dpVTi^wl ztqJPf0fwai8jDdo8|*}!?fx2?9!I?~ImW!@VKCX59XA`F(Jc$WnKt|#-F{Alg|$3g zHuyXm&9Ss*S?DmwSvMm*nbs;$k7F*?X*lsgo{dg_uH_1n%)@vmj=zKy`gWSJ8z>GXQ z&rm0vsxRDl_OVU?&ywO#9%4KNn__I#S{Cz4U`KJ{Nt2>y%%7QT11g0 zrlrnro#Ij&`3C)KisZ?92zJA_2wLi!NBx?vYbiLgHT7?OxQ%dO z)OsYoeiZK#gR?K;@tdxi{u{l$MO>%tdLtli*ZPzFh}5I*7~z44pnr=L>z?;T{c6PM zgpX&XTsloh@@h|`dW(9Eh6oHwei9F2P;DJ68}yq8$34%h&=RGVr4oqhP8K8yD`FO= zcG8@s<7oQ460s;#O*q>ezA(TEJHBPpi&V=JayS9y9v%n(iF#bBy7*(Uu(?@@|B_bF zq{I)1M+DB#B>yW2DKSN6&8L;?D6ug{Ej%1oyl=6CL!6))PS<$csU0ykq|}TtDKyn^ zboo7I>@L6d^`WIh4=zU&Z$yG|fH;0V$e61Dpr5936rgBgHL2 zW60Q;VB-o%(A!~1u^B%X(p@-|v7XsxPMpnd3=kc<6XVM2gR<6n+*t?KQ!BKsD^?AL zYr$!Z1Ms?HbuqmDQBr6%MdnJ!6RP>JwuLoO(e4^&jYVQu`&5G%E_;xu2z!Bdmh`wJR~C3KjY zT%g+~UHNC?5piMH$iGH`bY)u$U3nr$(>j%-sZ`^6JUclmtcXfv9LLm1pIlwMaBmjp zM_o8S3VrYCTo_**Q#{*{+o4QL`R3Q3EPe7YOsDOU_gI3(-OVy4_@Dq@YGT#5Rr64^ z_~;7j&74G@}{~!Nhz>YCLvjoq=8j|HFyLgSUjWN4xL!^$BRx@1;ZMtWM>K>L+ zau}pll%o-c9mK^}BJEgMijO`zry%Xckvm$uBNVN2;+6;}2QJBb)iXU~ukCEw>Ni!d z-m7}`^S-az-$Wuo4xYuq0ofntxW8ki`FI{cRtMDj>pC91~wn1ge2*Ez}e zB`5hwoJ8gzirvpkx253|%r*eF!T)KSWVRu&4U^bQSEGj%(?&oWZPTWiHU`>wo3^9k z&LrNTV65+qEuF}92SI!Wl^zOXcEGuDHLfG9Vj|cbb(mn-h$-bUCZ7tswA5) zJkE|1?SWn_Wygy;QO>}cJ%B~dj)=Jjq#$KAn#;mKMA5Q&sYHcbQ7tL5Sjdr~QiUe4 zUlcxgwSEv^a+{Hk>ey!bSoP*+w5xi3GuckljF`*bi;aC8yx0|Q#%Fea zf1zh|Q7>zzuLNP^F$u{9p+t*$SWV3gu(*j%B-2j`ZV!|KIh=>05NSH&r`;G>4~j!5 z`ceD{#Q=(9DDWiEUKDH=N3hh_6fr1h*a%C|ns~goIH@8LA= zoV}AHZsL)g__yq`^U`bSLHZ>3%f?Yw%vA_SkXsBMHz2USfv9!vj7v;tAS)S@R0net;I>mzI2zZ#ito*}=4( zJ9_J4IuFPlD*)rU8sl{{i=8p_Fx{aYJ@qo(&fX6*-9fz{K?`^TQlvGTw%H!F+gmHw zz8ZaOgig{wH)6%xE@A%8<(R!H0|(je*TZt$_CLn_51P#&SR9uUq&x3ePTD(mqM<#q zLwmB4%B0QY_cPG!09X|T&B8x{S^%icF1Y~UsH}>#B0K<)_Q(Z^JT@a@Sue_k93?6# z=nfdL9C(Nw;?2?k9!uwVD#E_=F~2% zFe8Eh=&g_j0^KKAfBYH4oN2L=|O);zfW#A@UO=3qc3Aupp%$4ZRuz zHHrXXh9sips9a*akbFd_O(cR|1hA$dwDMaGM52hvW~f;eMBNODng&EkBycc21ps`F z2G9z4Nh<+65&?J64BKdlUTw9z6*DN81W}S0ss`Dt#5`;tjbK0CvYsmxzc7Qasj!L? zx(BTlWo0)y&@hX$5esQyGu@4?C%}-RK>pHx4Lb+enm%2<`CTA3?^~E%Nw3WsfywH~ zRx~~T^XGH*LmNkjSM^uJM)cF_^{sI7>4n;{XP4?7E0PhuRK2#9>Ulo6-gkcE(1o>* zSCWyMsZMQ0Qcp+Md(Lj8&#m5GpOU_vQvN)ptm}`BNToXVPcN71sio^~BYCEJW2>ic z(Yv7iF0>-84ZqTjp4oMG=T@v^K415&l#SSx>iAZ&yVhMFHIiqmH!SC_#rNv`#=uY4 zhK=Mn^btBQ{UaZe;9Q$x>)JRQK zr@$cD{oGrlFM?o{I9HwAN_0V8dl9NjM&g6&|3_XC$QRB=iTNoP2(9VQ!=M?CPiyx>_CGiuKmUR!*-RF=8LVWa5WwcUCU1 z3>opOP3p$&*qglkus=VXyZ~C-fg31p*=s{Lqb{I5&$7=3qW}Z3AY5cUKr~H|4>4^R zyjzxxv9X2W_}bRYIAtnL>d49IhM%OxRD0 zikz#MT?`>hk zyRJhss6%lPUhTIaFlEo|k+Sj5r;lp6XTPf7UL7{#A6F-~I(lo@>%{07u1;ebb=A+W z9ygL7L(V|W&cS%+{1^3(Rksnp0zU0W-bhUDd)}W+T6Z=KZSUjzoB!+hlHF`-9biO# zc=*=c%V*yXb{=td_adQP$2rUnX#@^#>)vD?+?~1@+QA!;+U`4ZqSq*yabzPl37B+SSs$VLVD zh{_eIWX5t<748&RZ>GzSA+Rxuw9Ybz<%yLs=TUb7#2ZX0dK(2M4gCM0nwZ1$R;p4PHwZTS>ksc+FfRmB*=p=S=1BB^=MA85Z_Dm#XMS2Ux zu9W%(>QIcqt9=6kQ|kC0Db>z`V;JgzHsTZeLJgdMhBfu?awoqOnM)WeY zvkV$G67M&2$1Wd8Hqgt{JGGH#Jn&aE+w7-7J_lu@#XQ|L??$Na$*7f;IJ-=z8Ppys zqLRB$)JhwS;p$tU#6XB`3*K6+`xD!UXv=Y^NL6;jux=!7g8vG@^K(zm4xspQZ4N5i(E-PZqn6(0 e@OOLM5p)noZE%~z-|giCPzhY?s literal 0 HcmV?d00001 diff --git a/Backend/src/models/__pycache__/group_booking.cpython-312.pyc b/Backend/src/models/__pycache__/group_booking.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c54dfbee1623a0b19ad85b28fc27475a4c4fce0d GIT binary patch literal 8673 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 bIP4g0EbIvI+Ihzja9nmYlz$8O<_7&Q80I7t literal 0 HcmV?d00001 diff --git a/Backend/src/models/__pycache__/package.cpython-312.pyc b/Backend/src/models/__pycache__/package.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..066d8e78a37fbf1bf181a33f24bdbb0817c61745 GIT binary patch literal 4221 zcmbtXO>7j&74DhonV$b2d+hNK-Zg*A&Mx2tHcKGD%3y5cUCat>h+5IMnyJEW=X~8em?To# z?l-Sqz4xl>RoC}*{~?u%OK@G?zt8whT9W>Pjn)-uDzE+q$`=xmNWhe=fEuuZYS0R) zAxlJqzA^BP|5x9<@J)bklIEU-ni+b8H3imm zhqbpR7c-maQcUp3`LbEHY_43iT{=e@k6d+`Vb5`8x@u8ol(=%PTsEn0b9si|b-6rs z_3|{AFW6O!$H&Wz8gur?w8o=jx=Uvai*gyYVt^+ZHFei0+s?dEfp(OD-35>7%^(gQ z;PN@$p(TH^z6>-Qul9iQg+wJaKqNIt0&0i^Rhfj;FpG!Za z49|K{^r9$$Sdm_VlzuofGGDgn$eew5WQ;nu-Ew7Qvg}gx2-N8|RBq%POpDrN#9^fo zNKZ{?q~g!?Xr;!wF&_#AZU?h}F1<+g)W=_B`sW7T1xjS2 z5+#OHDpzfHx)@`9IH`UV11N@3>_xE;#eNhxF?JBeAPTXJA=KV(i4-W#FbG(AB3B=S z_0}i+m5UmPW~TrCq#ulN+iK80vJH5Sw$~siQJIEG5dT&~?V}VyJKPG9pEMsaIF!NF zNc*Tn+a5~WBPe{L?JO~oJq~fPC-?-zr6!tjNt$ZMg=y**E_=td8gGx5X+~v9f+SnH zw&l&WJyIPWVUq61(zUBkkYqZ107S4NAoC>Ko*}Lvm>!!x7yzx>5#AvkV1#&yS2gM9+Y}h46;n2C81XeS?668_4YMS~Dld|{0vUvKc8h0vRN)pR$J^cXVCW*Ed@;fCw`j~MR6y%b`A<^mizM-5_Z!Lfh@ zA3&F%EF^RuE!))!0=J(#dIh2oLskMjeFIQUtCTBMfO+chLW>k=Sm?(k(UBq-AaRgg1#1J080sN!EegFuj#Sij~6uH zR-vo}y8=wZ5*U?iS57ksk6VVV*~Z+w>wvD`ZR!j`j>qrlra`ou4B$5E>u#CHVE^Z+ zR%IrO39oqwH~kb@sI=v1rLs*7!JRw~tjlx__CRMzaAz5eiB#Q^UH&DH!!rZLBbx5= zxW7}VGkDFBc&t5Bc+dd+VTKHxwF;El*$K~wFgFUMJ;#CP0pL0RR{heSW0^%|X=W|I zKI6rv>gP5Rxd-nwx*xv39A2C85~u28zNPEod*F3pJ?|yP!PeE+$S=!Y*OB_i8>#LG zV~wHb`9o_5*N46vo&9!n)=OQhkN+bgb@e?`mV!&8U#8dcUhc#C)JD(Xqlu-_rG+mq zL9*js&qRHCV_^7kVL7)v`t{J${Q817aPgU3fZ+T<7I`w+zm6vzbZrzlRM5D@Tl)J}t#F7Dd_#_SySwIP_D$ELu< zczbrlM_hbq1E2%Y5gfx6p|&8~??Bx45j!h-J7Akf;bq)*@%>W6_7RF5@2OaBQS+aH;`BCw zaQ`kqi1_k&VENMXy{Fb^pIy1;<*xf@z3{+oTz+0SyjJzHXW+o6^NZI3deVpM6C2sy zCfLk++2hc+17Nbfi*o?13tr}AeeyN1x?bXNeGG5}p!7uJSYzSg+2t`W`D1_&z%q<* zz07Hd=s?f0_5EJYczqh8dIlROmS>-idik^UOaBjyWd<83*RFf%Gp#QatX8qWyyA+fwPMT{wcwa0P}wV`yUIB literal 0 HcmV?d00001 diff --git a/Backend/src/models/__pycache__/payment.cpython-312.pyc b/Backend/src/models/__pycache__/payment.cpython-312.pyc index fddd5b55226b492a3c146d76e398bde1bf389207..f30adc218207b7ce00757ab93449a5f989562d7a 100644 GIT binary patch delta 469 zcmZ1=woQ!pG%qg~0}vGbQ_7Uy$UBFTQD^c6W?7LG)+qKAwkVDi_9)I2jwr55PED@K z*BQgPZ?Pri7iA_V`e|}cHe_;VDw3F7$`rw|NLZ delta 421 zcmdlcwm^*cG%qg~0}wokP{?H6$UBFTQDd?$v!qB0OB7oQYZQA5TNFnMdlY9ShbHIb z+l=ANewtj9ZJFFBcQS=Cs!qPoq{hwWCOcSVX^{; z6&u)a)yWAQJ(6hFYJp4z5z?DkIVBk(_G(R5;i}RE>j&!uD?-)yi^C>2KczG$)vm~Y i@^LN=A$CTw3C=S_=1b0${K5d_d=Z+=$Zg013KIZ~%}nzE diff --git a/Backend/src/models/__pycache__/rate_plan.cpython-312.pyc b/Backend/src/models/__pycache__/rate_plan.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..42d44f9c9542409b3a173c096e73c12e1849e2cd GIT binary patch literal 4894 zcma)AO>7(25nhtZ|9^=T^=r$2vFX@S?8I^71aU0u&x)<2meLg4BHI<;N?Mz{OTS&Z zmQM_jLytCm2n`=1BNqx55FL8ZQ9U(4fkJ^7i1msf2zn?C^pG~{C8y53C0CTHxLKRS zd9yR`y_tD$zM=k-OvVIwW{w=z3Y~)RcO0~yP?P!LpTK-75P|s0g6@-ix?l3^0V$x1 zlBfrzpdOM!dRPkU5h& za?{WKxCI{*M-)pfxm$LruQ~YP6fmC(sv!A@Ao+<;3J|{}l7JK>q7))QDNI6AgoLFi ziAb@fXfehT*J1AW%Zf3(R8d)AU`B8%3N7x8HLs4Yo=iy@Y3C1A!@ z?5f2=O3Bu4t1O}!O)hGc7FCQDVPW-7MWZU2E{5pab0WLep^RN@WK~EZN^Xd>v;qxNT4|7Y*{7wds zH0{EqJ@f5n<0!YnU9_)vY(KlUd0*ORt>gR7bi&mHS|C~PJhaEU*qjT>XPd_-x%A~- z-kjpH)9TQTkK8=9Q`r9w$h`6*&7ZXI^lrl$us=(3#J8Gnoki(YF6-JQdyUHqq?`1t z_Iju19GCWy?l$Y!m->qXEYhk&EaufmYj!2T;uWgtP}Jp{09!%LAnF~KRI0YA86~P> z;eSrR+wm2EETdX3BWBPckUT#Nq!?hKs-fMgsuYL(#fp#fD}MgP!bF`{s%86m5EZaC z{R2Mme1DPReSeM}$=xJ1OJM31qU$Ec~EgD4RD@COru&s+;rE7TnB4} zxD_8u%>g9H6|+(Wyj3k0e&0o?9W1;K#g;|AFx>4>%H>mofE}u>o(AY!RE(mZz5{Er za7nRB3Q;-kBj8LI&+jSCEGf%2Mx}{xc#|o1EzGYGicT{TUoo)+_(UTph!fQ+QLTdg z07d^@oI-G9QG_lY3AdtS`aywp>wZ z3DO3j%u@5(9hJzfS2T|12`|*LMp#VO4B5~Y7Htc73HLjo?BxH8MOk9gEr#_LX6zQkZt%>%_^O$L3BKnzW)*-JFa-GYveDl!}^ zg(g5i(iY5REl~r>$!&LbeyieyOrs>Z`ZFrEfnGPTVG$E`RekFlbNku*-CcaAFUsFnme&} zZe!@{kq^Hb`Or!Jx;Fl=kkEJNaiNiIT>5-qJ^y9E>Akus7Pfi^Jz;L+meYF$h55dR z6U!qF-=pcZ*^M(!eylbP_PO5r@y8b$=F=l@f0=VSr)yWYvW0rMan#8U*QR#y{fC#& zgDd?PHpL#Wgq@4awv#;tYU%tvvOMdgPu3>kO}?*wzVX5OsFS-?yS7yrs4uJq*Q-up z3|h9Wo`a9W4PWET=gD<(Lvea0H^nXxE{E5SIz8uM026feKOA2^`)Fz{w?6CSM{3hs zeC-c5#+?2sSbeMez{A_ibC2$>jXK?LY>IiX={^9q))wXv;qU;WD3O(#9_@ub`Q z;W663llvodWk0`@<9*5h_HtOt;E`IrT(k|*gR2n_rDA|R*eOIw)I;0$_i=o$CIDA= zY?4iz6p4{|YbNM26RSxN(^_5KUY%5%PR5%jy=$I!oh)A;x(yO-v*g|1o?Ufv&77LQ zfxo!+Jb?BYZ*PScY5t^r)0HgVyq9{H^2IJX0?V?rhfy-GC$khc1X$Vn zg@f=oPS1dlzKY}=l2?%Y0ttdGeFMqsNDyu51tf1Gc?$_IMDxg91TtOh+k?+^Y`cLGY@XUIDNJ@iI=%myFeSt&BXGhsg@K0)%X5uGkBl{Y5^&8 zYYRgNq}Zy^4ty7|Z0fc-M^T?2Jf5OK9C~T{G4huCr-Qd?6<$CE6D(}HzYq9)zHbHN zYyST$!T3fv{;hE2Z$kVV;qbN?@pa#OwJv@ZeHh&qV6dGNK{8W6`dRT|aa(}FcH+G6 avhNS}wt(N;)Yt2~;D%|0MsD> literal 0 HcmV?d00001 diff --git a/Backend/src/models/__pycache__/room.cpython-312.pyc b/Backend/src/models/__pycache__/room.cpython-312.pyc index bbcffa218041fbee143c0e2fea2e363dbc28c58f..b69d4ed678254e2f38255344720d7bf3c5e8849d 100644 GIT binary patch delta 374 zcmX>j^iqWHG%qg~0}yzyDrNp=-^f?Z#5iSh7n3bh3=>1DVv0nHaEjO(@zu;AH4F?< zYG6@GG*NZ1s1%y023S-YO;oc|MpJh4X%>D)*IWES`T4oNiJ5sNsd3jP(bXaklY1%{R3nG%qg~0}wokP{<5s+sIeV#Avg*i^-OWrBXsuaiL~4#bh7(9g2q`OpM~YCzDK}8yk_&vZel-$|<2C*EOBSAh8jVT1(}S%NB9qAFT3HD*bwWX09El~5Cwtjd<6DgyTt z&7_r5QxP9C(^gtd125sYnX$5JRzM<}ML6*YVVPw2V(mO?uA!8YCtN5gEOFB@eamL@ zZQCQOgtEk4j~ezWlNQN_$5Jy6CB~|KhcuW{(LJ(gScJ*bj$;ztW>WR;!hA_!8A?pu zGaTDpGwL8K7+gJYJrQ{tfl1T4OKR-`+BDG-PB#VKH9@L?kt$+AjfsdzB#uS?*^lkP zy!cTQZ9PyKOM7sO9}Ir`iJp(bec+qKa^GWL+2duvQBys8vS$}xu&245WFTh$PBzlz zNdDeyQIK=wzqL5NpX$|Ai0;RM$~b*4YZsBh$YbbS*5OE3jQY#;up5cA|5FQO?V~&j zo?|$N^ZNt6?q7;@g>DV%cw?|M#Im0_j`e{DurK=_VR;Iorgvp`u@qzJIyEevHnerv zgScU1vca;t?>UBDqr@V%x2f=J?xcZdN+MHi-!%1Qlh7fU)(Hku5@?b0|8PFWWK33c z-}IQ`*nXX_OJv?ue3wujrX)rRI>!CQ=qFMziI>Y#{Q_aVa?5&cU8_5FAJ&k#O!^d} z%$F`OMbm8EBAUihnr1oJH#witwEMnpb}U&<^LVXok#bNd-#I24n97`yqmh8bpDN4s zQb(~_zArz}O`lNy)L-<1?sTevl4iIi;d?dPdB`%R?s{6gdCXEBMq6%B{x(?LcAyoS zJw?wR0)VTnTQ$&BEx`!)EcB6b`%>+P8 zRq)7+Z6}&vh3OkXrIj1se&;AR@%1dd)@^XUlxHdTzNwqFHDWdTTM%I$ArkFE8%GXG z2j>2rNrzeyByE`UHOF2tRwp_7%kb|SEC1oB5r j5QN{*(l2P~2rV6>E63=SV>I!XG%8%&s{D=MCkpZ}t_QD} literal 0 HcmV?d00001 diff --git a/Backend/src/models/__pycache__/room_inspection.cpython-312.pyc b/Backend/src/models/__pycache__/room_inspection.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e564bc9f079e20ae97d5267b00d606f72e81eb9 GIT binary patch literal 3227 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 delta 120 zcmX@ceVdE-G%qg~0}wokP{>rC$orpBZ8I;UIwMOZzox+C1f~tlewyNw)tL8B{>>cD zIBjw;%XF4T*CL(CY^(*!+CY6pIv_$9Nc`fk$<0qG%}KQ@G6ZrNfw)+C@=R74elAAa Mj*_noKnkoH0Bt87b^rhX diff --git a/Backend/src/models/__pycache__/security_event.cpython-312.pyc b/Backend/src/models/__pycache__/security_event.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1881fb7f36c1e63796f3c9f9336f5cab8c2ea4d7 GIT binary patch literal 7008 zcmcIpOKcm*8D2_!lgl^t^dphw2mPWQE4G`&X=2N=EhVy3O9`@R%TSz=wDOY6%r0#+ zDNqK;tw3SJKtn-?D5p}efC}ivr@}ooKrbq^MdU4lBIwDt7Evy_wEsU#E-8kx9kgA6 z|IYmL&&)qN^Zk#}pGDCx!1L|?A^G>cg76QlG#+=o@Xe4z5Iz-%KpcvYcSw$Wm(-Q- zmb&v!$(eUauDn}v=RJ}q@0GlHpX76(Y?tEC2c!TmcPqhsPzv(0QxPSRxQJT`<-=0g zA#@3s1mgKbAYK}w;zFb`ee-?aX_ciYm-T_H|EaPuE*k*ZAPwK|u8$DsToJgT7H)!b z!@!NSaFd)H1#YZ`+rznW;3itQy=7-A`4}fj`Aqi(RVn2QrsqmQr*o8G*@D> zUcPvf7WAodk(#2S&dG&LR@Z61s5h!wDVL*~)~L>AWu?TZ=>}o7q?_SlR?}{&jAU~2 z*}@z(LpFm^jp}A7o6D)Cf}T;-+zm?1NTXgV)H%^2WqDcC;3H)SEyy4g*GgJZ&dF*? z%Vcx9d=nHk1MumxQ03}Gc_ptBc~;Kx4{VB@MW_PbS1p+SY*B7V`{iP0raoPq!46~; zIWOxOdOJrcfe8a@wxrL4j_?IcpQfo+aWa)Oy_rlQo2QwK>Ca?f!jgjJU?%fHDXUm5 zECw6J;z$xml1O@x044?2hom3L0Fpr@yO8WgvIogtB+nsv9?1}reLz-(Z-5Hlji%?- zJWbCPZl_17c0*T->C384m1A(yZott@pU=YAEs(Uva%oVKDq0#&72n|u#VtNwEVC#M zi^KyzEeqrmVKcF>@@5MWla;Z}o`aREo5}r^NsIKR)}4vXz9Z|-c5Yc3m&xR zT(>ak+zvLp-X7@KAFO%3y{Y+j4ZyO)mrH_H%~`>M1Fv@*udf5IpYsNYo48j+`)dty z?yhaUcXOT#=(c@;IlJpC-NPle>#-Mk@B<^;adpo0SX&{Nv?VLRPLhZn{z zEn6w9x@$FJ@AUzYJxHR|JLg!9+52~>&fT+(o8k^a+=e32%XKA z6rFP)BSIf<0Tfmoy!kQ4H;)m^D_xwyUP1;zg0>b$L59_Y4%m6-lU9VSFm0{)I_V9<)_&6p24 z@Tqk0zc4DqAuO9ghsOe31DH%zZc@U~w3~rO6})*%Wu|OKcuNV#%oM1$OHY05hHZ1a_LdDchR}N z`}js*`oaE(x)FV&GX5x>yi3*xPi*v@d~k7n;{7it=KeA}t zTJo(Uz^L*5YNF;eV#g{IsOr@6s1ZtW4Xe&&(Clbs9Hb&WcjjuYwUQA&SGoKs)>rMT zjT*6&&;H zMBnBr1^cS*+Fb2jBX9->8LW=29a-CLgn!)dWw<+?ec3Q<*R%UFuqUk+lzY5Yw{3+Q zzRW{BEf&vf+an*hXnx2px>kL*?dATBqIC|ErVs2|_1Y35?*|#d!gH{YwuiV@1GXgh zSab);9s39E6`)6p6OoEwF+;ZAiE=m}}_AcHlqtlqz|`5cnhkemSm=FT>3&=IVdII#&N*N||-;gjnTHnnV?4epsTHzgeU>^+qKDG;z{T!AL|J=_|o-$VWT*aZnr zt$hjvQ9slH^&FZAwdn^V4S)lHdzQPG25Y(z8v&dEsBYDg51dBiT;&RYv+DVLpfM(edq^%YVB1$E)i<&lyp|@c;-oy*98GG(zV-zGOvnsA*>e9Q~go zfcoHkd+gKW=chNoPkr-D2RT>VCdnflcwJfWQf=LzlHSv;Y=kruV;ht8&zv-BI%VH{IidSEUYm945y(tq2$->sHMzLPk zEc88o0kjxP)x&8#5A1a$-2c3SrPq+yNSsXdJuCi9Cq~m|kjPq5$(H%WiRsHJGK9Yn zNSk842+0J4yP+svx~NS4kYC_rN=z|*x(fFgycoo=QZCG@yyn(&YLRM9JewhKU3gOi z5j=$PR^;0e#%7%u2qwi(7KKWjU_5sEC-LdMadlR)` zLrj0qJ|qXLmumFEw2{2PW7KE!c^{8krVH2H8UDZdLGb4us4dQGkMim=%{UJs-Y^jj z698A1b{yDh0yxAW+H2)mF4*_M0p)hQw`Oop zjbkyd+X(Ena=NP<;iHXw4wsthGyU2JN><6u(|oxl7r^b|Hw;#Q&o>dT7p!Xzo)v_e z9gnLlZv{g5?M)}ee{h6zYGGEMJB}9+Gw|;V7RI)2O=tX?#JU>cdH?%3FvcC+;_^Lc z+nn#buUNP0ud-i*99SYjL;e5R?Ql5$E@ZwGGJh2^UkL}l5_bPh2z(_BZ8>9(UDfXD z)MxK5zPlwrW$TdBF;Km*ocL}3=lxp(RJMY{j@KM_Mz;ifZ@u9NI$pgqyd}VEYnN!1 a;kC6V?4Vo>-&;db$FVz!Zv}X9lldQ`h?tQ8 literal 0 HcmV?d00001 diff --git a/Backend/src/models/__pycache__/workflow.cpython-312.pyc b/Backend/src/models/__pycache__/workflow.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93bd0ee91a5ff177463d1181aed4f4c576a6779d GIT binary patch literal 7116 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 literal 0 HcmV?d00001 diff --git a/Backend/src/models/booking.py b/Backend/src/models/booking.py index e36d1127..ba60fa6d 100644 --- a/Backend/src/models/booking.py +++ b/Backend/src/models/booking.py @@ -35,4 +35,8 @@ class Booking(Base): payments = relationship('Payment', back_populates='booking', cascade='all, delete-orphan') invoices = relationship('Invoice', back_populates='booking', cascade='all, delete-orphan') service_usages = relationship('ServiceUsage', back_populates='booking', cascade='all, delete-orphan') - checkin_checkout = relationship('CheckInCheckOut', back_populates='booking', uselist=False) \ No newline at end of file + checkin_checkout = relationship('CheckInCheckOut', back_populates='booking', uselist=False) + group_booking_id = Column(Integer, ForeignKey('group_bookings.id'), nullable=True) + group_booking = relationship('GroupBooking', back_populates='individual_bookings') + rate_plan_id = Column(Integer, ForeignKey('rate_plans.id'), nullable=True) + rate_plan = relationship('RatePlan', back_populates='bookings') \ No newline at end of file diff --git a/Backend/src/models/email_campaign.py b/Backend/src/models/email_campaign.py new file mode 100644 index 00000000..f0367ea6 --- /dev/null +++ b/Backend/src/models/email_campaign.py @@ -0,0 +1,285 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON, Enum, Boolean, Numeric +from sqlalchemy.orm import relationship +from datetime import datetime +import enum +from ..config.database import Base + +class CampaignStatus(str, enum.Enum): + draft = 'draft' + scheduled = 'scheduled' + sending = 'sending' + sent = 'sent' + paused = 'paused' + cancelled = 'cancelled' + +class CampaignType(str, enum.Enum): + newsletter = 'newsletter' + promotional = 'promotional' + transactional = 'transactional' + abandoned_booking = 'abandoned_booking' + welcome = 'welcome' + drip = 'drip' + custom = 'custom' + +class EmailStatus(str, enum.Enum): + pending = 'pending' + sent = 'sent' + delivered = 'delivered' + opened = 'opened' + clicked = 'clicked' + bounced = 'bounced' + failed = 'failed' + unsubscribed = 'unsubscribed' + +class Campaign(Base): + __tablename__ = 'email_campaigns' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + name = Column(String(200), nullable=False, index=True) + subject = Column(String(500), nullable=False) + campaign_type = Column(Enum(CampaignType), nullable=False, default=CampaignType.newsletter) + status = Column(Enum(CampaignStatus), nullable=False, default=CampaignStatus.draft, index=True) + + # Content + html_content = Column(Text, nullable=True) + text_content = Column(Text, nullable=True) + template_id = Column(Integer, ForeignKey('email_templates.id'), nullable=True) + + # Scheduling + scheduled_at = Column(DateTime, nullable=True, index=True) + sent_at = Column(DateTime, nullable=True) + + # Segmentation + segment_id = Column(Integer, ForeignKey('campaign_segments.id'), nullable=True) + segment_criteria = Column(JSON, nullable=True) # Store segment criteria as JSON + + # A/B Testing + is_ab_test = Column(Boolean, nullable=False, default=False) + ab_test_variant_a_id = Column(Integer, ForeignKey('email_campaigns.id'), nullable=True) + ab_test_variant_b_id = Column(Integer, ForeignKey('email_campaigns.id'), nullable=True) + ab_test_split_percentage = Column(Integer, nullable=True, default=50) # Percentage for variant A + ab_test_winner = Column(String(1), nullable=True) # 'A' or 'B' + + # Drip Campaign + is_drip = Column(Boolean, nullable=False, default=False) + drip_sequence_id = Column(Integer, ForeignKey('drip_sequences.id'), nullable=True) + drip_delay_days = Column(Integer, nullable=True) # Days to wait before sending + + # Analytics + total_recipients = Column(Integer, nullable=False, default=0) + total_sent = Column(Integer, nullable=False, default=0) + total_delivered = Column(Integer, nullable=False, default=0) + total_opened = Column(Integer, nullable=False, default=0) + total_clicked = Column(Integer, nullable=False, default=0) + total_bounced = Column(Integer, nullable=False, default=0) + total_unsubscribed = Column(Integer, nullable=False, default=0) + + # Metrics (calculated) + open_rate = Column(Numeric(5, 2), nullable=True) # Percentage + click_rate = Column(Numeric(5, 2), nullable=True) # Percentage + bounce_rate = Column(Numeric(5, 2), nullable=True) # Percentage + + # Settings + from_name = Column(String(200), nullable=True) + from_email = Column(String(255), nullable=True) + reply_to_email = Column(String(255), nullable=True) + track_opens = Column(Boolean, nullable=False, default=True) + track_clicks = Column(Boolean, nullable=False, default=True) + + # Metadata + created_by = Column(Integer, ForeignKey('users.id'), nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + template = relationship('EmailTemplate', foreign_keys=[template_id]) + segment = relationship('CampaignSegment', foreign_keys=[segment_id]) + variant_a = relationship('Campaign', foreign_keys=[ab_test_variant_a_id], remote_side=[id]) + variant_b = relationship('Campaign', foreign_keys=[ab_test_variant_b_id], remote_side=[id]) + creator = relationship('User', foreign_keys=[created_by]) + emails = relationship('CampaignEmail', back_populates='campaign', cascade='all, delete-orphan') + drip_sequence = relationship('DripSequence', foreign_keys=[drip_sequence_id]) + +class CampaignSegment(Base): + __tablename__ = 'campaign_segments' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + name = Column(String(200), nullable=False, index=True) + description = Column(Text, nullable=True) + + # Segment criteria (stored as JSON for flexibility) + criteria = Column(JSON, nullable=False) # e.g., {"role": "customer", "last_booking_days": 30} + + # Estimated count + estimated_count = Column(Integer, nullable=True) + last_calculated_at = Column(DateTime, nullable=True) + + is_active = Column(Boolean, nullable=False, default=True) + created_by = Column(Integer, ForeignKey('users.id'), nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + creator = relationship('User', foreign_keys=[created_by]) + +class EmailTemplate(Base): + __tablename__ = 'email_templates' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + name = Column(String(200), nullable=False, index=True) + subject = Column(String(500), nullable=False) + html_content = Column(Text, nullable=False) + text_content = Column(Text, nullable=True) + + # Template variables (e.g., {{name}}, {{booking_number}}) + variables = Column(JSON, nullable=True) # List of available variables + + # Category + category = Column(String(100), nullable=True, index=True) # 'newsletter', 'transactional', etc. + + is_active = Column(Boolean, nullable=False, default=True) + is_system = Column(Boolean, nullable=False, default=False) # System templates can't be deleted + + created_by = Column(Integer, ForeignKey('users.id'), nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + creator = relationship('User', foreign_keys=[created_by]) + +class CampaignEmail(Base): + __tablename__ = 'campaign_emails' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + campaign_id = Column(Integer, ForeignKey('email_campaigns.id'), nullable=False, index=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=True, index=True) + email = Column(String(255), nullable=False, index=True) + + # Status tracking + status = Column(Enum(EmailStatus), nullable=False, default=EmailStatus.pending, index=True) + sent_at = Column(DateTime, nullable=True) + delivered_at = Column(DateTime, nullable=True) + opened_at = Column(DateTime, nullable=True) + clicked_at = Column(DateTime, nullable=True) + bounced_at = Column(DateTime, nullable=True) + unsubscribed_at = Column(DateTime, nullable=True) + + # Tracking + open_count = Column(Integer, nullable=False, default=0) + click_count = Column(Integer, nullable=False, default=0) + last_opened_at = Column(DateTime, nullable=True) + last_clicked_at = Column(DateTime, nullable=True) + + # A/B Test tracking + ab_test_variant = Column(String(1), nullable=True) # 'A' or 'B' + + # Error tracking + error_message = Column(Text, nullable=True) + + created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True) + + # Relationships + campaign = relationship('Campaign', back_populates='emails') + user = relationship('User') + +class EmailClick(Base): + __tablename__ = 'email_clicks' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + campaign_email_id = Column(Integer, ForeignKey('campaign_emails.id'), nullable=False, index=True) + url = Column(String(1000), nullable=False) + clicked_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True) + ip_address = Column(String(45), nullable=True) + user_agent = Column(String(500), nullable=True) + + # Relationships + campaign_email = relationship('CampaignEmail') + +class DripSequence(Base): + __tablename__ = 'drip_sequences' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + name = Column(String(200), nullable=False, index=True) + description = Column(Text, nullable=True) + trigger_event = Column(String(100), nullable=True) # 'booking_created', 'checkout_abandoned', etc. + + is_active = Column(Boolean, nullable=False, default=True) + created_by = Column(Integer, ForeignKey('users.id'), nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + creator = relationship('User', foreign_keys=[created_by]) + steps = relationship('DripSequenceStep', back_populates='sequence', cascade='all, delete-orphan', order_by='DripSequenceStep.step_order') + campaigns = relationship('Campaign', foreign_keys=[Campaign.drip_sequence_id], overlaps="drip_sequence") + +class DripSequenceStep(Base): + __tablename__ = 'drip_sequence_steps' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + sequence_id = Column(Integer, ForeignKey('drip_sequences.id'), nullable=False, index=True) + step_order = Column(Integer, nullable=False) # Order in the sequence + + # Email content + subject = Column(String(500), nullable=False) + html_content = Column(Text, nullable=False) + text_content = Column(Text, nullable=True) + template_id = Column(Integer, ForeignKey('email_templates.id'), nullable=True) + + # Timing + delay_days = Column(Integer, nullable=False, default=0) # Days to wait after previous step + delay_hours = Column(Integer, nullable=False, default=0) # Additional hours + + # Conditions (optional - skip this step if conditions not met) + conditions = Column(JSON, nullable=True) + + is_active = Column(Boolean, nullable=False, default=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + sequence = relationship('DripSequence', back_populates='steps') + template = relationship('EmailTemplate', foreign_keys=[template_id]) + +class DripSequenceEnrollment(Base): + __tablename__ = 'drip_sequence_enrollments' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + sequence_id = Column(Integer, ForeignKey('drip_sequences.id'), nullable=False, index=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=False, index=True) + + # Progress tracking + current_step = Column(Integer, nullable=False, default=0) + next_send_at = Column(DateTime, nullable=True, index=True) + completed = Column(Boolean, nullable=False, default=False) + completed_at = Column(DateTime, nullable=True) + + # Trigger context + trigger_data = Column(JSON, nullable=True) # Store context about what triggered enrollment + + enrolled_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + sequence = relationship('DripSequence') + user = relationship('User') + +class Unsubscribe(Base): + __tablename__ = 'email_unsubscribes' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + email = Column(String(255), nullable=False, index=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=True, index=True) + campaign_id = Column(Integer, ForeignKey('email_campaigns.id'), nullable=True) + + # Unsubscribe type + unsubscribe_all = Column(Boolean, nullable=False, default=False) # True = all emails, False = specific campaign + unsubscribe_type = Column(String(50), nullable=True) # 'newsletter', 'promotional', etc. + + reason = Column(Text, nullable=True) + unsubscribed_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True) + + # Relationships + user = relationship('User') + campaign = relationship('Campaign') + diff --git a/Backend/src/models/gdpr_compliance.py b/Backend/src/models/gdpr_compliance.py new file mode 100644 index 00000000..ccac1314 --- /dev/null +++ b/Backend/src/models/gdpr_compliance.py @@ -0,0 +1,87 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON, Enum, Boolean +from sqlalchemy.orm import relationship +from datetime import datetime +import enum +from ..config.database import Base + +class DataSubjectRequestType(str, enum.Enum): + access = 'access' # Right to access + rectification = 'rectification' # Right to rectification + erasure = 'erasure' # Right to erasure (right to be forgotten) + portability = 'portability' # Right to data portability + restriction = 'restriction' # Right to restriction of processing + objection = 'objection' # Right to object + +class DataSubjectRequestStatus(str, enum.Enum): + pending = 'pending' + in_progress = 'in_progress' + completed = 'completed' + rejected = 'rejected' + cancelled = 'cancelled' + +class DataSubjectRequest(Base): + __tablename__ = 'data_subject_requests' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=True, index=True) + email = Column(String(255), nullable=False, index=True) + request_type = Column(Enum(DataSubjectRequestType), nullable=False, index=True) + status = Column(Enum(DataSubjectRequestStatus), nullable=False, default=DataSubjectRequestStatus.pending, index=True) + + # Request details + description = Column(Text, nullable=True) + verification_token = Column(String(100), nullable=True, unique=True, index=True) + verified = Column(Boolean, nullable=False, default=False) + verified_at = Column(DateTime, nullable=True) + + # Processing + assigned_to = Column(Integer, ForeignKey('users.id'), nullable=True) + notes = Column(Text, nullable=True) + response_data = Column(JSON, nullable=True) # For access requests, store the data + + # Completion + completed_at = Column(DateTime, nullable=True) + completed_by = Column(Integer, ForeignKey('users.id'), nullable=True) + + # Metadata + ip_address = Column(String(45), nullable=True) + user_agent = Column(String(500), nullable=True) + + created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + user = Column(Integer, ForeignKey('users.id'), nullable=True) + assignee = relationship('User', foreign_keys=[assigned_to]) + completer = relationship('User', foreign_keys=[completed_by]) + +class DataRetentionPolicy(Base): + __tablename__ = 'data_retention_policies' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + data_type = Column(String(100), nullable=False, unique=True) # e.g., 'user_data', 'booking_data', 'payment_data' + retention_days = Column(Integer, nullable=False) # Days to retain data + auto_delete = Column(Boolean, nullable=False, default=False) + description = Column(Text, nullable=True) + is_active = Column(Boolean, nullable=False, default=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + +class ConsentRecord(Base): + __tablename__ = 'consent_records' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=False, index=True) + consent_type = Column(String(100), nullable=False, index=True) # 'marketing', 'analytics', 'cookies', etc. + granted = Column(Boolean, nullable=False, default=False) + granted_at = Column(DateTime, nullable=True) + revoked_at = Column(DateTime, nullable=True) + ip_address = Column(String(45), nullable=True) + user_agent = Column(String(500), nullable=True) + version = Column(String(50), nullable=True) # Policy version when consent was given + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + user = relationship('User') + diff --git a/Backend/src/models/group_booking.py b/Backend/src/models/group_booking.py new file mode 100644 index 00000000..fd8fa6e0 --- /dev/null +++ b/Backend/src/models/group_booking.py @@ -0,0 +1,183 @@ +from sqlalchemy import Column, Integer, String, DateTime, Numeric, Boolean, Text, Enum, ForeignKey, JSON +from sqlalchemy.orm import relationship +from datetime import datetime +import enum +from ..config.database import Base + +class GroupBookingStatus(str, enum.Enum): + draft = 'draft' + pending = 'pending' + confirmed = 'confirmed' + partially_confirmed = 'partially_confirmed' + checked_in = 'checked_in' + checked_out = 'checked_out' + cancelled = 'cancelled' + +class PaymentOption(str, enum.Enum): + coordinator_pays_all = 'coordinator_pays_all' + individual_payments = 'individual_payments' + split_payment = 'split_payment' + +class GroupBooking(Base): + __tablename__ = 'group_bookings' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + group_booking_number = Column(String(50), unique=True, nullable=False, index=True) + + # Coordinator information + coordinator_id = Column(Integer, ForeignKey('users.id'), nullable=False) + coordinator_name = Column(String(100), nullable=False) + coordinator_email = Column(String(100), nullable=False) + coordinator_phone = Column(String(20), nullable=True) + + # Group details + group_name = Column(String(200), nullable=True) + group_type = Column(String(50), nullable=True) # corporate, wedding, conference, etc. + total_rooms = Column(Integer, nullable=False, default=0) + total_guests = Column(Integer, nullable=False, default=0) + + # Dates + check_in_date = Column(DateTime, nullable=False) + check_out_date = Column(DateTime, nullable=False) + + # Pricing + base_rate_per_room = Column(Numeric(10, 2), nullable=False) + group_discount_percentage = Column(Numeric(5, 2), nullable=True, default=0) + group_discount_amount = Column(Numeric(10, 2), nullable=True, default=0) + original_total_price = Column(Numeric(10, 2), nullable=False) + discount_amount = Column(Numeric(10, 2), nullable=True, default=0) + total_price = Column(Numeric(10, 2), nullable=False) + + # Payment + payment_option = Column(Enum(PaymentOption), nullable=False, default=PaymentOption.coordinator_pays_all) + deposit_required = Column(Boolean, nullable=False, default=False) + deposit_percentage = Column(Integer, nullable=True) + deposit_amount = Column(Numeric(10, 2), nullable=True) + amount_paid = Column(Numeric(10, 2), nullable=False, default=0) + balance_due = Column(Numeric(10, 2), nullable=False) + + # Status and policies + status = Column(Enum(GroupBookingStatus), nullable=False, default=GroupBookingStatus.draft) + cancellation_policy = Column(Text, nullable=True) + cancellation_deadline = Column(DateTime, nullable=True) + cancellation_penalty_percentage = Column(Numeric(5, 2), nullable=True, default=0) + + # Additional information + special_requests = Column(Text, nullable=True) + notes = Column(Text, nullable=True) + contract_terms = Column(Text, nullable=True) + + # Metadata + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + confirmed_at = Column(DateTime, nullable=True) + cancelled_at = Column(DateTime, nullable=True) + + # Relationships + coordinator = relationship('User', foreign_keys=[coordinator_id]) + room_blocks = relationship('GroupRoomBlock', back_populates='group_booking', cascade='all, delete-orphan') + members = relationship('GroupBookingMember', back_populates='group_booking', cascade='all, delete-orphan') + individual_bookings = relationship('Booking', back_populates='group_booking', cascade='all, delete-orphan') + payments = relationship('GroupPayment', back_populates='group_booking', cascade='all, delete-orphan') + +class GroupRoomBlock(Base): + __tablename__ = 'group_room_blocks' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + group_booking_id = Column(Integer, ForeignKey('group_bookings.id'), nullable=False) + room_type_id = Column(Integer, ForeignKey('room_types.id'), nullable=False) + + # Blocking details + rooms_blocked = Column(Integer, nullable=False, default=0) + rooms_confirmed = Column(Integer, nullable=False, default=0) + rooms_available = Column(Integer, nullable=False, default=0) + + # Pricing + rate_per_room = Column(Numeric(10, 2), nullable=False) + total_block_price = Column(Numeric(10, 2), nullable=False) + + # Status + is_active = Column(Boolean, nullable=False, default=True) + block_released_at = Column(DateTime, nullable=True) + + # Metadata + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + group_booking = relationship('GroupBooking', back_populates='room_blocks') + room_type = relationship('RoomType') + +class GroupBookingMember(Base): + __tablename__ = 'group_booking_members' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + group_booking_id = Column(Integer, ForeignKey('group_bookings.id'), nullable=False) + + # Guest information + full_name = Column(String(100), nullable=False) + email = Column(String(100), nullable=True) + phone = Column(String(20), nullable=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=True) # If member is a registered user + + # Room assignment + room_block_id = Column(Integer, ForeignKey('group_room_blocks.id'), nullable=True) + assigned_room_id = Column(Integer, ForeignKey('rooms.id'), nullable=True) + individual_booking_id = Column(Integer, ForeignKey('bookings.id'), nullable=True) + + # Guest preferences + special_requests = Column(Text, nullable=True) + preferences = Column(JSON, nullable=True) # Store preferences as JSON + + # Payment (if individual payment option) + individual_amount = Column(Numeric(10, 2), nullable=True) + individual_paid = Column(Numeric(10, 2), nullable=True, default=0) + individual_balance = Column(Numeric(10, 2), nullable=True, default=0) + + # Status + is_checked_in = Column(Boolean, nullable=False, default=False) + checked_in_at = Column(DateTime, nullable=True) + is_checked_out = Column(Boolean, nullable=False, default=False) + checked_out_at = Column(DateTime, nullable=True) + + # Metadata + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + group_booking = relationship('GroupBooking', back_populates='members') + user = relationship('User', foreign_keys=[user_id]) + room_block = relationship('GroupRoomBlock') + assigned_room = relationship('Room', foreign_keys=[assigned_room_id]) + individual_booking = relationship('Booking', foreign_keys=[individual_booking_id]) + +class GroupPayment(Base): + __tablename__ = 'group_payments' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + group_booking_id = Column(Integer, ForeignKey('group_bookings.id'), nullable=False) + + # Payment details + amount = Column(Numeric(10, 2), nullable=False) + payment_method = Column(String(50), nullable=False) # Using same PaymentMethod enum values + payment_type = Column(String(50), nullable=False, default='deposit') # deposit, full, remaining + payment_status = Column(String(50), nullable=False, default='pending') # pending, completed, failed, refunded + + # Transaction details + transaction_id = Column(String(100), nullable=True) + payment_date = Column(DateTime, nullable=True) + notes = Column(Text, nullable=True) + + # Payer information (if individual payment) + paid_by_member_id = Column(Integer, ForeignKey('group_booking_members.id'), nullable=True) + paid_by_user_id = Column(Integer, ForeignKey('users.id'), nullable=True) + + # Metadata + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + group_booking = relationship('GroupBooking', back_populates='payments') + paid_by_member = relationship('GroupBookingMember', foreign_keys=[paid_by_member_id]) + paid_by_user = relationship('User', foreign_keys=[paid_by_user_id]) + diff --git a/Backend/src/models/housekeeping_task.py b/Backend/src/models/housekeeping_task.py new file mode 100644 index 00000000..5b823e25 --- /dev/null +++ b/Backend/src/models/housekeeping_task.py @@ -0,0 +1,64 @@ +from sqlalchemy import Column, Integer, String, Text, Enum, ForeignKey, DateTime, Boolean, JSON +from sqlalchemy.orm import relationship +from datetime import datetime +import enum +from ..config.database import Base + +class HousekeepingStatus(str, enum.Enum): + pending = 'pending' + in_progress = 'in_progress' + completed = 'completed' + skipped = 'skipped' + cancelled = 'cancelled' + +class HousekeepingType(str, enum.Enum): + checkout = 'checkout' # Deep cleaning after checkout + stayover = 'stayover' # Daily cleaning for occupied rooms + vacant = 'vacant' # Cleaning for vacant rooms + inspection = 'inspection' # Pre-check-in inspection + turndown = 'turndown' # Evening turndown service + +class HousekeepingTask(Base): + __tablename__ = 'housekeeping_tasks' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + room_id = Column(Integer, ForeignKey('rooms.id'), nullable=False, index=True) + booking_id = Column(Integer, ForeignKey('bookings.id'), nullable=True, index=True) + + task_type = Column(Enum(HousekeepingType), nullable=False) + status = Column(Enum(HousekeepingStatus), nullable=False, default=HousekeepingStatus.pending) + + # Scheduling + scheduled_time = Column(DateTime, nullable=False, index=True) + started_at = Column(DateTime, nullable=True) + completed_at = Column(DateTime, nullable=True) + + # Assignment + assigned_to = Column(Integer, ForeignKey('users.id'), nullable=True) + created_by = Column(Integer, ForeignKey('users.id'), nullable=True) + + # Task details + checklist_items = Column(JSON, nullable=True) # Array of {item: string, completed: bool, notes: string} + notes = Column(Text, nullable=True) + issues_found = Column(Text, nullable=True) + + # Quality control + inspected_by = Column(Integer, ForeignKey('users.id'), nullable=True) + inspected_at = Column(DateTime, nullable=True) + inspection_notes = Column(Text, nullable=True) + quality_score = Column(Integer, nullable=True) # 1-5 rating + + # Duration tracking + estimated_duration_minutes = Column(Integer, nullable=True) + actual_duration_minutes = Column(Integer, nullable=True) + + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + room = relationship('Room', back_populates='housekeeping_tasks') + booking = relationship('Booking') + assigned_staff = relationship('User', foreign_keys=[assigned_to]) + creator = relationship('User', foreign_keys=[created_by]) + inspector = relationship('User', foreign_keys=[inspected_by]) + diff --git a/Backend/src/models/notification.py b/Backend/src/models/notification.py new file mode 100644 index 00000000..81c5854d --- /dev/null +++ b/Backend/src/models/notification.py @@ -0,0 +1,125 @@ +from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, Enum, ForeignKey, JSON +from sqlalchemy.orm import relationship +from datetime import datetime +import enum +from ..config.database import Base + +class NotificationChannel(str, enum.Enum): + email = 'email' + sms = 'sms' + push = 'push' + whatsapp = 'whatsapp' + in_app = 'in_app' + +class NotificationStatus(str, enum.Enum): + pending = 'pending' + sent = 'sent' + delivered = 'delivered' + failed = 'failed' + read = 'read' + +class NotificationType(str, enum.Enum): + booking_confirmation = 'booking_confirmation' + payment_receipt = 'payment_receipt' + pre_arrival_reminder = 'pre_arrival_reminder' + check_in_reminder = 'check_in_reminder' + check_out_reminder = 'check_out_reminder' + marketing_campaign = 'marketing_campaign' + loyalty_update = 'loyalty_update' + system_alert = 'system_alert' + custom = 'custom' + +class Notification(Base): + __tablename__ = 'notifications' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=True) # Nullable for system-wide notifications + notification_type = Column(Enum(NotificationType), nullable=False) + channel = Column(Enum(NotificationChannel), nullable=False) + subject = Column(String(255), nullable=True) # For email/push + content = Column(Text, nullable=False) + template_id = Column(Integer, ForeignKey('notification_templates.id'), nullable=True) + status = Column(Enum(NotificationStatus), nullable=False, default=NotificationStatus.pending) + priority = Column(String(20), nullable=False, default='normal') # low, normal, high, urgent + scheduled_at = Column(DateTime, nullable=True) # For scheduled notifications + sent_at = Column(DateTime, nullable=True) + delivered_at = Column(DateTime, nullable=True) + read_at = Column(DateTime, nullable=True) + error_message = Column(Text, nullable=True) + external_id = Column(String(255), nullable=True) # ID from external service (e.g., Twilio, SendGrid) + meta_data = Column(JSON, nullable=True) # Additional data (recipient info, attachments, etc.) + booking_id = Column(Integer, ForeignKey('bookings.id'), nullable=True) + payment_id = Column(Integer, ForeignKey('payments.id'), nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + user = relationship('User') + template = relationship('NotificationTemplate') + booking = relationship('Booking') + payment = relationship('Payment') + delivery_logs = relationship('NotificationDeliveryLog', back_populates='notification', cascade='all, delete-orphan') + +class NotificationTemplate(Base): + __tablename__ = 'notification_templates' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + name = Column(String(255), nullable=False) + notification_type = Column(Enum(NotificationType), nullable=False) + channel = Column(Enum(NotificationChannel), nullable=False) + subject = Column(String(255), nullable=True) + content = Column(Text, nullable=False) + variables = Column(JSON, nullable=True) # Available template variables + is_active = Column(Boolean, nullable=False, default=True) + created_by = Column(Integer, ForeignKey('users.id'), nullable=False) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + creator = relationship('User', foreign_keys=[created_by]) + notifications = relationship('Notification', back_populates='template') + +class NotificationPreference(Base): + __tablename__ = 'notification_preferences' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=False, unique=True) + email_enabled = Column(Boolean, nullable=False, default=True) + sms_enabled = Column(Boolean, nullable=False, default=True) + push_enabled = Column(Boolean, nullable=False, default=True) + whatsapp_enabled = Column(Boolean, nullable=False, default=False) + in_app_enabled = Column(Boolean, nullable=False, default=True) + # Per-type preferences + booking_confirmation_email = Column(Boolean, nullable=False, default=True) + booking_confirmation_sms = Column(Boolean, nullable=False, default=False) + payment_receipt_email = Column(Boolean, nullable=False, default=True) + payment_receipt_sms = Column(Boolean, nullable=False, default=False) + pre_arrival_reminder_email = Column(Boolean, nullable=False, default=True) + pre_arrival_reminder_sms = Column(Boolean, nullable=False, default=True) + check_in_reminder_email = Column(Boolean, nullable=False, default=True) + check_in_reminder_sms = Column(Boolean, nullable=False, default=True) + check_out_reminder_email = Column(Boolean, nullable=False, default=True) + check_out_reminder_sms = Column(Boolean, nullable=False, default=True) + marketing_campaign_email = Column(Boolean, nullable=False, default=True) + marketing_campaign_sms = Column(Boolean, nullable=False, default=False) + loyalty_update_email = Column(Boolean, nullable=False, default=True) + loyalty_update_sms = Column(Boolean, nullable=False, default=False) + system_alert_email = Column(Boolean, nullable=False, default=True) + system_alert_push = Column(Boolean, nullable=False, default=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + user = relationship('User') + +class NotificationDeliveryLog(Base): + __tablename__ = 'notification_delivery_logs' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + notification_id = Column(Integer, ForeignKey('notifications.id'), nullable=False) + channel = Column(Enum(NotificationChannel), nullable=False) + status = Column(Enum(NotificationStatus), nullable=False) + external_id = Column(String(255), nullable=True) + error_message = Column(Text, nullable=True) + response_data = Column(JSON, nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + + notification = relationship('Notification', back_populates='delivery_logs') + diff --git a/Backend/src/models/package.py b/Backend/src/models/package.py new file mode 100644 index 00000000..ebe7a638 --- /dev/null +++ b/Backend/src/models/package.py @@ -0,0 +1,90 @@ +from sqlalchemy import Column, Integer, String, Numeric, Boolean, Text, JSON, Enum, ForeignKey, DateTime, Date +from sqlalchemy.orm import relationship +from datetime import datetime +import enum +from ..config.database import Base + +class PackageStatus(str, enum.Enum): + active = 'active' + inactive = 'inactive' + scheduled = 'scheduled' + expired = 'expired' + +class PackageItemType(str, enum.Enum): + room = 'room' + service = 'service' + breakfast = 'breakfast' + activity = 'activity' + amenity = 'amenity' + discount = 'discount' + +class Package(Base): + __tablename__ = 'packages' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + name = Column(String(100), nullable=False, index=True) + code = Column(String(50), unique=True, nullable=False, index=True) + description = Column(Text, nullable=True) + status = Column(Enum(PackageStatus), nullable=False, default=PackageStatus.active) + + # Pricing + base_price = Column(Numeric(10, 2), nullable=True) # Fixed package price (if set, overrides item prices) + price_modifier = Column(Numeric(5, 2), nullable=False, default=1.0) # Multiplier for total package price + discount_percentage = Column(Numeric(5, 2), nullable=True, default=0) + + # Applicability + room_type_id = Column(Integer, ForeignKey('room_types.id'), nullable=True) # None = all room types + min_nights = Column(Integer, nullable=True) + max_nights = Column(Integer, nullable=True) + + # Date range + valid_from = Column(Date, nullable=True) + valid_to = Column(Date, nullable=True) + + # Package details + image_url = Column(String(500), nullable=True) + highlights = Column(JSON, nullable=True) # Array of highlight strings + terms_conditions = Column(Text, nullable=True) + + # Additional data (metadata is reserved by SQLAlchemy) + extra_data = Column(JSON, nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + room_type = relationship('RoomType', back_populates='packages') + items = relationship('PackageItem', back_populates='package', cascade='all, delete-orphan') + rate_plans = relationship('RatePlan', back_populates='package') + +class PackageItem(Base): + __tablename__ = 'package_items' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + package_id = Column(Integer, ForeignKey('packages.id'), nullable=False, index=True) + + # Item details + item_type = Column(Enum(PackageItemType), nullable=False) + item_id = Column(Integer, nullable=True) # ID of room_type, service, etc. + item_name = Column(String(200), nullable=False) # Name for display + item_description = Column(Text, nullable=True) + + # Quantity + quantity = Column(Integer, nullable=False, default=1) + unit = Column(String(50), nullable=True) # 'per_night', 'per_stay', 'per_person', etc. + + # Pricing + price = Column(Numeric(10, 2), nullable=True) # Item price (if set) + included = Column(Boolean, nullable=False, default=True) # If True, included in package price + price_modifier = Column(Numeric(5, 2), nullable=True, default=1.0) # Price multiplier for this item + + # Display order + display_order = Column(Integer, nullable=False, default=0) + + # Additional data + extra_data = Column(JSON, nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + package = relationship('Package', back_populates='items') + diff --git a/Backend/src/models/payment.py b/Backend/src/models/payment.py index f057b30a..35cd26b5 100644 --- a/Backend/src/models/payment.py +++ b/Backend/src/models/payment.py @@ -12,6 +12,7 @@ class PaymentMethod(str, enum.Enum): e_wallet = 'e_wallet' stripe = 'stripe' paypal = 'paypal' + borica = 'borica' class PaymentType(str, enum.Enum): full = 'full' diff --git a/Backend/src/models/rate_plan.py b/Backend/src/models/rate_plan.py new file mode 100644 index 00000000..5cfd95fa --- /dev/null +++ b/Backend/src/models/rate_plan.py @@ -0,0 +1,107 @@ +from sqlalchemy import Column, Integer, String, Numeric, Boolean, Text, JSON, Enum, ForeignKey, DateTime, Date +from sqlalchemy.orm import relationship +from datetime import datetime, date +import enum +from ..config.database import Base + +class RatePlanType(str, enum.Enum): + BAR = 'BAR' # Best Available Rate + non_refundable = 'non_refundable' + advance_purchase = 'advance_purchase' + corporate = 'corporate' + government = 'government' + military = 'military' + long_stay = 'long_stay' + package = 'package' + +class RatePlanStatus(str, enum.Enum): + active = 'active' + inactive = 'inactive' + scheduled = 'scheduled' + expired = 'expired' + +class RatePlan(Base): + __tablename__ = 'rate_plans' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + name = Column(String(100), nullable=False, index=True) + code = Column(String(50), unique=True, nullable=False, index=True) + description = Column(Text, nullable=True) + plan_type = Column(Enum(RatePlanType), nullable=False, default=RatePlanType.BAR) + status = Column(Enum(RatePlanStatus), nullable=False, default=RatePlanStatus.active) + + # Pricing + base_price_modifier = Column(Numeric(5, 2), nullable=False, default=1.0) # Multiplier (1.0 = 100%, 0.9 = 90%) + discount_percentage = Column(Numeric(5, 2), nullable=True, default=0) # Percentage discount + fixed_discount = Column(Numeric(10, 2), nullable=True, default=0) # Fixed amount discount + + # Applicability + room_type_id = Column(Integer, ForeignKey('room_types.id'), nullable=True) # None = all room types + min_nights = Column(Integer, nullable=True) # Minimum nights required + max_nights = Column(Integer, nullable=True) # Maximum nights allowed + advance_days_required = Column(Integer, nullable=True) # Days in advance required for booking + + # Date range + valid_from = Column(Date, nullable=True) + valid_to = Column(Date, nullable=True) + + # Restrictions + is_refundable = Column(Boolean, nullable=False, default=True) + requires_deposit = Column(Boolean, nullable=False, default=False) + deposit_percentage = Column(Numeric(5, 2), nullable=True, default=0) + cancellation_hours = Column(Integer, nullable=True) # Hours before check-in for free cancellation + + # Corporate/Government specific + corporate_code = Column(String(50), nullable=True, index=True) + requires_verification = Column(Boolean, nullable=False, default=False) + verification_type = Column(String(50), nullable=True) # 'corporate_id', 'government_id', 'military_id' + + # Long-stay specific + long_stay_nights = Column(Integer, nullable=True) # Nights required for long-stay discount + + # Package specific + is_package = Column(Boolean, nullable=False, default=False) + package_id = Column(Integer, ForeignKey('packages.id'), nullable=True) + + # Priority (lower number = higher priority) + priority = Column(Integer, nullable=False, default=100) + + # Additional data (metadata is reserved by SQLAlchemy) + extra_data = Column(JSON, nullable=True) # Additional flexible data + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + room_type = relationship('RoomType', back_populates='rate_plans') + package = relationship('Package', back_populates='rate_plans') + rules = relationship('RatePlanRule', back_populates='rate_plan', cascade='all, delete-orphan') + bookings = relationship('Booking', back_populates='rate_plan') + +class RatePlanRule(Base): + __tablename__ = 'rate_plan_rules' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + rate_plan_id = Column(Integer, ForeignKey('rate_plans.id'), nullable=False, index=True) + + # Rule type + rule_type = Column(String(50), nullable=False) # 'day_of_week', 'season', 'occupancy', 'date_range', etc. + rule_key = Column(String(100), nullable=False) # e.g., 'monday', 'summer', '2_guests', '2024-12-01_to_2024-12-31' + rule_value = Column(JSON, nullable=True) # Flexible value storage + + # Price adjustment + price_modifier = Column(Numeric(5, 2), nullable=True, default=1.0) + discount_percentage = Column(Numeric(5, 2), nullable=True, default=0) + fixed_adjustment = Column(Numeric(10, 2), nullable=True, default=0) + + # Priority within the rate plan + priority = Column(Integer, nullable=False, default=100) + + # Additional data + extra_data = Column(JSON, nullable=True) + + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + rate_plan = relationship('RatePlan', back_populates='rules') + diff --git a/Backend/src/models/room.py b/Backend/src/models/room.py index 4bc4e0e4..1497841e 100644 --- a/Backend/src/models/room.py +++ b/Backend/src/models/room.py @@ -30,4 +30,8 @@ class Room(Base): room_type = relationship('RoomType', back_populates='rooms') bookings = relationship('Booking', back_populates='room') reviews = relationship('Review', back_populates='room') - favorites = relationship('Favorite', back_populates='room', cascade='all, delete-orphan') \ No newline at end of file + favorites = relationship('Favorite', back_populates='room', cascade='all, delete-orphan') + maintenance_records = relationship('RoomMaintenance', back_populates='room', cascade='all, delete-orphan') + housekeeping_tasks = relationship('HousekeepingTask', back_populates='room', cascade='all, delete-orphan') + inspections = relationship('RoomInspection', back_populates='room', cascade='all, delete-orphan') + attributes = relationship('RoomAttribute', back_populates='room', cascade='all, delete-orphan') \ No newline at end of file diff --git a/Backend/src/models/room_attribute.py b/Backend/src/models/room_attribute.py new file mode 100644 index 00000000..9a67acd2 --- /dev/null +++ b/Backend/src/models/room_attribute.py @@ -0,0 +1,30 @@ +from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, Boolean, JSON +from sqlalchemy.orm import relationship +from datetime import datetime +from ..config.database import Base + +class RoomAttribute(Base): + __tablename__ = 'room_attributes' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + room_id = Column(Integer, ForeignKey('rooms.id'), nullable=False, index=True) + + # Attribute details + attribute_name = Column(String(100), nullable=False) # e.g., 'view_quality', 'noise_level', 'accessibility' + attribute_value = Column(String(255), nullable=True) # e.g., 'ocean_view', 'quiet', 'wheelchair_accessible' + attribute_data = Column(JSON, nullable=True) # Additional structured data + + # Tracking + last_updated = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + updated_by = Column(Integer, ForeignKey('users.id'), nullable=True) + notes = Column(Text, nullable=True) + + # Status + is_active = Column(Boolean, nullable=False, default=True) + + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + + # Relationships + room = relationship('Room', back_populates='attributes') + updater = relationship('User') + diff --git a/Backend/src/models/room_inspection.py b/Backend/src/models/room_inspection.py new file mode 100644 index 00000000..734bdb2f --- /dev/null +++ b/Backend/src/models/room_inspection.py @@ -0,0 +1,68 @@ +from sqlalchemy import Column, Integer, String, Text, Enum, ForeignKey, DateTime, Boolean, JSON, Numeric +from sqlalchemy.orm import relationship +from datetime import datetime +import enum +from ..config.database import Base + +class InspectionType(str, enum.Enum): + pre_checkin = 'pre_checkin' + post_checkout = 'post_checkout' + routine = 'routine' + maintenance = 'maintenance' + damage = 'damage' + +class InspectionStatus(str, enum.Enum): + pending = 'pending' + in_progress = 'in_progress' + completed = 'completed' + failed = 'failed' + cancelled = 'cancelled' + +class RoomInspection(Base): + __tablename__ = 'room_inspections' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + room_id = Column(Integer, ForeignKey('rooms.id'), nullable=False, index=True) + booking_id = Column(Integer, ForeignKey('bookings.id'), nullable=True, index=True) + + inspection_type = Column(Enum(InspectionType), nullable=False) + status = Column(Enum(InspectionStatus), nullable=False, default=InspectionStatus.pending) + + # Scheduling + scheduled_at = Column(DateTime, nullable=False, index=True) + started_at = Column(DateTime, nullable=True) + completed_at = Column(DateTime, nullable=True) + + # Assignment + inspected_by = Column(Integer, ForeignKey('users.id'), nullable=True) + created_by = Column(Integer, ForeignKey('users.id'), nullable=True) + + # Checklist + checklist_template_id = Column(Integer, nullable=True) # Reference to checklist template + checklist_items = Column(JSON, nullable=False) # Array of {category: string, item: string, status: string, notes: string, photos: string[]} + # status can be: 'pass', 'fail', 'needs_attention', 'not_applicable' + + # Overall assessment + overall_score = Column(Numeric(3, 2), nullable=True) # 0-5 rating + overall_notes = Column(Text, nullable=True) + issues_found = Column(JSON, nullable=True) # Array of {severity: string, description: string, photo: string} + # severity can be: 'critical', 'major', 'minor', 'cosmetic' + + # Photos + photos = Column(JSON, nullable=True) # Array of photo URLs + + # Follow-up + requires_followup = Column(Boolean, nullable=False, default=False) + followup_notes = Column(Text, nullable=True) + maintenance_request_id = Column(Integer, ForeignKey('room_maintenance.id'), nullable=True) + + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + room = relationship('Room', back_populates='inspections') + booking = relationship('Booking') + inspector = relationship('User', foreign_keys=[inspected_by]) + creator = relationship('User', foreign_keys=[created_by]) + maintenance_request = relationship('RoomMaintenance', foreign_keys=[maintenance_request_id]) + diff --git a/Backend/src/models/room_maintenance.py b/Backend/src/models/room_maintenance.py new file mode 100644 index 00000000..94de36be --- /dev/null +++ b/Backend/src/models/room_maintenance.py @@ -0,0 +1,62 @@ +from sqlalchemy import Column, Integer, String, Text, Enum, ForeignKey, DateTime, Boolean, Numeric +from sqlalchemy.orm import relationship +from datetime import datetime +import enum +from ..config.database import Base + +class MaintenanceType(str, enum.Enum): + preventive = 'preventive' + corrective = 'corrective' + emergency = 'emergency' + upgrade = 'upgrade' + inspection = 'inspection' + +class MaintenanceStatus(str, enum.Enum): + scheduled = 'scheduled' + in_progress = 'in_progress' + completed = 'completed' + cancelled = 'cancelled' + on_hold = 'on_hold' + +class RoomMaintenance(Base): + __tablename__ = 'room_maintenance' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + room_id = Column(Integer, ForeignKey('rooms.id'), nullable=False, index=True) + maintenance_type = Column(Enum(MaintenanceType), nullable=False) + status = Column(Enum(MaintenanceStatus), nullable=False, default=MaintenanceStatus.scheduled) + title = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + + # Scheduling + scheduled_start = Column(DateTime, nullable=False) + scheduled_end = Column(DateTime, nullable=True) + actual_start = Column(DateTime, nullable=True) + actual_end = Column(DateTime, nullable=True) + + # Assignment + assigned_to = Column(Integer, ForeignKey('users.id'), nullable=True) + reported_by = Column(Integer, ForeignKey('users.id'), nullable=True) + + # Cost tracking + estimated_cost = Column(Numeric(10, 2), nullable=True) + actual_cost = Column(Numeric(10, 2), nullable=True) + + # Room blocking + blocks_room = Column(Boolean, nullable=False, default=True) + block_start = Column(DateTime, nullable=True) + block_end = Column(DateTime, nullable=True) + + # Additional info + priority = Column(String(20), nullable=False, default='medium') # low, medium, high, urgent + notes = Column(Text, nullable=True) + completion_notes = Column(Text, nullable=True) + + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + room = relationship('Room', back_populates='maintenance_records') + assigned_staff = relationship('User', foreign_keys=[assigned_to]) + reporter = relationship('User', foreign_keys=[reported_by]) + diff --git a/Backend/src/models/room_type.py b/Backend/src/models/room_type.py index a9efa2b7..ea77b20a 100644 --- a/Backend/src/models/room_type.py +++ b/Backend/src/models/room_type.py @@ -13,4 +13,6 @@ class RoomType(Base): amenities = Column(JSON, nullable=True) created_at = Column(DateTime, default=datetime.utcnow, nullable=False) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) - rooms = relationship('Room', back_populates='room_type') \ No newline at end of file + rooms = relationship('Room', back_populates='room_type') + rate_plans = relationship('RatePlan', back_populates='room_type') + packages = relationship('Package', back_populates='room_type') \ No newline at end of file diff --git a/Backend/src/models/security_event.py b/Backend/src/models/security_event.py new file mode 100644 index 00000000..2c77b30c --- /dev/null +++ b/Backend/src/models/security_event.py @@ -0,0 +1,135 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON, Enum, Boolean +from sqlalchemy.orm import relationship +from datetime import datetime +import enum +from ..config.database import Base + +class SecurityEventType(str, enum.Enum): + login_attempt = 'login_attempt' + login_success = 'login_success' + login_failure = 'login_failure' + logout = 'logout' + password_change = 'password_change' + password_reset = 'password_reset' + account_locked = 'account_locked' + account_unlocked = 'account_unlocked' + permission_denied = 'permission_denied' + suspicious_activity = 'suspicious_activity' + data_access = 'data_access' + data_modification = 'data_modification' + data_deletion = 'data_deletion' + api_access = 'api_access' + ip_blocked = 'ip_blocked' + rate_limit_exceeded = 'rate_limit_exceeded' + oauth_login = 'oauth_login' + sso_login = 'sso_login' + +class SecurityEventSeverity(str, enum.Enum): + low = 'low' + medium = 'medium' + high = 'high' + critical = 'critical' + +class SecurityEvent(Base): + __tablename__ = 'security_events' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=True, index=True) + event_type = Column(Enum(SecurityEventType), nullable=False, index=True) + severity = Column(Enum(SecurityEventSeverity), nullable=False, default=SecurityEventSeverity.medium, index=True) + + # Request details + ip_address = Column(String(45), nullable=True, index=True) + user_agent = Column(String(500), nullable=True) + request_path = Column(String(500), nullable=True) + request_method = Column(String(10), nullable=True) + request_id = Column(String(36), nullable=True, index=True) + + # Event details + description = Column(Text, nullable=True) + details = Column(JSON, nullable=True) + extra_data = Column(JSON, nullable=True) # Additional metadata (metadata is reserved by SQLAlchemy) + + # Status + resolved = Column(Boolean, nullable=False, default=False) + resolved_at = Column(DateTime, nullable=True) + resolved_by = Column(Integer, ForeignKey('users.id'), nullable=True) + resolution_notes = Column(Text, nullable=True) + + # Location (if available) + country = Column(String(100), nullable=True) + city = Column(String(100), nullable=True) + latitude = Column(String(20), nullable=True) + longitude = Column(String(20), nullable=True) + + created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True) + + # Relationships + user = relationship('User', foreign_keys=[user_id]) + resolver = relationship('User', foreign_keys=[resolved_by]) + +class IPWhitelist(Base): + __tablename__ = 'ip_whitelist' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + ip_address = Column(String(45), nullable=False, unique=True, index=True) + description = Column(String(255), nullable=True) + is_active = Column(Boolean, nullable=False, default=True) + created_by = Column(Integer, ForeignKey('users.id'), nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + creator = relationship('User', foreign_keys=[created_by]) + +class IPBlacklist(Base): + __tablename__ = 'ip_blacklist' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + ip_address = Column(String(45), nullable=False, unique=True, index=True) + reason = Column(Text, nullable=True) + is_active = Column(Boolean, nullable=False, default=True) + blocked_until = Column(DateTime, nullable=True) # Temporary block + created_by = Column(Integer, ForeignKey('users.id'), nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + creator = relationship('User', foreign_keys=[created_by]) + +class OAuthProvider(Base): + __tablename__ = 'oauth_providers' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + name = Column(String(50), nullable=False, unique=True) # google, microsoft, github, etc. + display_name = Column(String(100), nullable=False) + client_id = Column(String(500), nullable=False) + client_secret = Column(String(500), nullable=False) # Should be encrypted + authorization_url = Column(String(500), nullable=False) + token_url = Column(String(500), nullable=False) + userinfo_url = Column(String(500), nullable=False) + scopes = Column(String(500), nullable=True) # space-separated scopes + is_active = Column(Boolean, nullable=False, default=True) + is_sso_enabled = Column(Boolean, nullable=False, default=False) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + +class OAuthToken(Base): + __tablename__ = 'oauth_tokens' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=False, index=True) + provider_id = Column(Integer, ForeignKey('oauth_providers.id'), nullable=False) + provider_user_id = Column(String(255), nullable=False) # User ID from OAuth provider + access_token = Column(Text, nullable=False) # Should be encrypted + refresh_token = Column(Text, nullable=True) # Should be encrypted + token_type = Column(String(50), nullable=True, default='Bearer') + expires_at = Column(DateTime, nullable=True) + scopes = Column(String(500), nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + user = relationship('User') + provider = relationship('OAuthProvider') + diff --git a/Backend/src/models/workflow.py b/Backend/src/models/workflow.py new file mode 100644 index 00000000..9ac4ea7e --- /dev/null +++ b/Backend/src/models/workflow.py @@ -0,0 +1,127 @@ +from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, Enum, ForeignKey, JSON +from sqlalchemy.orm import relationship +from datetime import datetime +import enum +from ..config.database import Base + +class WorkflowType(str, enum.Enum): + pre_arrival = 'pre_arrival' + room_preparation = 'room_preparation' + maintenance = 'maintenance' + guest_communication = 'guest_communication' + follow_up = 'follow_up' + custom = 'custom' + +class WorkflowStatus(str, enum.Enum): + active = 'active' + inactive = 'inactive' + archived = 'archived' + +class WorkflowTrigger(str, enum.Enum): + booking_created = 'booking_created' + booking_confirmed = 'booking_confirmed' + check_in = 'check_in' + check_out = 'check_out' + maintenance_request = 'maintenance_request' + guest_message = 'guest_message' + manual = 'manual' + scheduled = 'scheduled' + +class Workflow(Base): + __tablename__ = 'workflows' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + name = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + workflow_type = Column(Enum(WorkflowType), nullable=False) + status = Column(Enum(WorkflowStatus), nullable=False, default=WorkflowStatus.active) + trigger = Column(Enum(WorkflowTrigger), nullable=False) + trigger_config = Column(JSON, nullable=True) # Configuration for trigger (e.g., time before check-in) + steps = Column(JSON, nullable=False) # Array of workflow steps + sla_hours = Column(Integer, nullable=True) # SLA in hours for workflow completion + is_active = Column(Boolean, nullable=False, default=True) + created_by = Column(Integer, ForeignKey('users.id'), nullable=False) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + creator = relationship('User', foreign_keys=[created_by]) + workflow_instances = relationship('WorkflowInstance', back_populates='workflow', cascade='all, delete-orphan') + +class WorkflowInstance(Base): + __tablename__ = 'workflow_instances' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + workflow_id = Column(Integer, ForeignKey('workflows.id'), nullable=False) + booking_id = Column(Integer, ForeignKey('bookings.id'), nullable=True) + room_id = Column(Integer, ForeignKey('rooms.id'), nullable=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=True) # Guest user + status = Column(String(50), nullable=False, default='pending') # pending, in_progress, completed, cancelled + started_at = Column(DateTime, default=datetime.utcnow, nullable=False) + completed_at = Column(DateTime, nullable=True) + due_date = Column(DateTime, nullable=True) + meta_data = Column(JSON, nullable=True) # Additional context data + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + workflow = relationship('Workflow', back_populates='workflow_instances') + booking = relationship('Booking') + room = relationship('Room') + user = relationship('User', foreign_keys=[user_id]) + tasks = relationship('Task', back_populates='workflow_instance', cascade='all, delete-orphan') + +class TaskStatus(str, enum.Enum): + pending = 'pending' + assigned = 'assigned' + in_progress = 'in_progress' + completed = 'completed' + cancelled = 'cancelled' + overdue = 'overdue' + +class TaskPriority(str, enum.Enum): + low = 'low' + medium = 'medium' + high = 'high' + urgent = 'urgent' + +class Task(Base): + __tablename__ = 'tasks' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + title = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + task_type = Column(String(100), nullable=False) # e.g., 'room_cleaning', 'maintenance', 'guest_communication' + status = Column(Enum(TaskStatus), nullable=False, default=TaskStatus.pending) + priority = Column(Enum(TaskPriority), nullable=False, default=TaskPriority.medium) + workflow_instance_id = Column(Integer, ForeignKey('workflow_instances.id'), nullable=True) + booking_id = Column(Integer, ForeignKey('bookings.id'), nullable=True) + room_id = Column(Integer, ForeignKey('rooms.id'), nullable=True) + assigned_to = Column(Integer, ForeignKey('users.id'), nullable=True) + created_by = Column(Integer, ForeignKey('users.id'), nullable=False) + due_date = Column(DateTime, nullable=True) + completed_at = Column(DateTime, nullable=True) + estimated_duration_minutes = Column(Integer, nullable=True) + actual_duration_minutes = Column(Integer, nullable=True) + notes = Column(Text, nullable=True) + meta_data = Column(JSON, nullable=True) # Additional task-specific data + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + workflow_instance = relationship('WorkflowInstance', back_populates='tasks') + booking = relationship('Booking') + room = relationship('Room') + assignee = relationship('User', foreign_keys=[assigned_to]) + creator_user = relationship('User', foreign_keys=[created_by]) + task_comments = relationship('TaskComment', back_populates='task', cascade='all, delete-orphan') + +class TaskComment(Base): + __tablename__ = 'task_comments' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + task_id = Column(Integer, ForeignKey('tasks.id'), nullable=False) + user_id = Column(Integer, ForeignKey('users.id'), nullable=False) + comment = Column(Text, nullable=False) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + + task = relationship('Task', back_populates='task_comments') + user = relationship('User') + diff --git a/Backend/src/routes/__pycache__/advanced_room_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/advanced_room_routes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1adfb8330ca78202e79dbc102920b97d631118d5 GIT binary patch literal 43079 zcmeIbd2}1snI{Sm0EvYpKoT2Q0NnRQltf9SNKqsuQ8G#G3oVNwSs+CUAe92Bg$AX> zaT0pkb0W37Thv6;qB@=@=~*+DyE9`=_nZ@@<0OimcB5G6DHzR+m2S`C^ZtQipH4cR zIp=-fE!0A=C{gL;^jlJgytwOkZ{1sU>sx;H)dv|FMim@yA1I#K{zod+-(x~JvJ;Wd zmeW+KYbr`bseLNHdQk088BFnO1~q=|pw_P&)cI2fQ`K0W;!E@E2lal#puuk(G?F}x z&*V=ZO!sFDX81D)GyUd4Gs)NbEdH#)EPwW3Hc9JzIsTl%9KUtY3h7iT&1du52kj)T z_c{E|L8m`=FqfnazC3^aU_OZ(eFgr)!9o%@`HK9mL6_e>=qBlOpT}Q3SnMwuEFtL( zU#Y)ru*_dRSnjVFtRQ)rzDj@9V3ogmu-acUSVQv6zFL3XV4c5yu-@M=*g*0uzD9r3 zU=xXF`8N2Q2b&?TqOyG({hJ0ik$4WA)8@g={+7WOwJJsBRZ~{VHm<&Be=CKb%fT() zEt3IZf~8-|@s^rTL8}z_&!wDHuFy(e=Cuo{u@oxLyN$~C=2Hb;{a7kh_^R%$1j)fx z=;xwDUx-mfLTl(lN4eHat#=Gs%I)1wdA#jZG5iZ-F;XS6v2?64meLiym#sThPLrj?96tov6}KT zTklM!P>m~Avq{!ZC-n1GVGX5UYJ+Uf*56MvoR7ROJ+)ERSJxV=wMo{Oj#~d}t*rT* zTsytu+HDrsu0@O`ucC$8B0J;mHC9o6#_ONmtt(b>o2;LnHCC}zHnZ#Rrwvw7-j|-* zF6(R88mrhY>+5+|@i)0{@_VjBTqEi3OJ1!^YKQF1cCWEo@-tiiEOxF~t(~%d_N=j5 zU9uTme?Q%@TJpa1RFAB$-ZfThm#nXqtF_*qHPmjHVSaxT_OMiHkF4*#7t5-8KgCU@ zDm7;4+~0d}dM47kf*Y-VaS zrlq`r(W;b~_JxT+Fs2_MT@3pmM^6n0y}=2;H)g<)^7?|qpW;)rN*zlb_XdZk5vXOs z*ys#Rd#8d!GXWU3d3YvxdYYb?^$yX~K5qc3YmYzy{vL!3?Cjw5v_EFV@DMo{$V=Nf zJ$+_kYCM*ah$KohF*DZd8=ja7dZ&h`M!hk!EOGeaS#K;$mLWE??3sq~obh_k0@cIA zfitmec@AGMwz3uwgK5mf4tu8pXT76j!ZW2f>CPgh6TPWp8Q7*X7^EjgVDaEwZ6pJh zV|>aFb92Z`pPLxP0>jSXfVXcN<`ZY4dtx*g({xTuWXKC-)#D$nPI6NMhwKfcsj_Rgi`TWB^LLjCaru-9AFCO9NgClOF;;SQP8tWhIH ztf5__hDNNRTcd`S(h2;=TW2;^dSwc4Qc6=kS>rB6wja1Q-*s+ zfzm7LpiVg7$y6aG{f4xYI9o{vWunpr3gWpLo36arO_LpFM1;en=sgnK???3G1vw{EG;lI z3TF}MpUvx^b_ZrAg3`&0bAcNY4l9=ChpMpK+)bhJnun(ZY#Q`Vh*s%hDRBBGGI}`O z5!mr{W4`HWI;K50;XP04fBHF`Qk6NTfrkT)w-1e1Oe|xJp7u`!reU)E!@*b@?LF%o z9`(|dSeQ8m>obIFLuQ8Fh0S(jum^))49Hk?=Z1YV@KBN-h?+o1n^WVSMUBL$^0@COCbzVb65X z>#IvVx*B#4kDh@?cSC?4ZJ_aK9uPNoe%<-Z`m+~fIsDc=l-RoYlRpMsq8t>MhKc-N zs$YCb70;~EwM88zoTGwqRNTpA9WC?eQrgHmnj!5h<(!p_v+_

)bS-5w&?ZTNz_3 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 P0fVd}*A?i=I>7%0ZTiu@ delta 26550 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 KQeh-(;{OeC;XG?uY1 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 literal 0 HcmV?d00001 diff --git a/Backend/src/routes/__pycache__/group_booking_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/group_booking_routes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..274d10f1ed0b79ddff7d51417dc04e22d673e449 GIT binary patch literal 30943 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 P}1Wqp+UY1s0*fwI`x8vHr+InH3FtqA!?dM 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 z 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 a1@Y3MyBHO^Yyf5{P!KIueTorJKK?)UHO$8V literal 0 HcmV?d00001 diff --git a/Backend/src/routes/__pycache__/guest_profile_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/guest_profile_routes.cpython-312.pyc index 994b24cff34b3ac7e3f68c43d5dcd62d3e8fdda2..866c79f6d9bc3306b4711dde1a22de1f4ffe01c8 100644 GIT binary patch delta 8783 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 TJC6Ut9l3F<`|rHrZCLvs7C|?3 delta 3523 zcmZ`+Z)_CD72mnJ-P^st_AS1DK6~$c=R0o<*v5uJa0quc0T*HZqy$0%gFPF1#`clD zHI(EWQi{|_mD-|d)ej+UKlB3>qmWVwQiYN`6k!sn+@T`SwQ0UKl_DYKOB7XA=k5CJ zqaiEZ^P4wsX5YT|o8LS?*mGsK_Y;rD$+71b2V*&B^t?BO{&+t9tB8%$5OEXFF*NVJ zYy$_5I}p~wlWS?*6Wz?FdD3(lSrgWXG|&6%uBJsM^8AV?EGx9-;ZW&%kxxWw6JTPWKss5E?vjRo%`NzzGD^etOF4zK9YVoL#uB;Bw_ z!hB7z>)SU-zy|6Q8$8pnL6QZr9fFXb5(GVh-OuezsDhqge`=1|f~Jq< zh~CadI=oMhfrv9va};JLCbU96RuCimFR z^Mj8fsfUr()yV3rscK~X`Q(kMTlx3%^kYZb{V0;S_)^t3P!R{dqE%;nG|g6?MVT}@ zw=u(Uw?Hd#QYMK@QXr$?lEQ6g$JnEIr0dT;_SEmz_B4q3y2Dra!afEgcMG9gG&l((f^r zszhuA&)exKS6t!iXrIGHTxj%xBH8J6*L7YZPRsk+pWG6n54}Oxiuy9rLT`Cmv7ZF! zr=A1~QsnKFLPgLhpIw>>%IsDxFc%R!0J9 zzaSL(-=m+QIguzNM&d=0p`zmgrg2|4FA$N1vD=sF77REK=2MjFk<8oc`!GatbJ82q zDP+XxOec)CCR!lxuVM^$VdZyo4BPl0$A#6}P+L9|n)X$x9UwWbefs(GbN-$8da z?^4Z^4~MYknb5VPhDH`^zY7-a2C)Z37hPzMcj^G2OV}iYnq!)=!_i}!{tP$(HZO5% zv))7P{!OTt4*7?pfI$_|?f2!)6u87mr6PVexgKcmYjkGd) z*d}o$&b3wLO0E~$*x*hV*zc%q+XTEIU}jm6{39W}&>Gm*hZp+Xpg$KFk?~!bXZqa` z9%-c?2l~(-=zju#MK1bDFy$pYVX!Omtm-1Gx+fH*pSDJ@WXLoVN~0C@pRHl+G)E~K zi+UvYsJ@;BrEj36P_V@+#_S2wP>e46MX00KOx9lT%z}W7>-(5cUHU=Ln?M`_VY0ws z`fRxU$N?BH*TG@UZ3gRdF<|GKul~+D=a@7BOxRcyeHxa6fFKGV(VOAv#%vcV`;Dq^ zwj$2b=OUwhGCT8wS;Un&G$f-k@((rPvK+{&xU9NCUmF;5;oB~r>9?EkkW4>_tfSMS zoBlWQgDkmGFkZ4wbj=^^H}M zwF-&U8&x`-qc!2^bbfZmQ0@A0w#d4&UIlFmL>Ju|Ripn`8}rCPfmBi7Pk$a&4x1$b z&efk@kvvQ-SH#?Z25e8Sf(}z4;D{D}OPkxaT2H4-)ql`P#hv$umQ$p zK%e=rgm%x_i?&JGWX>Im2Rn=wxbtMOj&$Z|(NY)vjnZ+(YBBVez-(siTM!NNW?LZ; zZAvq=nlO={p3WI2$J9jDn@o)#oj7jceG~JxZaKj3BBNOAmKfy}^Eu4X=VRm0M=-{y z`gT^to%BjPo5~L0@&FI|2AmzFzW8c%6;fg^qDeQj zaFU(fRH-7K^wPLnrqPZRbxKWiPe+IBs3URCPQ;>}NLKO~DO~9QQV7z!9sOrqi)k8y z?=_)hwh2p&uG%aRn&b_ALPLAtwz$9#)&?$sp@}}TOMr&MY$MEmYq?)wjUn4J-3Y{S za2)_)0ufq4iw^X4^qoYT^E5L68x-DWOpu~Km6OpWI=0$`>pk!Fe0cEASZVD3n(t9H zskjCG6<92`apw@rDi{oFBmmD$xRsMl~KbdW(|wC%wuDKCsbaC;7tnsk|~@Ck*~$U q={%X8(l+UDF&q4YRq(Ov<}pSHeZ~zxpu?%QYsXUmw%LEe=KcpAcL3D@ diff --git a/Backend/src/routes/__pycache__/notification_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/notification_routes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e02ae843f86f04f19d41ae504aa9052141434c0 GIT binary patch literal 16024 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 literal 0 HcmV?d00001 diff --git a/Backend/src/routes/__pycache__/payment_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/payment_routes.cpython-312.pyc index f6c44d4bb6cb55c719c8b397d575d09aa0f1e1aa..6943c26430e5dc22a91132472c9807520902e379 100644 GIT binary patch literal 71155 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 delta 18793 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 literal 0 HcmV?d00001 diff --git a/Backend/src/routes/__pycache__/report_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/report_routes.cpython-312.pyc index 4234b0a17b2b6f6e957464700f0d7ae1b89ad536..a36b695e1aeb6c51e5fc3012ced54d0272f96348 100644 GIT binary patch delta 9304 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 O^JHP>i;91R`TjqxLg$(Q delta 7844 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 s+$$VlE7c?**Hmp}+pdhRFi5ScVDwKRnD+ng9R* diff --git a/Backend/src/routes/__pycache__/room_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/room_routes.cpython-312.pyc index 1d3079960d2b26cd1b0d85943f4fc1d9ea2df3c9..3b3b020e272ffc88a8d02b2807663b98dbb569fc 100644 GIT binary patch delta 7002 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 delta 5507 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 diff --git a/Backend/src/routes/__pycache__/security_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/security_routes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f19111e303778407a668340666df0832153a81a8 GIT binary patch literal 32617 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 literal 0 HcmV?d00001 diff --git a/Backend/src/routes/__pycache__/service_booking_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/service_booking_routes.cpython-312.pyc index 5446b4cd214656b4c847911c2088f360f56b7a80..8f06fd71baa0a7ec33ce0470f65111f78a2827c0 100644 GIT binary patch delta 3298 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 diff --git a/Backend/src/routes/__pycache__/system_settings_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/system_settings_routes.cpython-312.pyc index e78b85b6b18fa6eb41c7c4ceec05c5d8152ad35a..40ef948aa6d52dd1dfa542dc3513569585065cc5 100644 GIT binary patch delta 14995 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 X7Exr~I4@(lM`o_RyHl>Hgzo-dlxh|u delta 7602 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 ep-p+M8Zc!{pm?_5H{jqe?)#b+ z>NKoF)2dBd(XG@!om93zAl#KIQm1u)s6<5wot90JHd*CQe--V=VC|%d{nC?ztc5Ip<#gE&tWo#=AC~g@e}eY%t{rUT$pVe|@?4@`z-RtZ8qiZJ~|l zfVZW6nf8Tto)b8kml`GeDZb>mF5p0pTj-EGmiExDz9zpD^aA{xlIuFJX)9kC(L{Cp zz%F;GxiS(g^^!;Olv*B>w1UiAYP&9K9%??RL-6l2NY==dj#eU9&;zt@GRio?k%Ee29K6^oDQ^_0}e-ugdqFB@c)m^|~3Ksy0zyqu#+xt4H@qM9WuY}|WT!>S5RK|95#JB8FcUJn6 zL)}&3i;l{el9Qgb_&Z$lkN6(a4tml6c==`o2$odM)BCU2F?+0TXj{z})T z;>oinS&N1gU$Rw@M*rH})r)7z8bha1MX<3ZHLA~7_@a?siMG%k9@ne(`TOrek$lv6 z1kxvv&8dM!h_pne!S+6$JwZg05hUA@j3OBV649~6mKzhf+?iDNR4iM_Bmho`SO$-X zoqptLJ66Sk<_Y1X*)Vh=BAB&?0@Ic=g&af0B$6p0kuDZl23&FwrDL?crL*}EvUnoo z1tgdXXFf#{H%k&^&?0?)n0se^Trfn{e=SQ@7J&UX5#zd`h>QM%?I zoo<_F1)yxo#}(x@$ahv6$OH`4QV(OV1(4yP9lMA!(vJieMF#2Kfd3hU&?9|8UEDSi zF=$og3~I5|JW*e2AiHKl&^Q?ASh!f=j?tTe&XH>Z*#~3G#tf8qsB{`Kyk!yU%aib6 zMJ!3%x(=+2Jw7ReL(O$H=&Wj_6Y=C3E&MK$!hyB$Gncg;)J}vU{wNxw+pfaL;7V`uz^8GgA6qXeeK85upvq0xT@V;%`SpGiFZq-;p2+ckP>9{9LIC- zab`~4dKUK#e%e|EpC*Gc-4NPpJT#T)2{J)b#^ zlBrZ3_ZBai=}I^fG8Fl99PvEeJFygb$w2=c9&HXk&4T)Shj$^ESd*O0m-lIwxHj?c7ooq<(QM)%=1g&!Hpi4Lhx4c zo3VVbMz1uv=?%ZBaoM6)g-d<(p}}n}S}JF?GytR89pKng8CVoqN_B|hQk3!$H~o{* zME}!wmLH{=h?n<(teGavP4uIPVW##Bky?ulkiq!g6f{>XgR3#DW-#UsYakg|w!(Bf zSq7W?8*IM1=ZBRQ_=$ekO8b0@LZr*)nH))^BuUP)+D=Y_8xljpaGgdjgXF0RjS8H* zt`0ENvl76Pl_f?NP1s$LLk>_+@AP+U&tr#`@lMi&JWENyqIO@`RrMsw2o`XImZEfc zz{^MJ{6OooJS6yYlJ0pHHFC=C_4fqV$yK&;?$Y%Ep{nuchWivE-|Z^) zt>?R{z@F#a@TjoQy1Dfxe$LIWyIpfmaXlQKvx;}Edf@LmMUX%CkTAh2SrJ$lR)?L9 z{Hk|*kl(9@K+KwCESV}#35GuQmkd=^^mS{NOfs;nhf2}Q(D!zc$+?Jh6dwMTZW`siTyIRaaY`&PU)=j;y#`2{EuJIW{|6~vo1 zjsC+5(cb9Ww`rf+Fil~Rj!cL6OH-XwA?{X?2mV&bH60Ldjfbaw;^#g+@Sg`nki#Dt zW$Y-g()BTA#Z^#KX=R8#T~EvDJp7ws$x`-gDYpL3#41YMXW2k<1$@L4=rH7$#Z*=$ zC$M`O4RraOqQXl(l~l<^6o~nPN)Wc>=SXlv*SK%E4(uSlADMP%a#A5JKS!=W7jFq= ZmHuWtVz|de?{VX+UyT1$(0u^n{{kz4S^@w7 delta 3903 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 diff --git a/Backend/src/routes/__pycache__/workflow_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/workflow_routes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81f143397577941f076d435097b24d2112946741 GIT binary patch literal 15001 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 Wx-(mhHgIPivW;zPxVz-r@c#zDw5ag_ literal 0 HcmV?d00001 diff --git a/Backend/src/routes/advanced_room_routes.py b/Backend/src/routes/advanced_room_routes.py new file mode 100644 index 00000000..eca434f6 --- /dev/null +++ b/Backend/src/routes/advanced_room_routes.py @@ -0,0 +1,859 @@ +from fastapi import APIRouter, Depends, HTTPException, status, Query, Request +from sqlalchemy.orm import Session, joinedload, load_only +from sqlalchemy import and_, or_, func, desc +from typing import List, Optional +from datetime import datetime, timedelta +from ..config.database import get_db +from ..middleware.auth import get_current_user, authorize_roles +from ..models.user import User +from ..models.role import Role +from ..models.room import Room, RoomStatus +from ..models.booking import Booking, BookingStatus +from ..models.room_maintenance import RoomMaintenance, MaintenanceType, MaintenanceStatus +from ..models.housekeeping_task import HousekeepingTask, HousekeepingStatus, HousekeepingType +from ..models.room_inspection import RoomInspection, InspectionType, InspectionStatus +from ..models.room_attribute import RoomAttribute +from ..services.room_assignment_service import RoomAssignmentService +from pydantic import BaseModel +from typing import Dict, Any + +router = APIRouter(prefix='/advanced-rooms', tags=['advanced-room-management']) + + +# ==================== Room Assignment Optimization ==================== + +@router.post('/assign-optimal-room') +async def assign_optimal_room( + request_data: dict, + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Find the best available room for a booking based on preferences""" + try: + room_type_id = request_data.get('room_type_id') + check_in_str = request_data.get('check_in') + check_out_str = request_data.get('check_out') + num_guests = request_data.get('num_guests', 1) + guest_preferences = request_data.get('guest_preferences', {}) + exclude_room_ids = request_data.get('exclude_room_ids', []) + + if not room_type_id or not check_in_str or not check_out_str: + raise HTTPException(status_code=400, detail='Missing required fields') + + check_in = datetime.fromisoformat(check_in_str.replace('Z', '+00:00')) + check_out = datetime.fromisoformat(check_out_str.replace('Z', '+00:00')) + + best_room = RoomAssignmentService.find_best_room( + db=db, + room_type_id=room_type_id, + check_in=check_in, + check_out=check_out, + num_guests=num_guests, + guest_preferences=guest_preferences, + exclude_room_ids=exclude_room_ids + ) + + if not best_room: + return { + 'status': 'success', + 'data': {'room': None, 'message': 'No suitable room available'} + } + + return { + 'status': 'success', + 'data': { + 'room': { + 'id': best_room.id, + 'room_number': best_room.room_number, + 'floor': best_room.floor, + 'view': best_room.view, + 'status': best_room.status.value + } + } + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get('/{room_id}/availability-calendar') +async def get_room_availability_calendar( + room_id: int, + start_date: str = Query(...), + end_date: str = Query(...), + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get detailed availability calendar for a room""" + try: + start = datetime.fromisoformat(start_date.replace('Z', '+00:00')) + end = datetime.fromisoformat(end_date.replace('Z', '+00:00')) + + calendar = RoomAssignmentService.get_room_availability_calendar( + db=db, + room_id=room_id, + start_date=start, + end_date=end + ) + + if not calendar: + raise HTTPException(status_code=404, detail='Room not found') + + return {'status': 'success', 'data': calendar} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# ==================== Room Maintenance ==================== + +@router.get('/maintenance') +async def get_maintenance_records( + room_id: Optional[int] = Query(None), + status: Optional[str] = Query(None), + maintenance_type: Optional[str] = Query(None), + page: int = Query(1, ge=1), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get maintenance records with filtering""" + try: + # Check if user is staff (not admin) - staff should only see their assigned records + role = db.query(Role).filter(Role.id == current_user.role_id).first() + is_staff = role and role.name == 'staff' + + query = db.query(RoomMaintenance) + + # Filter by assigned_to for staff users + if is_staff: + query = query.filter(RoomMaintenance.assigned_to == current_user.id) + + if room_id: + query = query.filter(RoomMaintenance.room_id == room_id) + if status: + query = query.filter(RoomMaintenance.status == MaintenanceStatus(status)) + if maintenance_type: + query = query.filter(RoomMaintenance.maintenance_type == MaintenanceType(maintenance_type)) + + total = query.count() + query = query.order_by(desc(RoomMaintenance.scheduled_start)) + + offset = (page - 1) * limit + records = query.offset(offset).limit(limit).all() + + result = [] + for record in records: + result.append({ + 'id': record.id, + 'room_id': record.room_id, + 'room_number': record.room.room_number if record.room else None, + 'maintenance_type': record.maintenance_type.value, + 'status': record.status.value, + 'title': record.title, + 'description': record.description, + 'scheduled_start': record.scheduled_start.isoformat() if record.scheduled_start else None, + 'scheduled_end': record.scheduled_end.isoformat() if record.scheduled_end else None, + 'actual_start': record.actual_start.isoformat() if record.actual_start else None, + 'actual_end': record.actual_end.isoformat() if record.actual_end else None, + 'assigned_to': record.assigned_to, + 'assigned_staff_name': record.assigned_staff.full_name if record.assigned_staff else None, + 'priority': record.priority, + 'blocks_room': record.blocks_room, + 'estimated_cost': float(record.estimated_cost) if record.estimated_cost else None, + 'actual_cost': float(record.actual_cost) if record.actual_cost else None, + 'created_at': record.created_at.isoformat() if record.created_at else None + }) + + return { + 'status': 'success', + 'data': { + 'maintenance_records': result, + 'pagination': { + 'total': total, + 'page': page, + 'limit': limit, + 'total_pages': (total + limit - 1) // limit + } + } + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post('/maintenance') +async def create_maintenance_record( + maintenance_data: dict, + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Create a new maintenance record""" + try: + room = db.query(Room).filter(Room.id == maintenance_data.get('room_id')).first() + if not room: + raise HTTPException(status_code=404, detail='Room not found') + + scheduled_start = datetime.fromisoformat(maintenance_data['scheduled_start'].replace('Z', '+00:00')) + scheduled_end = None + if maintenance_data.get('scheduled_end'): + scheduled_end = datetime.fromisoformat(maintenance_data['scheduled_end'].replace('Z', '+00:00')) + + block_start = None + block_end = None + if maintenance_data.get('block_start'): + block_start = datetime.fromisoformat(maintenance_data['block_start'].replace('Z', '+00:00')) + if maintenance_data.get('block_end'): + block_end = datetime.fromisoformat(maintenance_data['block_end'].replace('Z', '+00:00')) + + maintenance = RoomMaintenance( + room_id=maintenance_data['room_id'], + maintenance_type=MaintenanceType(maintenance_data.get('maintenance_type', 'preventive')), + status=MaintenanceStatus(maintenance_data.get('status', 'scheduled')), + title=maintenance_data.get('title', 'Maintenance'), + description=maintenance_data.get('description'), + scheduled_start=scheduled_start, + scheduled_end=scheduled_end, + assigned_to=maintenance_data.get('assigned_to'), + reported_by=current_user.id, + estimated_cost=maintenance_data.get('estimated_cost'), + blocks_room=maintenance_data.get('blocks_room', True), + block_start=block_start, + block_end=block_end, + priority=maintenance_data.get('priority', 'medium'), + notes=maintenance_data.get('notes') + ) + + # Update room status if blocking and maintenance is active + if maintenance.blocks_room and maintenance.status in [MaintenanceStatus.scheduled, MaintenanceStatus.in_progress]: + # Only update if room is currently available + if room.status == RoomStatus.available: + room.status = RoomStatus.maintenance + + db.add(maintenance) + db.commit() + db.refresh(maintenance) + + return { + 'status': 'success', + 'message': 'Maintenance record created successfully', + 'data': {'maintenance_id': maintenance.id} + } + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + + +@router.put('/maintenance/{maintenance_id}') +async def update_maintenance_record( + maintenance_id: int, + maintenance_data: dict, + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Update a maintenance record""" + try: + maintenance = db.query(RoomMaintenance).filter(RoomMaintenance.id == maintenance_id).first() + if not maintenance: + raise HTTPException(status_code=404, detail='Maintenance record not found') + + # Check if user is staff (not admin) - staff can only update their own assigned records + role = db.query(Role).filter(Role.id == current_user.role_id).first() + is_staff = role and role.name == 'staff' + + if is_staff: + # Staff can only update records assigned to them + if maintenance.assigned_to != current_user.id: + raise HTTPException(status_code=403, detail='You can only update maintenance assigned to you') + # Staff can only update status and completion fields + allowed_fields = {'status', 'actual_start', 'actual_end', 'completion_notes', 'actual_cost'} + if any(key not in allowed_fields for key in maintenance_data.keys()): + raise HTTPException(status_code=403, detail='You can only update status and completion information') + + # Update fields + if 'status' in maintenance_data: + new_status = MaintenanceStatus(maintenance_data['status']) + + # Only the assigned user can mark the maintenance as completed + if new_status == MaintenanceStatus.completed: + if not maintenance.assigned_to: + raise HTTPException(status_code=400, detail='Maintenance must be assigned before it can be marked as completed') + if maintenance.assigned_to != current_user.id: + raise HTTPException(status_code=403, detail='Only the assigned staff member can mark this maintenance as completed') + + old_status = maintenance.status + maintenance.status = new_status + + # Update room status based on maintenance status + if maintenance.status == MaintenanceStatus.completed and maintenance.blocks_room: + # Check if room has other active maintenance + other_maintenance = db.query(RoomMaintenance).filter( + and_( + RoomMaintenance.room_id == maintenance.room_id, + RoomMaintenance.id != maintenance_id, + RoomMaintenance.blocks_room == True, + RoomMaintenance.status.in_([MaintenanceStatus.scheduled, MaintenanceStatus.in_progress]) + ) + ).first() + + if not other_maintenance: + # Check if room has active bookings + from datetime import datetime + active_booking = db.query(Booking).filter( + and_( + Booking.room_id == maintenance.room_id, + Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in]), + Booking.check_in_date <= datetime.utcnow(), + Booking.check_out_date > datetime.utcnow() + ) + ).first() + + if active_booking: + maintenance.room.status = RoomStatus.occupied + else: + maintenance.room.status = RoomStatus.available + elif maintenance.status in [MaintenanceStatus.scheduled, MaintenanceStatus.in_progress] and maintenance.blocks_room: + # Set room to maintenance if it's not occupied + if maintenance.room.status == RoomStatus.available: + maintenance.room.status = RoomStatus.maintenance + + if 'actual_start' in maintenance_data: + maintenance.actual_start = datetime.fromisoformat(maintenance_data['actual_start'].replace('Z', '+00:00')) + if 'actual_end' in maintenance_data: + maintenance.actual_end = datetime.fromisoformat(maintenance_data['actual_end'].replace('Z', '+00:00')) + if 'completion_notes' in maintenance_data: + maintenance.completion_notes = maintenance_data['completion_notes'] + if 'actual_cost' in maintenance_data: + maintenance.actual_cost = maintenance_data['actual_cost'] + + db.commit() + + return { + 'status': 'success', + 'message': 'Maintenance record updated successfully' + } + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + + +# ==================== Housekeeping Tasks ==================== + +@router.get('/housekeeping') +async def get_housekeeping_tasks( + room_id: Optional[int] = Query(None), + status: Optional[str] = Query(None), + task_type: Optional[str] = Query(None), + date: Optional[str] = Query(None), + page: int = Query(1, ge=1), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get housekeeping tasks with filtering""" + try: + # Check if user is staff (not admin) - staff should only see their assigned tasks + role = db.query(Role).filter(Role.id == current_user.role_id).first() + is_staff = role and role.name == 'staff' + + query = db.query(HousekeepingTask) + + # Filter by assigned_to for staff users + if is_staff: + query = query.filter(HousekeepingTask.assigned_to == current_user.id) + + if room_id: + query = query.filter(HousekeepingTask.room_id == room_id) + if status: + query = query.filter(HousekeepingTask.status == HousekeepingStatus(status)) + if task_type: + query = query.filter(HousekeepingTask.task_type == HousekeepingType(task_type)) + if date: + date_obj = datetime.fromisoformat(date.replace('Z', '+00:00')).date() + query = query.filter(func.date(HousekeepingTask.scheduled_time) == date_obj) + + total = query.count() + query = query.order_by(HousekeepingTask.scheduled_time) + + offset = (page - 1) * limit + tasks = query.offset(offset).limit(limit).all() + + result = [] + for task in tasks: + result.append({ + 'id': task.id, + 'room_id': task.room_id, + 'room_number': task.room.room_number if task.room else None, + 'booking_id': task.booking_id, + 'task_type': task.task_type.value, + 'status': task.status.value, + 'scheduled_time': task.scheduled_time.isoformat() if task.scheduled_time else None, + 'started_at': task.started_at.isoformat() if task.started_at else None, + 'completed_at': task.completed_at.isoformat() if task.completed_at else None, + 'assigned_to': task.assigned_to, + 'assigned_staff_name': task.assigned_staff.full_name if task.assigned_staff else None, + 'checklist_items': task.checklist_items, + 'notes': task.notes, + 'quality_score': task.quality_score, + 'estimated_duration_minutes': task.estimated_duration_minutes, + 'actual_duration_minutes': task.actual_duration_minutes + }) + + return { + 'status': 'success', + 'data': { + 'tasks': result, + 'pagination': { + 'total': total, + 'page': page, + 'limit': limit, + 'total_pages': (total + limit - 1) // limit + } + } + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post('/housekeeping') +async def create_housekeeping_task( + task_data: dict, + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Create a new housekeeping task""" + try: + room = db.query(Room).filter(Room.id == task_data.get('room_id')).first() + if not room: + raise HTTPException(status_code=404, detail='Room not found') + + scheduled_time = datetime.fromisoformat(task_data['scheduled_time'].replace('Z', '+00:00')) + assigned_to = task_data.get('assigned_to') + + task = HousekeepingTask( + room_id=task_data['room_id'], + booking_id=task_data.get('booking_id'), + task_type=HousekeepingType(task_data.get('task_type', 'vacant')), + status=HousekeepingStatus(task_data.get('status', 'pending')), + scheduled_time=scheduled_time, + assigned_to=assigned_to, + created_by=current_user.id, + checklist_items=task_data.get('checklist_items', []), + notes=task_data.get('notes'), + estimated_duration_minutes=task_data.get('estimated_duration_minutes') + ) + + db.add(task) + db.commit() + db.refresh(task) + + # Send notification to assigned staff member if task is assigned + if assigned_to: + try: + from ..routes.chat_routes import manager + assigned_staff = db.query(User).filter(User.id == assigned_to).first() + task_data_notification = { + 'id': task.id, + 'room_id': task.room_id, + 'room_number': room.room_number, + 'task_type': task.task_type.value, + 'status': task.status.value, + 'scheduled_time': task.scheduled_time.isoformat() if task.scheduled_time else None, + 'assigned_to': task.assigned_to, + 'created_at': task.created_at.isoformat() if task.created_at else None + } + notification_data = { + 'type': 'housekeeping_task_assigned', + 'data': task_data_notification + } + # Send notification to the specific staff member + if assigned_to in manager.staff_connections: + try: + await manager.staff_connections[assigned_to].send_json(notification_data) + except Exception as e: + print(f'Error sending housekeeping task notification to staff {assigned_to}: {e}') + except Exception as e: + print(f'Error setting up housekeeping task notification: {e}') + + return { + 'status': 'success', + 'message': 'Housekeeping task created successfully', + 'data': {'task_id': task.id} + } + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + + +@router.put('/housekeeping/{task_id}') +async def update_housekeeping_task( + task_id: int, + task_data: dict, + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Update a housekeeping task""" + try: + task = db.query(HousekeepingTask).filter(HousekeepingTask.id == task_id).first() + if not task: + raise HTTPException(status_code=404, detail='Housekeeping task not found') + + # Check if user is staff (not admin) - staff can only update their own assigned tasks + role = db.query(Role).filter(Role.id == current_user.role_id).first() + is_staff = role and role.name == 'staff' + + if is_staff: + # Staff can only update tasks assigned to them + if task.assigned_to != current_user.id: + raise HTTPException(status_code=403, detail='You can only update tasks assigned to you') + # Staff cannot change assignment + if 'assigned_to' in task_data and task_data.get('assigned_to') != task.assigned_to: + raise HTTPException(status_code=403, detail='You cannot change task assignment') + + old_assigned_to = task.assigned_to + assigned_to_changed = False + + if 'assigned_to' in task_data and not is_staff: + new_assigned_to = task_data.get('assigned_to') + if new_assigned_to != old_assigned_to: + task.assigned_to = new_assigned_to + assigned_to_changed = True + + if 'status' in task_data: + new_status = HousekeepingStatus(task_data['status']) + + # Only the assigned user can mark the task as completed + if new_status == HousekeepingStatus.completed: + if not task.assigned_to: + raise HTTPException(status_code=400, detail='Task must be assigned before it can be marked as completed') + if task.assigned_to != current_user.id: + raise HTTPException(status_code=403, detail='Only the assigned staff member can mark this task as completed') + + task.status = new_status + + if new_status == HousekeepingStatus.in_progress and not task.started_at: + task.started_at = datetime.utcnow() + elif new_status == HousekeepingStatus.completed and not task.completed_at: + task.completed_at = datetime.utcnow() + if task.started_at: + duration = (task.completed_at - task.started_at).total_seconds() / 60 + task.actual_duration_minutes = int(duration) + + if 'checklist_items' in task_data: + task.checklist_items = task_data['checklist_items'] + if 'notes' in task_data: + task.notes = task_data['notes'] + if 'issues_found' in task_data: + task.issues_found = task_data['issues_found'] + if 'quality_score' in task_data: + task.quality_score = task_data['quality_score'] + if 'inspected_by' in task_data: + task.inspected_by = task_data['inspected_by'] + task.inspected_at = datetime.utcnow() + if 'inspection_notes' in task_data: + task.inspection_notes = task_data['inspection_notes'] + + db.commit() + db.refresh(task) + + # Send notification if assignment changed + if assigned_to_changed and task.assigned_to: + try: + from ..routes.chat_routes import manager + room = db.query(Room).filter(Room.id == task.room_id).first() + task_data_notification = { + 'id': task.id, + 'room_id': task.room_id, + 'room_number': room.room_number if room else None, + 'task_type': task.task_type.value, + 'status': task.status.value, + 'scheduled_time': task.scheduled_time.isoformat() if task.scheduled_time else None, + 'assigned_to': task.assigned_to, + 'updated_at': task.updated_at.isoformat() if task.updated_at else None + } + notification_data = { + 'type': 'housekeeping_task_assigned', + 'data': task_data_notification + } + # Send notification to the newly assigned staff member + if task.assigned_to in manager.staff_connections: + try: + await manager.staff_connections[task.assigned_to].send_json(notification_data) + except Exception as e: + print(f'Error sending housekeeping task notification to staff {task.assigned_to}: {e}') + except Exception as e: + print(f'Error setting up housekeeping task notification: {e}') + + return { + 'status': 'success', + 'message': 'Housekeeping task updated successfully' + } + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + + +# ==================== Room Inspections ==================== + +@router.get('/inspections') +async def get_room_inspections( + room_id: Optional[int] = Query(None), + inspection_type: Optional[str] = Query(None), + status: Optional[str] = Query(None), + page: int = Query(1, ge=1), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get room inspections with filtering""" + try: + # Check if user is staff (not admin) - staff should only see their assigned inspections + role = db.query(Role).filter(Role.id == current_user.role_id).first() + is_staff = role and role.name == 'staff' + + query = db.query(RoomInspection) + + # Filter by inspected_by for staff users + if is_staff: + query = query.filter(RoomInspection.inspected_by == current_user.id) + + if room_id: + query = query.filter(RoomInspection.room_id == room_id) + if inspection_type: + query = query.filter(RoomInspection.inspection_type == InspectionType(inspection_type)) + if status: + query = query.filter(RoomInspection.status == InspectionStatus(status)) + + total = query.count() + query = query.order_by(desc(RoomInspection.scheduled_at)) + + offset = (page - 1) * limit + inspections = query.offset(offset).limit(limit).all() + + result = [] + for inspection in inspections: + result.append({ + 'id': inspection.id, + 'room_id': inspection.room_id, + 'room_number': inspection.room.room_number if inspection.room else None, + 'booking_id': inspection.booking_id, + 'inspection_type': inspection.inspection_type.value, + 'status': inspection.status.value, + 'scheduled_at': inspection.scheduled_at.isoformat() if inspection.scheduled_at else None, + 'started_at': inspection.started_at.isoformat() if inspection.started_at else None, + 'completed_at': inspection.completed_at.isoformat() if inspection.completed_at else None, + 'inspected_by': inspection.inspected_by, + 'inspector_name': inspection.inspector.full_name if inspection.inspector else None, + 'checklist_items': inspection.checklist_items, + 'overall_score': float(inspection.overall_score) if inspection.overall_score else None, + 'overall_notes': inspection.overall_notes, + 'issues_found': inspection.issues_found, + 'requires_followup': inspection.requires_followup, + 'created_at': inspection.created_at.isoformat() if inspection.created_at else None + }) + + return { + 'status': 'success', + 'data': { + 'inspections': result, + 'pagination': { + 'total': total, + 'page': page, + 'limit': limit, + 'total_pages': (total + limit - 1) // limit + } + } + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post('/inspections') +async def create_room_inspection( + inspection_data: dict, + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Create a new room inspection""" + try: + room = db.query(Room).filter(Room.id == inspection_data.get('room_id')).first() + if not room: + raise HTTPException(status_code=404, detail='Room not found') + + scheduled_at = datetime.fromisoformat(inspection_data['scheduled_at'].replace('Z', '+00:00')) + + inspection = RoomInspection( + room_id=inspection_data['room_id'], + booking_id=inspection_data.get('booking_id'), + inspection_type=InspectionType(inspection_data.get('inspection_type', 'routine')), + status=InspectionStatus(inspection_data.get('status', 'pending')), + scheduled_at=scheduled_at, + inspected_by=inspection_data.get('inspected_by'), + created_by=current_user.id, + checklist_items=inspection_data.get('checklist_items', []), + checklist_template_id=inspection_data.get('checklist_template_id') + ) + + db.add(inspection) + db.commit() + db.refresh(inspection) + + return { + 'status': 'success', + 'message': 'Room inspection created successfully', + 'data': {'inspection_id': inspection.id} + } + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + + +@router.put('/inspections/{inspection_id}') +async def update_room_inspection( + inspection_id: int, + inspection_data: dict, + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Update a room inspection""" + try: + inspection = db.query(RoomInspection).filter(RoomInspection.id == inspection_id).first() + if not inspection: + raise HTTPException(status_code=404, detail='Room inspection not found') + + # Check if user is staff (not admin) - staff can only update their own assigned inspections + role = db.query(Role).filter(Role.id == current_user.role_id).first() + is_staff = role and role.name == 'staff' + + if is_staff: + # Staff can only update inspections assigned to them + if inspection.inspected_by != current_user.id: + raise HTTPException(status_code=403, detail='You can only update inspections assigned to you') + # Staff can only update status and inspection results + allowed_fields = {'status', 'checklist_items', 'overall_score', 'overall_notes', 'issues_found', 'requires_followup', 'followup_notes'} + if any(key not in allowed_fields for key in inspection_data.keys()): + raise HTTPException(status_code=403, detail='You can only update status and inspection results') + + if 'status' in inspection_data: + new_status = InspectionStatus(inspection_data['status']) + + # Only the assigned user can mark the inspection as completed + if new_status == InspectionStatus.completed: + if not inspection.inspected_by: + raise HTTPException(status_code=400, detail='Inspection must be assigned before it can be marked as completed') + if inspection.inspected_by != current_user.id: + raise HTTPException(status_code=403, detail='Only the assigned inspector can mark this inspection as completed') + + inspection.status = new_status + + if new_status == InspectionStatus.in_progress and not inspection.started_at: + inspection.started_at = datetime.utcnow() + elif new_status == InspectionStatus.completed and not inspection.completed_at: + inspection.completed_at = datetime.utcnow() + + if 'checklist_items' in inspection_data: + inspection.checklist_items = inspection_data['checklist_items'] + if 'overall_score' in inspection_data: + inspection.overall_score = inspection_data['overall_score'] + if 'overall_notes' in inspection_data: + inspection.overall_notes = inspection_data['overall_notes'] + if 'issues_found' in inspection_data: + inspection.issues_found = inspection_data['issues_found'] + if 'photos' in inspection_data: + inspection.photos = inspection_data['photos'] + if 'requires_followup' in inspection_data: + inspection.requires_followup = inspection_data['requires_followup'] + if 'followup_notes' in inspection_data: + inspection.followup_notes = inspection_data['followup_notes'] + if 'maintenance_request_id' in inspection_data: + inspection.maintenance_request_id = inspection_data['maintenance_request_id'] + + db.commit() + + return { + 'status': 'success', + 'message': 'Room inspection updated successfully' + } + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + + +# ==================== Room Status Board ==================== + +@router.get('/status-board') +async def get_room_status_board( + floor: Optional[int] = Query(None), + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get visual room status board with all rooms and their current status""" + try: + query = db.query(Room).options(joinedload(Room.room_type)) + if floor: + query = query.filter(Room.floor == floor) + + rooms = query.order_by(Room.floor, Room.room_number).all() + + result = [] + for room in rooms: + # Get current booking if any + # Use load_only to avoid querying columns that don't exist in the database (rate_plan_id, group_booking_id) + current_booking = db.query(Booking).options( + joinedload(Booking.user), + load_only(Booking.id, Booking.user_id, Booking.room_id, Booking.check_in_date, Booking.check_out_date, Booking.status) + ).filter( + and_( + Booking.room_id == room.id, + Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in]), + Booking.check_in_date <= datetime.utcnow(), + Booking.check_out_date > datetime.utcnow() + ) + ).first() + + # Get active maintenance + active_maintenance = db.query(RoomMaintenance).filter( + and_( + RoomMaintenance.room_id == room.id, + RoomMaintenance.blocks_room == True, + RoomMaintenance.status.in_([MaintenanceStatus.scheduled, MaintenanceStatus.in_progress]) + ) + ).first() + + # Get pending housekeeping tasks + pending_housekeeping = db.query(HousekeepingTask).filter( + and_( + HousekeepingTask.room_id == room.id, + HousekeepingTask.status == HousekeepingStatus.pending, + func.date(HousekeepingTask.scheduled_time) == datetime.utcnow().date() + ) + ).count() + + result.append({ + 'id': room.id, + 'room_number': room.room_number, + 'floor': room.floor, + 'status': room.status.value, + 'room_type': room.room_type.name if room.room_type else None, + 'current_booking': { + 'id': current_booking.id, + 'guest_name': current_booking.user.full_name if current_booking.user else 'Unknown', + 'check_out': current_booking.check_out_date.isoformat() + } if current_booking else None, + 'active_maintenance': { + 'id': active_maintenance.id, + 'title': active_maintenance.title, + 'type': active_maintenance.maintenance_type.value + } if active_maintenance else None, + 'pending_housekeeping_count': pending_housekeeping + }) + + return { + 'status': 'success', + 'data': {'rooms': result} + } + except Exception as e: + import logging + import traceback + logger = logging.getLogger(__name__) + logger.error(f'Error in get_room_status_board: {str(e)}') + logger.error(f'Traceback: {traceback.format_exc()}') + raise HTTPException(status_code=500, detail=str(e)) + diff --git a/Backend/src/routes/analytics_routes.py b/Backend/src/routes/analytics_routes.py new file mode 100644 index 00000000..bce4abd2 --- /dev/null +++ b/Backend/src/routes/analytics_routes.py @@ -0,0 +1,301 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import Optional +from ..config.database import get_db +from ..middleware.auth import authorize_roles, get_current_user +from ..models.user import User +from ..services.analytics_service import AnalyticsService + +router = APIRouter(prefix='/analytics', tags=['analytics']) + +# ==================== REVENUE ANALYTICS ==================== + +@router.get('/revenue/revpar') +async def get_revpar( + start_date: Optional[str] = Query(None, alias='from'), + end_date: Optional[str] = Query(None, alias='to'), + current_user: User = Depends(authorize_roles('admin', 'staff', 'accountant')), + db: Session = Depends(get_db) +): + """Get RevPAR (Revenue Per Available Room)""" + try: + start, end = AnalyticsService.parse_date_range(start_date, end_date) + result = AnalyticsService.get_revpar(db, start, end) + return {'status': 'success', 'data': result} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/revenue/adr') +async def get_adr( + start_date: Optional[str] = Query(None, alias='from'), + end_date: Optional[str] = Query(None, alias='to'), + current_user: User = Depends(authorize_roles('admin', 'staff', 'accountant')), + db: Session = Depends(get_db) +): + """Get ADR (Average Daily Rate)""" + try: + start, end = AnalyticsService.parse_date_range(start_date, end_date) + result = AnalyticsService.get_adr(db, start, end) + return {'status': 'success', 'data': result} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/revenue/occupancy') +async def get_occupancy_rate( + start_date: Optional[str] = Query(None, alias='from'), + end_date: Optional[str] = Query(None, alias='to'), + current_user: User = Depends(authorize_roles('admin', 'staff', 'accountant')), + db: Session = Depends(get_db) +): + """Get Occupancy Rate""" + try: + start, end = AnalyticsService.parse_date_range(start_date, end_date) + result = AnalyticsService.get_occupancy_rate(db, start, end) + return {'status': 'success', 'data': result} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/revenue/forecast') +async def get_revenue_forecast( + days: int = Query(30, ge=1, le=365), + current_user: User = Depends(authorize_roles('admin', 'staff', 'accountant')), + db: Session = Depends(get_db) +): + """Get Revenue Forecast""" + try: + result = AnalyticsService.get_revenue_forecast(db, days) + return {'status': 'success', 'data': result} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/revenue/market-penetration') +async def get_market_penetration( + start_date: Optional[str] = Query(None, alias='from'), + end_date: Optional[str] = Query(None, alias='to'), + current_user: User = Depends(authorize_roles('admin', 'staff', 'accountant')), + db: Session = Depends(get_db) +): + """Get Market Penetration Analysis""" + try: + start, end = AnalyticsService.parse_date_range(start_date, end_date) + result = AnalyticsService.get_market_penetration(db, start, end) + return {'status': 'success', 'data': result} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +# ==================== OPERATIONAL ANALYTICS ==================== + +@router.get('/operational/staff-performance') +async def get_staff_performance( + start_date: Optional[str] = Query(None, alias='from'), + end_date: Optional[str] = Query(None, alias='to'), + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get Staff Performance Metrics""" + try: + start, end = AnalyticsService.parse_date_range(start_date, end_date) + result = AnalyticsService.get_staff_performance(db, start, end) + return {'status': 'success', 'data': result} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/operational/service-usage') +async def get_service_usage_analytics( + start_date: Optional[str] = Query(None, alias='from'), + end_date: Optional[str] = Query(None, alias='to'), + current_user: User = Depends(authorize_roles('admin', 'staff', 'accountant')), + db: Session = Depends(get_db) +): + """Get Service Usage Analytics""" + try: + start, end = AnalyticsService.parse_date_range(start_date, end_date) + result = AnalyticsService.get_service_usage_analytics(db, start, end) + return {'status': 'success', 'data': result} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/operational/efficiency') +async def get_operational_efficiency( + start_date: Optional[str] = Query(None, alias='from'), + end_date: Optional[str] = Query(None, alias='to'), + current_user: User = Depends(authorize_roles('admin', 'staff', 'accountant')), + db: Session = Depends(get_db) +): + """Get Operational Efficiency Metrics""" + try: + start, end = AnalyticsService.parse_date_range(start_date, end_date) + result = AnalyticsService.get_operational_efficiency(db, start, end) + return {'status': 'success', 'data': result} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +# ==================== GUEST ANALYTICS ==================== + +@router.get('/guest/lifetime-value') +async def get_guest_lifetime_value( + start_date: Optional[str] = Query(None, alias='from'), + end_date: Optional[str] = Query(None, alias='to'), + current_user: User = Depends(authorize_roles('admin', 'staff', 'accountant')), + db: Session = Depends(get_db) +): + """Get Guest Lifetime Value Analysis""" + try: + start, end = AnalyticsService.parse_date_range(start_date, end_date) + result = AnalyticsService.get_guest_lifetime_value(db, start, end) + return {'status': 'success', 'data': result} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/guest/acquisition-cost') +async def get_customer_acquisition_cost( + start_date: Optional[str] = Query(None, alias='from'), + end_date: Optional[str] = Query(None, alias='to'), + current_user: User = Depends(authorize_roles('admin', 'staff', 'accountant')), + db: Session = Depends(get_db) +): + """Get Customer Acquisition Cost Analysis""" + try: + start, end = AnalyticsService.parse_date_range(start_date, end_date) + result = AnalyticsService.get_customer_acquisition_cost(db, start, end) + return {'status': 'success', 'data': result} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/guest/repeat-rate') +async def get_repeat_guest_rate( + start_date: Optional[str] = Query(None, alias='from'), + end_date: Optional[str] = Query(None, alias='to'), + current_user: User = Depends(authorize_roles('admin', 'staff', 'accountant')), + db: Session = Depends(get_db) +): + """Get Repeat Guest Rate""" + try: + start, end = AnalyticsService.parse_date_range(start_date, end_date) + result = AnalyticsService.get_repeat_guest_rate(db, start, end) + return {'status': 'success', 'data': result} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/guest/satisfaction-trends') +async def get_guest_satisfaction_trends( + start_date: Optional[str] = Query(None, alias='from'), + end_date: Optional[str] = Query(None, alias='to'), + current_user: User = Depends(authorize_roles('admin', 'staff', 'accountant')), + db: Session = Depends(get_db) +): + """Get Guest Satisfaction Trends""" + try: + start, end = AnalyticsService.parse_date_range(start_date, end_date) + result = AnalyticsService.get_guest_satisfaction_trends(db, start, end) + return {'status': 'success', 'data': result} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +# ==================== FINANCIAL ANALYTICS ==================== + +@router.get('/financial/profit-loss') +async def get_profit_loss( + start_date: Optional[str] = Query(None, alias='from'), + end_date: Optional[str] = Query(None, alias='to'), + current_user: User = Depends(authorize_roles('admin', 'accountant')), + db: Session = Depends(get_db) +): + """Get Profit & Loss Report""" + try: + start, end = AnalyticsService.parse_date_range(start_date, end_date) + result = AnalyticsService.get_profit_loss(db, start, end) + return {'status': 'success', 'data': result} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/financial/payment-methods') +async def get_payment_method_analytics( + start_date: Optional[str] = Query(None, alias='from'), + end_date: Optional[str] = Query(None, alias='to'), + current_user: User = Depends(authorize_roles('admin', 'staff', 'accountant')), + db: Session = Depends(get_db) +): + """Get Payment Method Analytics""" + try: + start, end = AnalyticsService.parse_date_range(start_date, end_date) + result = AnalyticsService.get_payment_method_analytics(db, start, end) + return {'status': 'success', 'data': result} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/financial/refunds') +async def get_refund_analysis( + start_date: Optional[str] = Query(None, alias='from'), + end_date: Optional[str] = Query(None, alias='to'), + current_user: User = Depends(authorize_roles('admin', 'accountant')), + db: Session = Depends(get_db) +): + """Get Refund Analysis""" + try: + start, end = AnalyticsService.parse_date_range(start_date, end_date) + result = AnalyticsService.get_refund_analysis(db, start, end) + return {'status': 'success', 'data': result} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +# ==================== COMPREHENSIVE ANALYTICS ==================== + +@router.get('/comprehensive') +async def get_comprehensive_analytics( + start_date: Optional[str] = Query(None, alias='from'), + end_date: Optional[str] = Query(None, alias='to'), + include_revenue: bool = Query(True), + include_operational: bool = Query(True), + include_guest: bool = Query(True), + include_financial: bool = Query(True), + current_user: User = Depends(authorize_roles('admin', 'staff', 'accountant')), + db: Session = Depends(get_db) +): + """Get Comprehensive Analytics across all categories""" + try: + result = AnalyticsService.get_comprehensive_analytics( + db, + start_date, + end_date, + include_revenue, + include_operational, + include_guest, + include_financial + ) + return {'status': 'success', 'data': result} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + diff --git a/Backend/src/routes/booking_routes.py b/Backend/src/routes/booking_routes.py index dc39c343..32593540 100644 --- a/Backend/src/routes/booking_routes.py +++ b/Backend/src/routes/booking_routes.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query -from sqlalchemy.orm import Session, joinedload, selectinload -from sqlalchemy import and_, or_ +from sqlalchemy.orm import Session, joinedload, selectinload, load_only +from sqlalchemy import and_, or_, func from typing import Optional from datetime import datetime import random @@ -22,6 +22,8 @@ from fastapi import Request from ..utils.mailer import send_email from ..utils.email_templates import booking_confirmation_email_template, booking_status_changed_email_template from ..services.loyalty_service import LoyaltyService +from ..utils.currency_helpers import get_currency_symbol +from ..utils.response_helpers import success_response router = APIRouter(prefix='/bookings', tags=['bookings']) def _generate_invoice_email_html(invoice: dict, is_proforma: bool=False) -> str: @@ -46,7 +48,20 @@ def calculate_booking_payment_balance(booking: Booking) -> dict: @router.get('/') async def get_all_bookings(search: Optional[str]=Query(None), status_filter: Optional[str]=Query(None, alias='status'), startDate: Optional[str]=Query(None), endDate: Optional[str]=Query(None), page: int=Query(1, ge=1), limit: int=Query(10, ge=1, le=100), current_user: User=Depends(authorize_roles('admin', 'staff')), db: Session=Depends(get_db)): try: - query = db.query(Booking).options(selectinload(Booking.payments), joinedload(Booking.user), joinedload(Booking.room).joinedload(Room.room_type)) + # Use load_only to exclude non-existent columns (rate_plan_id, group_booking_id) + query = db.query(Booking).options( + load_only( + Booking.id, Booking.booking_number, Booking.user_id, Booking.room_id, + Booking.check_in_date, Booking.check_out_date, Booking.num_guests, + Booking.total_price, Booking.original_price, Booking.discount_amount, + Booking.promotion_code, Booking.status, Booking.deposit_paid, + Booking.requires_deposit, Booking.special_requests, + Booking.created_at, Booking.updated_at + ), + selectinload(Booking.payments), + joinedload(Booking.user), + joinedload(Booking.room).joinedload(Room.room_type) + ) if search: query = query.filter(Booking.booking_number.like(f'%{search}%')) if status_filter: @@ -60,7 +75,8 @@ async def get_all_bookings(search: Optional[str]=Query(None), status_filter: Opt if endDate: end = datetime.fromisoformat(endDate.replace('Z', '+00:00')) query = query.filter(Booking.check_in_date <= end) - total = query.count() + # Use func.count() to avoid loading all columns (including non-existent rate_plan_id) + total = query.with_entities(func.count(Booking.id)).scalar() offset = (page - 1) * limit bookings = query.order_by(Booking.created_at.desc()).offset(offset).limit(limit).all() result = [] @@ -96,7 +112,9 @@ async def get_all_bookings(search: Optional[str]=Query(None), status_filter: Opt else: booking_dict['payments'] = [] result.append(booking_dict) - return {'status': 'success', 'data': {'bookings': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}} + return success_response( + data={'bookings': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}} + ) except Exception as e: import logging import traceback @@ -140,7 +158,7 @@ async def get_my_bookings(request: Request, current_user: User=Depends(get_curre else: booking_dict['payments'] = [] result.append(booking_dict) - return {'success': True, 'data': {'bookings': result}} + return success_response(data={'bookings': result}) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -190,9 +208,36 @@ async def create_booking(booking_data: dict, current_user: User=Depends(get_curr check_out = datetime.fromisoformat(check_out_date.replace('Z', '+00:00')) else: check_out = datetime.strptime(check_out_date, '%Y-%m-%d') + if check_in >= check_out: + raise HTTPException(status_code=400, detail='Check-out date must be after check-in date') overlapping = db.query(Booking).filter(and_(Booking.room_id == room_id, Booking.status != BookingStatus.cancelled, Booking.check_in_date < check_out, Booking.check_out_date > check_in)).first() if overlapping: raise HTTPException(status_code=409, detail='Room already booked for the selected dates') + + # Check for maintenance blocks + from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus + maintenance_block = db.query(RoomMaintenance).filter( + and_( + RoomMaintenance.room_id == room_id, + RoomMaintenance.blocks_room == True, + RoomMaintenance.status.in_([MaintenanceStatus.scheduled, MaintenanceStatus.in_progress]), + or_( + and_( + RoomMaintenance.block_start.isnot(None), + RoomMaintenance.block_end.isnot(None), + RoomMaintenance.block_start < check_out, + RoomMaintenance.block_end > check_in + ), + and_( + RoomMaintenance.scheduled_start < check_out, + RoomMaintenance.scheduled_end.isnot(None), + RoomMaintenance.scheduled_end > check_in + ) + ) + ) + ).first() + if maintenance_block: + raise HTTPException(status_code=409, detail=f'Room is blocked for maintenance: {maintenance_block.title}') booking_number = generate_booking_number() # Calculate room price @@ -330,6 +375,17 @@ async def create_booking(booking_data: dict, current_user: User=Depends(get_curr db.add(service_usage) db.commit() db.refresh(booking) + + # Send booking confirmation notification + try: + from ..services.notification_service import NotificationService + if booking.status == BookingStatus.confirmed: + NotificationService.send_booking_confirmation(db, booking) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.warning(f'Failed to send booking confirmation notification: {e}') + try: from ..services.invoice_service import InvoiceService from ..utils.mailer import send_email @@ -430,7 +486,8 @@ async def create_booking(booking_data: dict, current_user: User=Depends(get_curr booking_dict['room'] = {'id': booking.room.id, 'room_number': booking.room.room_number, 'floor': booking.room.floor} if booking.room.room_type: booking_dict['room']['room_type'] = {'id': booking.room.room_type.id, 'name': booking.room.room_type.name, 'base_price': float(booking.room.room_type.base_price) if booking.room.room_type.base_price else 0.0, 'capacity': booking.room.room_type.capacity} - return {'success': True, 'data': {'booking': booking_dict}, 'message': f'Booking created. Please pay {deposit_percentage}% deposit to confirm.' if requires_deposit else 'Booking created successfully'} + message = f'Booking created. Please pay {deposit_percentage}% deposit to confirm.' if requires_deposit else 'Booking created successfully' + return success_response(data={'booking': booking_dict}, message=message) except HTTPException: raise except Exception as e: @@ -449,7 +506,8 @@ async def get_booking_by_id(id: int, request: Request, current_user: User=Depend booking = db.query(Booking).options(selectinload(Booking.payments), selectinload(Booking.service_usages).selectinload(ServiceUsage.service), joinedload(Booking.user), joinedload(Booking.room).joinedload(Room.room_type)).filter(Booking.id == id).first() if not booking: raise HTTPException(status_code=404, detail='Booking not found') - if current_user.role_id != 1 and booking.user_id != current_user.id: + from ..utils.role_helpers import is_admin + if not is_admin(current_user, db) and booking.user_id != current_user.id: raise HTTPException(status_code=403, detail='Forbidden') import logging logger = logging.getLogger(__name__) @@ -497,7 +555,7 @@ async def get_booking_by_id(id: int, request: Request, current_user: User=Depend else: logger.info(f'Get booking {id} - No service_usages found, initializing empty array') booking_dict['service_usages'] = [] - return {'success': True, 'data': {'booking': booking_dict}} + return success_response(data={'booking': booking_dict}) except HTTPException: raise except Exception as e: @@ -513,10 +571,10 @@ async def cancel_booking(id: int, current_user: User=Depends(get_current_user), raise HTTPException(status_code=403, detail='Forbidden') if booking.status == BookingStatus.cancelled: raise HTTPException(status_code=400, detail='Booking already cancelled') - if booking.status == BookingStatus.confirmed: - raise HTTPException(status_code=400, detail='Cannot cancel a confirmed booking. Please contact support for assistance.') + # Customers can only cancel pending bookings + # Admin/Staff can cancel any booking via update_booking endpoint if booking.status != BookingStatus.pending: - raise HTTPException(status_code=400, detail=f'Cannot cancel booking with status: {booking.status.value}. Only pending bookings can be cancelled.') + raise HTTPException(status_code=400, detail=f'Cannot cancel booking with status: {booking.status.value}. Only pending bookings can be cancelled. Please contact support for assistance.') booking = db.query(Booking).options(selectinload(Booking.payments)).filter(Booking.id == id).first() payments_updated = False if booking.payments: @@ -536,7 +594,37 @@ async def cancel_booking(id: int, current_user: User=Depends(get_current_user), payment.notes = existing_notes + cancellation_note if existing_notes else cancellation_note.strip() payments_updated = True booking.status = BookingStatus.cancelled - if payments_updated > 0: + + # Update room status when booking is cancelled + if booking.room: + # Check if room has other active bookings + active_booking = db.query(Booking).filter( + and_( + Booking.room_id == booking.room_id, + Booking.id != booking.id, + Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in]), + Booking.check_in_date <= datetime.utcnow(), + Booking.check_out_date > datetime.utcnow() + ) + ).first() + + if not active_booking: + # Check for maintenance + from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus + active_maintenance = db.query(RoomMaintenance).filter( + and_( + RoomMaintenance.room_id == booking.room_id, + RoomMaintenance.blocks_room == True, + RoomMaintenance.status.in_([MaintenanceStatus.scheduled, MaintenanceStatus.in_progress]) + ) + ).first() + + if active_maintenance: + booking.room.status = RoomStatus.maintenance + else: + booking.room.status = RoomStatus.available + + if payments_updated: db.flush() db.commit() try: @@ -549,14 +637,14 @@ async def cancel_booking(id: int, current_user: User=Depends(get_current_user), import logging logger = logging.getLogger(__name__) logger.error(f'Failed to send cancellation email: {e}') - return {'success': True, 'data': {'booking': booking}} + return success_response(data={'booking': booking}) except HTTPException: raise except Exception as e: db.rollback() raise HTTPException(status_code=500, detail=str(e)) -@router.put('/{id}', dependencies=[Depends(authorize_roles('admin'))]) +@router.put('/{id}', dependencies=[Depends(authorize_roles('admin', 'staff'))]) async def update_booking(id: int, booking_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): try: booking = db.query(Booking).options( @@ -567,11 +655,36 @@ async def update_booking(id: int, booking_data: dict, current_user: User=Depends raise HTTPException(status_code=404, detail='Booking not found') old_status = booking.status status_value = booking_data.get('status') + room = booking.room + new_status = None if status_value: try: new_status = BookingStatus(status_value) booking.status = new_status - if new_status == BookingStatus.cancelled: + + # Update room status based on booking status + if new_status == BookingStatus.checked_in: + # Set room to occupied when checked in + if room and room.status != RoomStatus.maintenance: + room.status = RoomStatus.occupied + elif new_status == BookingStatus.checked_out: + # Set room to cleaning when checked out (housekeeping needed) + if room: + # Check if there's active maintenance + from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus + active_maintenance = db.query(RoomMaintenance).filter( + and_( + RoomMaintenance.room_id == room.id, + RoomMaintenance.blocks_room == True, + RoomMaintenance.status.in_([MaintenanceStatus.scheduled, MaintenanceStatus.in_progress]) + ) + ).first() + if active_maintenance: + room.status = RoomStatus.maintenance + else: + room.status = RoomStatus.cleaning + elif new_status == BookingStatus.cancelled: + # Update room status when booking is cancelled if booking.payments: for payment in booking.payments: if payment.payment_status == PaymentStatus.pending: @@ -580,10 +693,48 @@ async def update_booking(id: int, booking_data: dict, current_user: User=Depends cancellation_note = f'\nPayment cancelled due to booking cancellation on {datetime.utcnow().isoformat()}' payment.notes = existing_notes + cancellation_note if existing_notes else cancellation_note.strip() db.flush() + + # Check if room has other active bookings + if room: + active_booking = db.query(Booking).filter( + and_( + Booking.room_id == room.id, + Booking.id != booking.id, + Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in]), + Booking.check_in_date <= datetime.utcnow(), + Booking.check_out_date > datetime.utcnow() + ) + ).first() + + if not active_booking: + # Check for maintenance + from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus + active_maintenance = db.query(RoomMaintenance).filter( + and_( + RoomMaintenance.room_id == room.id, + RoomMaintenance.blocks_room == True, + RoomMaintenance.status.in_([MaintenanceStatus.scheduled, MaintenanceStatus.in_progress]) + ) + ).first() + + if active_maintenance: + room.status = RoomStatus.maintenance + else: + room.status = RoomStatus.available except ValueError: raise HTTPException(status_code=400, detail='Invalid status') db.commit() + # Send booking confirmation notification if status changed to confirmed + if new_status == BookingStatus.confirmed: + try: + from ..services.notification_service import NotificationService + NotificationService.send_booking_confirmation(db, booking) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.warning(f'Failed to send booking confirmation notification: {e}') + # Reload booking with all relationships after commit booking = db.query(Booking).options( selectinload(Booking.payments), @@ -610,8 +761,7 @@ async def update_booking(id: int, booking_data: dict, current_user: User=Depends room_type_name = room.room_type.name if room and room.room_type else 'Room' currency_setting = db.query(SystemSettings).filter(SystemSettings.key == 'platform_currency').first() currency = currency_setting.value if currency_setting and currency_setting.value else 'USD' - currency_symbols = {'USD': '$', 'EUR': '€', 'GBP': '£', 'JPY': '¥', 'CNY': '¥', 'KRW': '₩', 'SGD': 'S$', 'THB': '฿', 'AUD': 'A$', 'CAD': 'C$', 'VND': '₫', 'INR': '₹', 'CHF': 'CHF', 'NZD': 'NZ$'} - currency_symbol = currency_symbols.get(currency, currency) + currency_symbol = get_currency_symbol(currency) guest_name = booking.user.full_name if booking.user else 'Guest' guest_email = booking.user.email if booking.user else None email_html = booking_confirmation_email_template(booking_number=booking.booking_number, guest_name=guest_name, room_number=room.room_number if room else 'N/A', room_type=room_type_name, check_in=booking.check_in_date.strftime('%B %d, %Y') if booking.check_in_date else 'N/A', check_out=booking.check_out_date.strftime('%B %d, %Y') if booking.check_out_date else 'N/A', num_guests=booking.num_guests, total_price=float(booking.total_price), requires_deposit=booking.requires_deposit, deposit_amount=float(booking.total_price) * 0.2 if booking.requires_deposit else None, original_price=float(booking.original_price) if booking.original_price else None, discount_amount=float(booking.discount_amount) if booking.discount_amount else None, promotion_code=booking.promotion_code, client_url=client_url, currency_symbol=currency_symbol) @@ -673,10 +823,10 @@ async def update_booking(id: int, booking_data: dict, current_user: User=Depends 'status': booking.status.value if isinstance(booking.status, BookingStatus) else booking.status, } - response_data = {'status': 'success', 'message': 'Booking updated successfully', 'data': {'booking': booking_dict}} + message = 'Booking updated successfully. ⚠️ Payment reminder: Guest has remaining balance.' if payment_warning else 'Booking updated successfully' + response_data = success_response(data={'booking': booking_dict}, message=message) if payment_warning: response_data['warning'] = payment_warning - response_data['message'] = 'Booking updated successfully. ⚠️ Payment reminder: Guest has remaining balance.' return response_data except HTTPException: raise @@ -722,7 +872,7 @@ async def check_booking_by_number(booking_number: str, db: Session=Depends(get_d booking_dict['payments'] = [] payment_balance = calculate_booking_payment_balance(booking) booking_dict['payment_balance'] = {'total_paid': payment_balance['total_paid'], 'total_price': payment_balance['total_price'], 'remaining_balance': payment_balance['remaining_balance'], 'is_fully_paid': payment_balance['is_fully_paid'], 'payment_percentage': payment_balance['payment_percentage']} - response_data = {'status': 'success', 'data': {'booking': booking_dict}} + response_data = success_response(data={'booking': booking_dict}) if payment_balance['remaining_balance'] > 0.01: response_data['warning'] = {'message': f'Guest has not fully paid. Remaining balance: {payment_balance['remaining_balance']:.2f}', 'remaining_balance': payment_balance['remaining_balance'], 'payment_percentage': payment_balance['payment_percentage']} return response_data @@ -758,6 +908,8 @@ async def admin_create_booking(booking_data: dict, current_user: User=Depends(au check_out_date = booking_data.get('check_out_date') total_price = booking_data.get('total_price') guest_count = booking_data.get('guest_count', 1) + if guest_count < 1 or guest_count > 20: + raise HTTPException(status_code=400, detail='Guest count must be between 1 and 20') notes = booking_data.get('notes') payment_method = booking_data.get('payment_method', 'cash') payment_status = booking_data.get('payment_status', 'unpaid') # 'full', 'deposit', or 'unpaid' @@ -793,6 +945,10 @@ async def admin_create_booking(booking_data: dict, current_user: User=Depends(au else: check_out = datetime.strptime(check_out_date, '%Y-%m-%d') + # Validate dates + if check_in >= check_out: + raise HTTPException(status_code=400, detail='Check-out date must be after check-in date') + # Check for overlapping bookings overlapping = db.query(Booking).filter( and_( @@ -1118,11 +1274,10 @@ async def admin_create_booking(booking_data: dict, current_user: User=Depends(au 'capacity': booking.room.room_type.capacity } - return { - 'success': True, - 'data': {'booking': booking_dict}, - 'message': f'Booking created successfully by {current_user.full_name}' - } + return success_response( + data={'booking': booking_dict}, + message=f'Booking created successfully by {current_user.full_name}' + ) except HTTPException: raise except Exception as e: diff --git a/Backend/src/routes/email_campaign_routes.py b/Backend/src/routes/email_campaign_routes.py new file mode 100644 index 00000000..4110029e --- /dev/null +++ b/Backend/src/routes/email_campaign_routes.py @@ -0,0 +1,584 @@ +from fastapi import APIRouter, Depends, HTTPException, status, Query, Request +from sqlalchemy.orm import Session, selectinload +from typing import Optional, List, Union +from datetime import datetime +from pydantic import BaseModel, EmailStr, field_validator + +from ..config.database import get_db +from ..middleware.auth import get_current_user, authorize_roles +from ..models.user import User +from ..models.email_campaign import ( + Campaign, CampaignStatus, CampaignType, + CampaignSegment, EmailTemplate, CampaignEmail, EmailStatus, + DripSequence, DripSequenceStep, Unsubscribe +) +from ..services.email_campaign_service import email_campaign_service + +router = APIRouter(prefix="/email-campaigns", tags=["Email Campaigns"]) + +# Pydantic Models +class CampaignCreate(BaseModel): + name: str + subject: str + html_content: str + text_content: Optional[str] = None + campaign_type: str = "newsletter" + segment_id: Optional[Union[int, str]] = None + scheduled_at: Optional[datetime] = None + template_id: Optional[Union[int, str]] = None + from_name: Optional[str] = None + from_email: Optional[str] = None + reply_to_email: Optional[str] = None + track_opens: bool = True + track_clicks: bool = True + + @field_validator('segment_id', 'template_id', mode='before') + @classmethod + def parse_int_or_none(cls, v): + if v is None or v == '' or v == 'undefined' or (isinstance(v, str) and v.strip() == ''): + return None + if isinstance(v, str): + try: + return int(v) + except (ValueError, TypeError): + return None + if isinstance(v, int): + return v + return None + +class CampaignUpdate(BaseModel): + name: Optional[str] = None + subject: Optional[str] = None + html_content: Optional[str] = None + text_content: Optional[str] = None + segment_id: Optional[Union[int, str]] = None + scheduled_at: Optional[datetime] = None + status: Optional[str] = None + + @field_validator('segment_id', mode='before') + @classmethod + def parse_int_or_none(cls, v): + if v is None or v == '' or v == 'undefined' or (isinstance(v, str) and v.strip() == ''): + return None + if isinstance(v, str): + try: + return int(v) + except (ValueError, TypeError): + return None + if isinstance(v, int): + return v + return None + +class SegmentCreate(BaseModel): + name: str + description: Optional[str] = None + criteria: dict + +class TemplateCreate(BaseModel): + name: str + subject: str + html_content: str + text_content: Optional[str] = None + category: Optional[str] = None + variables: Optional[List[str]] = None + +class DripSequenceCreate(BaseModel): + name: str + description: Optional[str] = None + trigger_event: Optional[str] = None + +class DripStepCreate(BaseModel): + subject: str + html_content: str + text_content: Optional[str] = None + delay_days: int = 0 + delay_hours: int = 0 + template_id: Optional[Union[int, str]] = None + + @field_validator('template_id', mode='before') + @classmethod + def parse_int_or_none(cls, v): + if v is None or v == '' or v == 'undefined' or (isinstance(v, str) and v.strip() == ''): + return None + if isinstance(v, str): + try: + return int(v) + except (ValueError, TypeError): + return None + if isinstance(v, int): + return v + return None + +# Campaign Routes +@router.get("") +async def get_campaigns( + status_filter: Optional[str] = Query(None, alias='status'), + campaign_type: Optional[str] = Query(None), + limit: int = Query(50, ge=1, le=100), + offset: int = Query(0, ge=0), + current_user: User = Depends(authorize_roles("admin")), + db: Session = Depends(get_db) +): + """Get all email campaigns""" + query = db.query(Campaign) + + if status_filter: + try: + status_enum = CampaignStatus(status_filter) + query = query.filter(Campaign.status == status_enum) + except ValueError: + pass + + if campaign_type: + try: + type_enum = CampaignType(campaign_type) + query = query.filter(Campaign.campaign_type == type_enum) + except ValueError: + pass + + campaigns = query.order_by(Campaign.created_at.desc()).offset(offset).limit(limit).all() + + return [{ + "id": c.id, + "name": c.name, + "subject": c.subject, + "campaign_type": c.campaign_type.value, + "status": c.status.value, + "total_recipients": c.total_recipients, + "total_sent": c.total_sent, + "total_opened": c.total_opened, + "total_clicked": c.total_clicked, + "open_rate": float(c.open_rate) if c.open_rate else None, + "click_rate": float(c.click_rate) if c.click_rate else None, + "scheduled_at": c.scheduled_at.isoformat() if c.scheduled_at else None, + "sent_at": c.sent_at.isoformat() if c.sent_at else None, + "created_at": c.created_at.isoformat() if c.created_at else None + } for c in campaigns] + +# Segment Routes (must be before /{campaign_id} to avoid route conflicts) +@router.get("/segments") +async def get_segments( + current_user: User = Depends(authorize_roles("admin")), + db: Session = Depends(get_db) +): + """Get all campaign segments""" + try: + segments = db.query(CampaignSegment).filter(CampaignSegment.is_active == True).all() + return [{ + "id": s.id, + "name": s.name, + "description": s.description, + "criteria": s.criteria, + "estimated_count": s.estimated_count, + "created_at": s.created_at.isoformat() if s.created_at else None + } for s in segments] + except HTTPException: + raise + except Exception as e: + from ..config.logging_config import get_logger + logger = get_logger(__name__) + logger.error(f"Error fetching segments: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Failed to fetch segments: {str(e)}") + +@router.post("/segments") +async def create_segment( + data: SegmentCreate, + current_user: User = Depends(authorize_roles("admin")), + db: Session = Depends(get_db) +): + """Create a new campaign segment""" + try: + segment = email_campaign_service.create_segment( + db=db, + name=data.name, + criteria=data.criteria, + description=data.description, + created_by=current_user.id + ) + return {"status": "success", "segment_id": segment.id, "estimated_count": segment.estimated_count} + except HTTPException: + raise + except Exception as e: + from ..config.logging_config import get_logger + logger = get_logger(__name__) + logger.error(f"Error creating segment: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Failed to create segment: {str(e)}") + +# Template Routes (must be before /{campaign_id} to avoid route conflicts) +@router.get("/templates") +async def get_templates( + category: Optional[str] = Query(None, description="Filter by template category"), + current_user: User = Depends(authorize_roles("admin")), + db: Session = Depends(get_db) +): + """Get all email templates""" + try: + query = db.query(EmailTemplate).filter(EmailTemplate.is_active == True) + if category: + query = query.filter(EmailTemplate.category == category) + + templates = query.all() + result = [{ + "id": t.id, + "name": t.name, + "subject": t.subject, + "category": t.category, + "variables": t.variables, + "created_at": t.created_at.isoformat() if t.created_at else None + } for t in templates] + return result + except HTTPException: + raise + except Exception as e: + from ..config.logging_config import get_logger + logger = get_logger(__name__) + logger.error(f"Error fetching templates: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Failed to fetch templates: {str(e)}") + +@router.post("/templates") +async def create_template( + data: TemplateCreate, + current_user: User = Depends(authorize_roles("admin")), + db: Session = Depends(get_db) +): + """Create a new email template""" + try: + template = EmailTemplate( + name=data.name, + subject=data.subject, + html_content=data.html_content, + text_content=data.text_content, + category=data.category, + variables=data.variables, + created_by=current_user.id + ) + db.add(template) + db.commit() + db.refresh(template) + return {"status": "success", "template_id": template.id} + except HTTPException: + raise + except Exception as e: + from ..config.logging_config import get_logger + logger = get_logger(__name__) + logger.error(f"Error creating template: {str(e)}", exc_info=True) + db.rollback() + raise HTTPException(status_code=500, detail=f"Failed to create template: {str(e)}") + +# Drip Sequence Routes (must be before /{campaign_id} to avoid route conflicts) +@router.get("/drip-sequences") +async def get_drip_sequences( + current_user: User = Depends(authorize_roles("admin")), + db: Session = Depends(get_db) +): + """Get all drip sequences""" + try: + # Use eager loading to avoid lazy loading issues + sequences = db.query(DripSequence).options( + selectinload(DripSequence.steps) + ).filter(DripSequence.is_active == True).all() + + return [{ + "id": s.id, + "name": s.name, + "description": s.description, + "trigger_event": s.trigger_event, + "step_count": len(s.steps) if s.steps else 0, + "created_at": s.created_at.isoformat() if s.created_at else None + } for s in sequences] + except HTTPException: + raise + except Exception as e: + from ..config.logging_config import get_logger + logger = get_logger(__name__) + logger.error(f"Error fetching drip sequences: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Failed to fetch drip sequences: {str(e)}") + +@router.post("/drip-sequences") +async def create_drip_sequence( + data: DripSequenceCreate, + current_user: User = Depends(authorize_roles("admin")), + db: Session = Depends(get_db) +): + """Create a new drip sequence""" + try: + sequence = email_campaign_service.create_drip_sequence( + db=db, + name=data.name, + description=data.description, + trigger_event=data.trigger_event, + created_by=current_user.id + ) + return {"status": "success", "sequence_id": sequence.id} + except HTTPException: + raise + except Exception as e: + from ..config.logging_config import get_logger + logger = get_logger(__name__) + logger.error(f"Error creating drip sequence: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Failed to create drip sequence: {str(e)}") + +@router.post("/drip-sequences/{sequence_id}/steps") +async def add_drip_step( + sequence_id: int, + data: DripStepCreate, + current_user: User = Depends(authorize_roles("admin")), + db: Session = Depends(get_db) +): + """Add a step to a drip sequence""" + try: + # Ensure template_id is integer or None + template_id = int(data.template_id) if data.template_id is not None else None + + step = email_campaign_service.add_drip_step( + db=db, + sequence_id=sequence_id, + subject=data.subject, + html_content=data.html_content, + text_content=data.text_content, + delay_days=data.delay_days, + delay_hours=data.delay_hours, + template_id=template_id + ) + return {"status": "success", "step_id": step.id} + except HTTPException: + raise + except Exception as e: + from ..config.logging_config import get_logger + logger = get_logger(__name__) + logger.error(f"Error adding drip step: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Failed to add drip step: {str(e)}") + +@router.get("/{campaign_id}") +async def get_campaign( + campaign_id: int, + current_user: User = Depends(authorize_roles("admin")), + db: Session = Depends(get_db) +): + """Get a specific campaign""" + campaign = db.query(Campaign).filter(Campaign.id == campaign_id).first() + if not campaign: + raise HTTPException(status_code=404, detail="Campaign not found") + + return { + "id": campaign.id, + "name": campaign.name, + "subject": campaign.subject, + "html_content": campaign.html_content, + "text_content": campaign.text_content, + "campaign_type": campaign.campaign_type.value, + "status": campaign.status.value, + "segment_id": campaign.segment_id, + "scheduled_at": campaign.scheduled_at.isoformat() if campaign.scheduled_at else None, + "total_recipients": campaign.total_recipients, + "total_sent": campaign.total_sent, + "total_delivered": campaign.total_delivered, + "total_opened": campaign.total_opened, + "total_clicked": campaign.total_clicked, + "total_bounced": campaign.total_bounced, + "open_rate": float(campaign.open_rate) if campaign.open_rate else None, + "click_rate": float(campaign.click_rate) if campaign.click_rate else None, + "created_at": campaign.created_at.isoformat() if campaign.created_at else None + } + +@router.post("") +async def create_campaign( + data: CampaignCreate, + current_user: User = Depends(authorize_roles("admin")), + db: Session = Depends(get_db) +): + """Create a new email campaign""" + try: + campaign_type = CampaignType(data.campaign_type) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid campaign type") + + campaign = email_campaign_service.create_campaign( + db=db, + name=data.name, + subject=data.subject, + html_content=data.html_content, + text_content=data.text_content, + campaign_type=campaign_type, + segment_id=data.segment_id, + scheduled_at=data.scheduled_at, + template_id=data.template_id, + created_by=current_user.id, + from_name=data.from_name, + from_email=data.from_email, + reply_to_email=data.reply_to_email, + track_opens=data.track_opens, + track_clicks=data.track_clicks + ) + + return {"status": "success", "campaign_id": campaign.id} + +@router.put("/{campaign_id}") +async def update_campaign( + campaign_id: int, + data: CampaignUpdate, + current_user: User = Depends(authorize_roles("admin")), + db: Session = Depends(get_db) +): + """Update a campaign""" + campaign = db.query(Campaign).filter(Campaign.id == campaign_id).first() + if not campaign: + raise HTTPException(status_code=404, detail="Campaign not found") + + if data.name: + campaign.name = data.name + if data.subject: + campaign.subject = data.subject + if data.html_content: + campaign.html_content = data.html_content + if data.text_content is not None: + campaign.text_content = data.text_content + if data.segment_id is not None: + campaign.segment_id = int(data.segment_id) if isinstance(data.segment_id, str) else data.segment_id + if data.scheduled_at is not None: + campaign.scheduled_at = data.scheduled_at + if data.status: + try: + campaign.status = CampaignStatus(data.status) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid status") + + db.commit() + db.refresh(campaign) + + return {"status": "success", "message": "Campaign updated"} + +@router.post("/{campaign_id}/send") +async def send_campaign( + campaign_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Send an email campaign""" + try: + result = email_campaign_service.send_campaign(db=db, campaign_id=campaign_id) + return {"status": "success", "result": result} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to send campaign: {str(e)}") + +@router.get("/{campaign_id}/analytics") +async def get_campaign_analytics( + campaign_id: int, + current_user: User = Depends(authorize_roles("admin")), + db: Session = Depends(get_db) +): + """Get campaign analytics""" + campaign = db.query(Campaign).filter(Campaign.id == campaign_id).first() + if not campaign: + raise HTTPException(status_code=404, detail="Campaign not found") + + # Get email status breakdown + emails = db.query(CampaignEmail).filter(CampaignEmail.campaign_id == campaign_id).all() + + status_breakdown = {} + for status in EmailStatus: + status_breakdown[status.value] = len([e for e in emails if e.status == status]) + + return { + "campaign_id": campaign.id, + "total_recipients": campaign.total_recipients, + "total_sent": campaign.total_sent, + "total_delivered": campaign.total_delivered, + "total_opened": campaign.total_opened, + "total_clicked": campaign.total_clicked, + "total_bounced": campaign.total_bounced, + "total_unsubscribed": campaign.total_unsubscribed, + "open_rate": float(campaign.open_rate) if campaign.open_rate else 0, + "click_rate": float(campaign.click_rate) if campaign.click_rate else 0, + "bounce_rate": float(campaign.bounce_rate) if campaign.bounce_rate else 0, + "status_breakdown": status_breakdown + } + +# Tracking Routes (public endpoints for email tracking) +@router.get("/track/open/{campaign_email_id}") +async def track_email_open( + campaign_email_id: int, + db: Session = Depends(get_db) +): + """Track email open (called by tracking pixel)""" + email_campaign_service.track_email_open(db=db, campaign_email_id=campaign_email_id) + # Return 1x1 transparent pixel (GIF) + from fastapi.responses import Response + # 1x1 transparent GIF + pixel = b'\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\xff\xff\xff\x00\x00\x00\x21\xf9\x04\x01\x00\x00\x00\x00\x2c\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x04\x01\x00\x3b' + return Response(content=pixel, media_type="image/gif") + +@router.get("/track/click/{campaign_email_id}") +async def track_email_click( + campaign_email_id: int, + url: str = Query(...), + request: Request = None, + db: Session = Depends(get_db) +): + """Track email click""" + ip_address = request.client.host if request and request.client else None + user_agent = request.headers.get("User-Agent") if request else None + + email_campaign_service.track_email_click( + db=db, + campaign_email_id=campaign_email_id, + url=url, + ip_address=ip_address, + user_agent=user_agent + ) + + # Redirect to the actual URL + from fastapi.responses import RedirectResponse + return RedirectResponse(url=url) + +# Unsubscribe Routes +@router.post("/unsubscribe") +async def unsubscribe( + email: EmailStr = Query(...), + campaign_id: Optional[Union[int, str]] = Query(None), + unsubscribe_all: bool = Query(False), + reason: Optional[str] = None, + db: Session = Depends(get_db) +): + """Unsubscribe from email campaigns""" + # Parse campaign_id if it's a string + parsed_campaign_id = None + if campaign_id is not None and campaign_id != '' and campaign_id != 'undefined': + try: + parsed_campaign_id = int(campaign_id) if isinstance(campaign_id, str) else campaign_id + except (ValueError, TypeError): + parsed_campaign_id = None + + user = db.query(User).filter(User.email == email).first() + + unsubscribe_record = Unsubscribe( + email=email, + user_id=user.id if user else None, + campaign_id=parsed_campaign_id, + unsubscribe_all=unsubscribe_all, + reason=reason + ) + db.add(unsubscribe_record) + db.commit() + + return {"status": "success", "message": "Successfully unsubscribed"} + +@router.post("/drip-sequences/process") +async def process_drip_sequences( + current_user: User = Depends(authorize_roles("admin")), + db: Session = Depends(get_db) +): + """Manually trigger drip sequence processing""" + try: + email_campaign_service.process_drip_sequences(db=db) + return {"status": "success", "message": "Drip sequences processed"} + except HTTPException: + raise + except Exception as e: + from ..config.logging_config import get_logger + logger = get_logger(__name__) + logger.error(f"Error processing drip sequences: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Failed to process drip sequences: {str(e)}") + diff --git a/Backend/src/routes/group_booking_routes.py b/Backend/src/routes/group_booking_routes.py new file mode 100644 index 00000000..28bc6dfb --- /dev/null +++ b/Backend/src/routes/group_booking_routes.py @@ -0,0 +1,575 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session, joinedload, selectinload +from typing import Optional, List +from datetime import datetime +from decimal import Decimal + +from ..config.database import get_db +from ..middleware.auth import get_current_user, authorize_roles +from ..models.user import User +from ..models.role import Role +from ..models.group_booking import ( + GroupBooking, GroupBookingMember, GroupRoomBlock, GroupPayment, + GroupBookingStatus, PaymentOption +) +from ..models.room import Room +from ..models.room_type import RoomType +from ..services.group_booking_service import GroupBookingService +from ..services.room_service import get_base_url +from fastapi import Request + +router = APIRouter(prefix='/group-bookings', tags=['group-bookings']) + + +@router.post('/') +async def create_group_booking( + booking_data: dict, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Create a new group booking""" + try: + # Extract data + coordinator_name = booking_data.get('coordinator_name') or current_user.full_name + coordinator_email = booking_data.get('coordinator_email') or current_user.email + coordinator_phone = booking_data.get('coordinator_phone') or current_user.phone + + check_in_date = datetime.fromisoformat(booking_data['check_in_date'].replace('Z', '+00:00')) + check_out_date = datetime.fromisoformat(booking_data['check_out_date'].replace('Z', '+00:00')) + + room_blocks = booking_data.get('room_blocks', []) + if not room_blocks: + raise HTTPException(status_code=400, detail="At least one room block is required") + + payment_option = PaymentOption(booking_data.get('payment_option', 'coordinator_pays_all')) + deposit_required = booking_data.get('deposit_required', False) + deposit_percentage = booking_data.get('deposit_percentage') + + group_booking = GroupBookingService.create_group_booking( + db=db, + coordinator_id=current_user.id, + coordinator_name=coordinator_name, + coordinator_email=coordinator_email, + coordinator_phone=coordinator_phone, + check_in_date=check_in_date, + check_out_date=check_out_date, + room_blocks=room_blocks, + group_name=booking_data.get('group_name'), + group_type=booking_data.get('group_type'), + payment_option=payment_option, + deposit_required=deposit_required, + deposit_percentage=deposit_percentage, + special_requests=booking_data.get('special_requests'), + notes=booking_data.get('notes'), + cancellation_policy=booking_data.get('cancellation_policy'), + cancellation_deadline=datetime.fromisoformat(booking_data['cancellation_deadline'].replace('Z', '+00:00')) if booking_data.get('cancellation_deadline') else None, + cancellation_penalty_percentage=booking_data.get('cancellation_penalty_percentage'), + group_discount_percentage=booking_data.get('group_discount_percentage') + ) + + # Load relationships + db.refresh(group_booking) + group_booking = db.query(GroupBooking).options( + selectinload(GroupBooking.room_blocks).joinedload(GroupRoomBlock.room_type), + selectinload(GroupBooking.members), + selectinload(GroupBooking.coordinator) + ).filter(GroupBooking.id == group_booking.id).first() + + return { + 'status': 'success', + 'message': 'Group booking created successfully', + 'data': { + 'group_booking': _serialize_group_booking(group_booking) + } + } + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + import logging + import traceback + logger = logging.getLogger(__name__) + logger.error(f'Error creating group booking: {str(e)}') + logger.error(traceback.format_exc()) + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get('/') +async def get_group_bookings( + search: Optional[str] = Query(None), + status_filter: Optional[str] = Query(None, alias='status'), + page: int = Query(1, ge=1), + limit: int = Query(10, ge=1, le=100), + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get all group bookings (admin/staff only)""" + try: + query = db.query(GroupBooking).options( + selectinload(GroupBooking.room_blocks).joinedload(GroupRoomBlock.room_type), + selectinload(GroupBooking.members), + selectinload(GroupBooking.coordinator), + selectinload(GroupBooking.payments) + ) + + if search: + query = query.filter( + GroupBooking.group_booking_number.like(f'%{search}%') | + GroupBooking.group_name.like(f'%{search}%') | + GroupBooking.coordinator_name.like(f'%{search}%') + ) + + if status_filter: + try: + query = query.filter(GroupBooking.status == GroupBookingStatus(status_filter)) + except ValueError: + pass + + total = query.count() + offset = (page - 1) * limit + group_bookings = query.order_by(GroupBooking.created_at.desc()).offset(offset).limit(limit).all() + + return { + 'status': 'success', + 'data': { + 'group_bookings': [_serialize_group_booking(gb) for gb in group_bookings], + 'pagination': { + 'total': total, + 'page': page, + 'limit': limit, + 'totalPages': (total + limit - 1) // limit + } + } + } + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f'Error getting group bookings: {str(e)}') + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get('/me') +async def get_my_group_bookings( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Get group bookings for current user (as coordinator)""" + try: + group_bookings = db.query(GroupBooking).options( + selectinload(GroupBooking.room_blocks).joinedload(GroupRoomBlock.room_type), + selectinload(GroupBooking.members), + selectinload(GroupBooking.payments) + ).filter(GroupBooking.coordinator_id == current_user.id).order_by(GroupBooking.created_at.desc()).all() + + return { + 'status': 'success', + 'data': { + 'group_bookings': [_serialize_group_booking(gb) for gb in group_bookings] + } + } + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f'Error getting my group bookings: {str(e)}') + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get('/{group_booking_id}') +async def get_group_booking( + group_booking_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Get a specific group booking""" + try: + group_booking = db.query(GroupBooking).options( + selectinload(GroupBooking.room_blocks).joinedload(GroupRoomBlock.room_type), + selectinload(GroupBooking.members), + selectinload(GroupBooking.coordinator), + selectinload(GroupBooking.payments), + selectinload(GroupBooking.individual_bookings) + ).filter(GroupBooking.id == group_booking_id).first() + + if not group_booking: + raise HTTPException(status_code=404, detail="Group booking not found") + + # Check authorization + role = db.query(Role).filter(Role.id == current_user.role_id).first() + if role and role.name not in ['admin', 'staff']: + if group_booking.coordinator_id != current_user.id: + raise HTTPException(status_code=403, detail="Not authorized to view this group booking") + + return { + 'status': 'success', + 'data': { + 'group_booking': _serialize_group_booking(group_booking, detailed=True) + } + } + except HTTPException: + raise + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f'Error getting group booking: {str(e)}') + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post('/{group_booking_id}/members') +async def add_member( + group_booking_id: int, + member_data: dict, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Add a member to a group booking""" + try: + # Check authorization + group_booking = db.query(GroupBooking).filter(GroupBooking.id == group_booking_id).first() + if not group_booking: + raise HTTPException(status_code=404, detail="Group booking not found") + + role = db.query(Role).filter(Role.id == current_user.role_id).first() + if role and role.name not in ['admin', 'staff']: + if group_booking.coordinator_id != current_user.id: + raise HTTPException(status_code=403, detail="Not authorized to add members") + + member = GroupBookingService.add_member_to_group( + db=db, + group_booking_id=group_booking_id, + full_name=member_data.get('full_name'), + email=member_data.get('email'), + phone=member_data.get('phone'), + user_id=member_data.get('user_id'), + room_block_id=member_data.get('room_block_id'), + special_requests=member_data.get('special_requests'), + preferences=member_data.get('preferences') + ) + + db.refresh(member) + member = db.query(GroupBookingMember).options( + joinedload(GroupBookingMember.user), + joinedload(GroupBookingMember.room_block), + joinedload(GroupBookingMember.assigned_room) + ).filter(GroupBookingMember.id == member.id).first() + + return { + 'status': 'success', + 'message': 'Member added successfully', + 'data': { + 'member': _serialize_member(member) + } + } + except HTTPException: + raise + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f'Error adding member: {str(e)}') + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post('/{group_booking_id}/confirm') +async def confirm_group_booking( + group_booking_id: int, + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Confirm a group booking""" + try: + group_booking = GroupBookingService.confirm_group_booking(db, group_booking_id) + + return { + 'status': 'success', + 'message': 'Group booking confirmed successfully', + 'data': { + 'group_booking': _serialize_group_booking(group_booking) + } + } + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f'Error confirming group booking: {str(e)}') + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post('/{group_booking_id}/members/{member_id}/assign-room') +async def assign_room_to_member( + group_booking_id: int, + member_id: int, + assignment_data: dict, + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Assign a room to a group member and create individual booking""" + try: + room_id = assignment_data.get('room_id') + if not room_id: + raise HTTPException(status_code=400, detail="room_id is required") + + booking = GroupBookingService.create_individual_booking_from_member( + db=db, + member_id=member_id, + room_id=room_id + ) + + return { + 'status': 'success', + 'message': 'Room assigned and booking created successfully', + 'data': { + 'booking': { + 'id': booking.id, + 'booking_number': booking.booking_number, + 'room_id': booking.room_id, + 'status': booking.status.value if hasattr(booking.status, 'value') else str(booking.status) + } + } + } + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f'Error assigning room: {str(e)}') + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post('/{group_booking_id}/payments') +async def add_payment( + group_booking_id: int, + payment_data: dict, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Add a payment to a group booking""" + try: + # Check authorization + group_booking = db.query(GroupBooking).filter(GroupBooking.id == group_booking_id).first() + if not group_booking: + raise HTTPException(status_code=404, detail="Group booking not found") + + role = db.query(Role).filter(Role.id == current_user.role_id).first() + if role and role.name not in ['admin', 'staff']: + if group_booking.coordinator_id != current_user.id: + raise HTTPException(status_code=403, detail="Not authorized to add payments") + + payment = GroupBookingService.add_group_payment( + db=db, + group_booking_id=group_booking_id, + amount=Decimal(str(payment_data.get('amount'))), + payment_method=payment_data.get('payment_method'), + payment_type=payment_data.get('payment_type', 'deposit'), + transaction_id=payment_data.get('transaction_id'), + paid_by_member_id=payment_data.get('paid_by_member_id'), + paid_by_user_id=payment_data.get('paid_by_user_id', current_user.id), + notes=payment_data.get('notes') + ) + + return { + 'status': 'success', + 'message': 'Payment added successfully', + 'data': { + 'payment': _serialize_payment(payment) + } + } + except HTTPException: + raise + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f'Error adding payment: {str(e)}') + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post('/{group_booking_id}/cancel') +async def cancel_group_booking( + group_booking_id: int, + cancellation_data: dict, + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Cancel a group booking""" + try: + group_booking = GroupBookingService.cancel_group_booking( + db=db, + group_booking_id=group_booking_id, + cancellation_reason=cancellation_data.get('reason') + ) + + return { + 'status': 'success', + 'message': 'Group booking cancelled successfully', + 'data': { + 'group_booking': _serialize_group_booking(group_booking) + } + } + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f'Error cancelling group booking: {str(e)}') + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get('/{group_booking_id}/availability') +async def check_availability( + group_booking_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Check room availability for a group booking""" + try: + group_booking = db.query(GroupBooking).filter(GroupBooking.id == group_booking_id).first() + if not group_booking: + raise HTTPException(status_code=404, detail="Group booking not found") + + availability_results = [] + room_blocks = db.query(GroupRoomBlock).filter( + GroupRoomBlock.group_booking_id == group_booking_id + ).all() + + for room_block in room_blocks: + availability = GroupBookingService.check_room_availability( + db=db, + room_type_id=room_block.room_type_id, + check_in=group_booking.check_in_date, + check_out=group_booking.check_out_date, + num_rooms=room_block.rooms_blocked + ) + + availability_results.append({ + 'room_block_id': room_block.id, + 'room_type_id': room_block.room_type_id, + 'rooms_blocked': room_block.rooms_blocked, + 'availability': availability + }) + + return { + 'status': 'success', + 'data': { + 'availability': availability_results + } + } + except HTTPException: + raise + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f'Error checking availability: {str(e)}') + raise HTTPException(status_code=500, detail=str(e)) + + +# Helper functions for serialization +def _serialize_group_booking(group_booking: GroupBooking, detailed: bool = False) -> dict: + """Serialize group booking to dict""" + data = { + 'id': group_booking.id, + 'group_booking_number': group_booking.group_booking_number, + 'coordinator': { + 'id': group_booking.coordinator_id, + 'name': group_booking.coordinator_name, + 'email': group_booking.coordinator_email, + 'phone': group_booking.coordinator_phone + }, + 'group_name': group_booking.group_name, + 'group_type': group_booking.group_type, + 'total_rooms': group_booking.total_rooms, + 'total_guests': group_booking.total_guests, + 'check_in_date': group_booking.check_in_date.isoformat() if group_booking.check_in_date else None, + 'check_out_date': group_booking.check_out_date.isoformat() if group_booking.check_out_date else None, + 'base_rate_per_room': float(group_booking.base_rate_per_room) if group_booking.base_rate_per_room else 0.0, + 'group_discount_percentage': float(group_booking.group_discount_percentage) if group_booking.group_discount_percentage else 0.0, + 'group_discount_amount': float(group_booking.group_discount_amount) if group_booking.group_discount_amount else 0.0, + 'original_total_price': float(group_booking.original_total_price) if group_booking.original_total_price else 0.0, + 'discount_amount': float(group_booking.discount_amount) if group_booking.discount_amount else 0.0, + 'total_price': float(group_booking.total_price) if group_booking.total_price else 0.0, + 'payment_option': group_booking.payment_option.value if hasattr(group_booking.payment_option, 'value') else str(group_booking.payment_option), + 'deposit_required': group_booking.deposit_required, + 'deposit_percentage': group_booking.deposit_percentage, + 'deposit_amount': float(group_booking.deposit_amount) if group_booking.deposit_amount else None, + 'amount_paid': float(group_booking.amount_paid) if group_booking.amount_paid else 0.0, + 'balance_due': float(group_booking.balance_due) if group_booking.balance_due else 0.0, + 'status': group_booking.status.value if hasattr(group_booking.status, 'value') else str(group_booking.status), + 'special_requests': group_booking.special_requests, + 'notes': group_booking.notes, + 'created_at': group_booking.created_at.isoformat() if group_booking.created_at else None, + 'updated_at': group_booking.updated_at.isoformat() if group_booking.updated_at else None + } + + if detailed: + data['room_blocks'] = [_serialize_room_block(rb) for rb in group_booking.room_blocks] if group_booking.room_blocks else [] + data['members'] = [_serialize_member(m) for m in group_booking.members] if group_booking.members else [] + data['payments'] = [_serialize_payment(p) for p in group_booking.payments] if group_booking.payments else [] + data['cancellation_policy'] = group_booking.cancellation_policy + data['cancellation_deadline'] = group_booking.cancellation_deadline.isoformat() if group_booking.cancellation_deadline else None + data['cancellation_penalty_percentage'] = float(group_booking.cancellation_penalty_percentage) if group_booking.cancellation_penalty_percentage else None + data['confirmed_at'] = group_booking.confirmed_at.isoformat() if group_booking.confirmed_at else None + data['cancelled_at'] = group_booking.cancelled_at.isoformat() if group_booking.cancelled_at else None + + return data + + +def _serialize_room_block(room_block: GroupRoomBlock) -> dict: + """Serialize room block to dict""" + return { + 'id': room_block.id, + 'room_type_id': room_block.room_type_id, + 'room_type': { + 'id': room_block.room_type.id, + 'name': room_block.room_type.name, + 'base_price': float(room_block.room_type.base_price) if room_block.room_type.base_price else 0.0 + } if room_block.room_type else None, + 'rooms_blocked': room_block.rooms_blocked, + 'rooms_confirmed': room_block.rooms_confirmed, + 'rooms_available': room_block.rooms_available, + 'rate_per_room': float(room_block.rate_per_room) if room_block.rate_per_room else 0.0, + 'total_block_price': float(room_block.total_block_price) if room_block.total_block_price else 0.0, + 'is_active': room_block.is_active, + 'block_released_at': room_block.block_released_at.isoformat() if room_block.block_released_at else None + } + + +def _serialize_member(member: GroupBookingMember) -> dict: + """Serialize group member to dict""" + return { + 'id': member.id, + 'full_name': member.full_name, + 'email': member.email, + 'phone': member.phone, + 'user_id': member.user_id, + 'room_block_id': member.room_block_id, + 'assigned_room_id': member.assigned_room_id, + 'individual_booking_id': member.individual_booking_id, + 'special_requests': member.special_requests, + 'preferences': member.preferences, + 'individual_amount': float(member.individual_amount) if member.individual_amount else None, + 'individual_paid': float(member.individual_paid) if member.individual_paid else 0.0, + 'individual_balance': float(member.individual_balance) if member.individual_balance else 0.0, + 'is_checked_in': member.is_checked_in, + 'checked_in_at': member.checked_in_at.isoformat() if member.checked_in_at else None, + 'is_checked_out': member.is_checked_out, + 'checked_out_at': member.checked_out_at.isoformat() if member.checked_out_at else None + } + + +def _serialize_payment(payment: GroupPayment) -> dict: + """Serialize group payment to dict""" + return { + 'id': payment.id, + 'amount': float(payment.amount) if payment.amount else 0.0, + 'payment_method': payment.payment_method, + 'payment_type': payment.payment_type, + 'payment_status': payment.payment_status, + 'transaction_id': payment.transaction_id, + 'payment_date': payment.payment_date.isoformat() if payment.payment_date else None, + 'notes': payment.notes, + 'paid_by_member_id': payment.paid_by_member_id, + 'paid_by_user_id': payment.paid_by_user_id, + 'created_at': payment.created_at.isoformat() if payment.created_at else None + } + diff --git a/Backend/src/routes/guest_profile_routes.py b/Backend/src/routes/guest_profile_routes.py index 1e016352..56d68bee 100644 --- a/Backend/src/routes/guest_profile_routes.py +++ b/Backend/src/routes/guest_profile_routes.py @@ -10,6 +10,7 @@ from ..models.guest_tag import GuestTag from ..models.guest_communication import GuestCommunication, CommunicationType, CommunicationDirection from ..models.guest_segment import GuestSegment from ..services.guest_profile_service import GuestProfileService +from ..utils.role_helpers import is_customer import json router = APIRouter(prefix='/guest-profiles', tags=['guest-profiles']) @@ -88,8 +89,9 @@ async def get_guest_profile( if not user: raise HTTPException(status_code=404, detail=f'User with ID {user_id} not found') - # Check if user is a customer (role_id == 3) - if user.role_id != 3: + # Check if user is a customer + from ..utils.role_helpers import is_customer + if not is_customer(user, db): raise HTTPException(status_code=404, detail=f'User with ID {user_id} is not a guest (customer)') # Get analytics @@ -189,8 +191,8 @@ async def update_guest_preferences( ): """Update guest preferences""" try: - user = db.query(User).filter(User.id == user_id, User.role_id == 3).first() - if not user: + user = db.query(User).filter(User.id == user_id).first() + if not user or not is_customer(user, db): raise HTTPException(status_code=404, detail='Guest not found') preferences = db.query(GuestPreference).filter(GuestPreference.user_id == user_id).first() @@ -240,8 +242,8 @@ async def create_guest_note( ): """Create a note for a guest""" try: - user = db.query(User).filter(User.id == user_id, User.role_id == 3).first() - if not user: + user = db.query(User).filter(User.id == user_id).first() + if not user or not is_customer(user, db): raise HTTPException(status_code=404, detail='Guest not found') note = GuestNote( @@ -302,8 +304,8 @@ async def toggle_vip_status( ): """Toggle VIP status for a guest""" try: - user = db.query(User).filter(User.id == user_id, User.role_id == 3).first() - if not user: + user = db.query(User).filter(User.id == user_id).first() + if not user or not is_customer(user, db): raise HTTPException(status_code=404, detail='Guest not found') user.is_vip = vip_data.get('is_vip', False) @@ -327,8 +329,8 @@ async def add_tag_to_guest( ): """Add a tag to a guest""" try: - user = db.query(User).filter(User.id == user_id, User.role_id == 3).first() - if not user: + user = db.query(User).filter(User.id == user_id).first() + if not user or not is_customer(user, db): raise HTTPException(status_code=404, detail='Guest not found') tag_id = tag_data.get('tag_id') @@ -357,8 +359,8 @@ async def remove_tag_from_guest( ): """Remove a tag from a guest""" try: - user = db.query(User).filter(User.id == user_id, User.role_id == 3).first() - if not user: + user = db.query(User).filter(User.id == user_id).first() + if not user or not is_customer(user, db): raise HTTPException(status_code=404, detail='Guest not found') tag = db.query(GuestTag).filter(GuestTag.id == tag_id).first() @@ -386,8 +388,8 @@ async def create_communication( ): """Create a communication record""" try: - user = db.query(User).filter(User.id == user_id, User.role_id == 3).first() - if not user: + user = db.query(User).filter(User.id == user_id).first() + if not user or not is_customer(user, db): raise HTTPException(status_code=404, detail='Guest not found') comm = GuestCommunication( @@ -420,8 +422,8 @@ async def get_guest_analytics( ): """Get guest analytics""" try: - user = db.query(User).filter(User.id == user_id, User.role_id == 3).first() - if not user: + user = db.query(User).filter(User.id == user_id).first() + if not user or not is_customer(user, db): raise HTTPException(status_code=404, detail='Guest not found') analytics = GuestProfileService.get_guest_analytics(user_id, db) @@ -441,8 +443,8 @@ async def update_guest_metrics( ): """Update guest metrics (lifetime value, satisfaction score, etc.)""" try: - user = db.query(User).filter(User.id == user_id, User.role_id == 3).first() - if not user: + user = db.query(User).filter(User.id == user_id).first() + if not user or not is_customer(user, db): raise HTTPException(status_code=404, detail='Guest not found') metrics = GuestProfileService.update_guest_metrics(user_id, db) diff --git a/Backend/src/routes/invoice_routes.py b/Backend/src/routes/invoice_routes.py index baafc62a..5e108db7 100644 --- a/Backend/src/routes/invoice_routes.py +++ b/Backend/src/routes/invoice_routes.py @@ -8,14 +8,16 @@ from ..models.user import User from ..models.invoice import Invoice, InvoiceStatus from ..models.booking import Booking from ..services.invoice_service import InvoiceService +from ..utils.role_helpers import can_access_all_invoices, can_create_invoices +from ..utils.response_helpers import success_response router = APIRouter(prefix='/invoices', tags=['invoices']) @router.get('/') async def get_invoices(booking_id: Optional[int]=Query(None), status_filter: Optional[str]=Query(None, alias='status'), page: int=Query(1, ge=1), limit: int=Query(10, ge=1, le=100), current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): try: - user_id = None if current_user.role_id in [1, 4] else current_user.id # admin and accountant can see all invoices + user_id = None if can_access_all_invoices(current_user, db) else current_user.id result = InvoiceService.get_invoices(db=db, user_id=user_id, booking_id=booking_id, status=status_filter, page=page, limit=limit) - return {'status': 'success', 'data': result} + return success_response(data=result) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -25,9 +27,9 @@ async def get_invoice_by_id(id: int, current_user: User=Depends(get_current_user invoice = InvoiceService.get_invoice(id, db) if not invoice: raise HTTPException(status_code=404, detail='Invoice not found') - if current_user.role_id not in [1, 4] and invoice['user_id'] != current_user.id: # admin and accountant can see all invoices + if not can_access_all_invoices(current_user, db) and invoice['user_id'] != current_user.id: raise HTTPException(status_code=403, detail='Forbidden') - return {'status': 'success', 'data': {'invoice': invoice}} + return success_response(data={'invoice': invoice}) except HTTPException: raise except Exception as e: @@ -36,7 +38,7 @@ async def get_invoice_by_id(id: int, current_user: User=Depends(get_current_user @router.post('/') async def create_invoice(invoice_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): try: - if current_user.role_id not in [1, 2, 4]: # admin, staff, and accountant can create invoices + if not can_create_invoices(current_user, db): raise HTTPException(status_code=403, detail='Forbidden') booking_id = invoice_data.get('booking_id') if not booking_id: @@ -55,7 +57,7 @@ async def create_invoice(invoice_data: dict, current_user: User=Depends(get_curr invoice_notes = f'{promotion_note}\n{invoice_notes}'.strip() if invoice_notes else promotion_note invoice_kwargs['notes'] = invoice_notes invoice = InvoiceService.create_invoice_from_booking(booking_id=booking_id, db=db, created_by_id=current_user.id, tax_rate=invoice_data.get('tax_rate', 0.0), discount_amount=invoice_data.get('discount_amount', 0.0), due_days=invoice_data.get('due_days', 30), **invoice_kwargs) - return {'status': 'success', 'message': 'Invoice created successfully', 'data': {'invoice': invoice}} + return success_response(data={'invoice': invoice}, message='Invoice created successfully') except HTTPException: raise except ValueError as e: @@ -70,7 +72,7 @@ async def update_invoice(id: int, invoice_data: dict, current_user: User=Depends if not invoice: raise HTTPException(status_code=404, detail='Invoice not found') updated_invoice = InvoiceService.update_invoice(invoice_id=id, db=db, updated_by_id=current_user.id, **invoice_data) - return {'status': 'success', 'message': 'Invoice updated successfully', 'data': {'invoice': updated_invoice}} + return success_response(data={'invoice': updated_invoice}, message='Invoice updated successfully') except HTTPException: raise except ValueError as e: @@ -83,7 +85,7 @@ async def mark_invoice_as_paid(id: int, payment_data: dict, current_user: User=D try: amount = payment_data.get('amount') updated_invoice = InvoiceService.mark_invoice_as_paid(invoice_id=id, db=db, amount=amount, updated_by_id=current_user.id) - return {'status': 'success', 'message': 'Invoice marked as paid successfully', 'data': {'invoice': updated_invoice}} + return success_response(data={'invoice': updated_invoice}, message='Invoice marked as paid successfully') except HTTPException: raise except ValueError as e: @@ -99,7 +101,7 @@ async def delete_invoice(id: int, current_user: User=Depends(authorize_roles('ad raise HTTPException(status_code=404, detail='Invoice not found') db.delete(invoice) db.commit() - return {'status': 'success', 'message': 'Invoice deleted successfully'} + return success_response(message='Invoice deleted successfully') except HTTPException: raise except Exception as e: @@ -112,10 +114,10 @@ async def get_invoices_by_booking(booking_id: int, current_user: User=Depends(ge booking = db.query(Booking).filter(Booking.id == booking_id).first() if not booking: raise HTTPException(status_code=404, detail='Booking not found') - if current_user.role_id not in [1, 4] and booking.user_id != current_user.id: # admin and accountant can see all invoices + if not can_access_all_invoices(current_user, db) and booking.user_id != current_user.id: raise HTTPException(status_code=403, detail='Forbidden') result = InvoiceService.get_invoices(db=db, booking_id=booking_id) - return {'status': 'success', 'data': result} + return success_response(data=result) except HTTPException: raise except Exception as e: diff --git a/Backend/src/routes/notification_routes.py b/Backend/src/routes/notification_routes.py new file mode 100644 index 00000000..b560e5c6 --- /dev/null +++ b/Backend/src/routes/notification_routes.py @@ -0,0 +1,306 @@ +from fastapi import APIRouter, Depends, HTTPException, Query, Body +from sqlalchemy.orm import Session +from typing import Optional, List, Dict, Any +from ..config.database import get_db +from ..middleware.auth import authorize_roles, get_current_user +from ..models.user import User +from ..models.notification import NotificationChannel, NotificationStatus, NotificationType +from ..services.notification_service import NotificationService +from pydantic import BaseModel +from datetime import datetime + +router = APIRouter(prefix='/notifications', tags=['notifications']) + +# Request/Response Models +class NotificationSendRequest(BaseModel): + user_id: Optional[int] = None + notification_type: str + channel: str + content: str + subject: Optional[str] = None + template_id: Optional[int] = None + priority: Optional[str] = 'normal' + scheduled_at: Optional[str] = None + booking_id: Optional[int] = None + payment_id: Optional[int] = None + meta_data: Optional[Dict[str, Any]] = None + +class TemplateCreateRequest(BaseModel): + name: str + notification_type: str + channel: str + content: str + subject: Optional[str] = None + variables: Optional[List[str]] = None + +class PreferencesUpdateRequest(BaseModel): + email_enabled: Optional[bool] = None + sms_enabled: Optional[bool] = None + push_enabled: Optional[bool] = None + whatsapp_enabled: Optional[bool] = None + in_app_enabled: Optional[bool] = None + booking_confirmation_email: Optional[bool] = None + booking_confirmation_sms: Optional[bool] = None + payment_receipt_email: Optional[bool] = None + payment_receipt_sms: Optional[bool] = None + pre_arrival_reminder_email: Optional[bool] = None + pre_arrival_reminder_sms: Optional[bool] = None + check_in_reminder_email: Optional[bool] = None + check_in_reminder_sms: Optional[bool] = None + check_out_reminder_email: Optional[bool] = None + check_out_reminder_sms: Optional[bool] = None + marketing_campaign_email: Optional[bool] = None + marketing_campaign_sms: Optional[bool] = None + loyalty_update_email: Optional[bool] = None + loyalty_update_sms: Optional[bool] = None + system_alert_email: Optional[bool] = None + system_alert_push: Optional[bool] = None + +# Notifications +@router.get('/') +async def get_notifications( + user_id: Optional[int] = Query(None), + notification_type: Optional[str] = Query(None), + channel: Optional[str] = Query(None), + status: Optional[str] = Query(None), + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=1000), + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get notifications""" + try: + notifications = NotificationService.get_notifications( + db=db, + user_id=user_id, + notification_type=NotificationType(notification_type) if notification_type else None, + channel=NotificationChannel(channel) if channel else None, + status=NotificationStatus(status) if status else None, + skip=skip, + limit=limit + ) + return {'status': 'success', 'data': [{ + 'id': n.id, + 'user_id': n.user_id, + 'notification_type': n.notification_type.value, + 'channel': n.channel.value, + 'subject': n.subject, + 'content': n.content, + 'status': n.status.value, + 'priority': n.priority, + 'sent_at': n.sent_at.isoformat() if n.sent_at else None, + 'delivered_at': n.delivered_at.isoformat() if n.delivered_at else None, + 'read_at': n.read_at.isoformat() if n.read_at else None, + 'created_at': n.created_at.isoformat(), + } for n in notifications]} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/my-notifications') +async def get_my_notifications( + status: Optional[str] = Query(None), + skip: int = Query(0, ge=0), + limit: int = Query(50, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Get current user's notifications""" + try: + notifications = NotificationService.get_notifications( + db=db, + user_id=current_user.id, + status=NotificationStatus(status) if status else None, + skip=skip, + limit=limit + ) + return {'status': 'success', 'data': [{ + 'id': n.id, + 'notification_type': n.notification_type.value, + 'channel': n.channel.value, + 'subject': n.subject, + 'content': n.content, + 'status': n.status.value, + 'read_at': n.read_at.isoformat() if n.read_at else None, + 'created_at': n.created_at.isoformat(), + } for n in notifications]} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post('/send') +async def send_notification( + notification_data: NotificationSendRequest, + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Send notification""" + try: + scheduled_at = None + if notification_data.scheduled_at: + scheduled_at = datetime.fromisoformat(notification_data.scheduled_at.replace('Z', '+00:00')) + + notification = NotificationService.send_notification( + db=db, + user_id=notification_data.user_id, + notification_type=NotificationType(notification_data.notification_type), + channel=NotificationChannel(notification_data.channel), + content=notification_data.content, + subject=notification_data.subject, + template_id=notification_data.template_id, + priority=notification_data.priority or 'normal', + scheduled_at=scheduled_at, + booking_id=notification_data.booking_id, + payment_id=notification_data.payment_id, + meta_data=notification_data.meta_data + ) + return {'status': 'success', 'data': { + 'id': notification.id, + 'status': notification.status.value, + 'created_at': notification.created_at.isoformat() + }} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post('/{notification_id}/read') +async def mark_notification_read( + notification_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Mark notification as read""" + try: + notification = NotificationService.mark_as_read(db, notification_id, current_user.id) + if not notification: + raise HTTPException(status_code=404, detail='Notification not found') + + return {'status': 'success', 'data': { + 'id': notification.id, + 'status': notification.status.value, + 'read_at': notification.read_at.isoformat() if notification.read_at else None + }} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +# Templates +@router.post('/templates') +async def create_template( + template_data: TemplateCreateRequest, + current_user: User = Depends(authorize_roles('admin')), + db: Session = Depends(get_db) +): + """Create notification template""" + try: + template = NotificationService.create_template( + db=db, + name=template_data.name, + notification_type=NotificationType(template_data.notification_type), + channel=NotificationChannel(template_data.channel), + content=template_data.content, + created_by=current_user.id, + subject=template_data.subject, + variables=template_data.variables + ) + return {'status': 'success', 'data': { + 'id': template.id, + 'name': template.name, + 'created_at': template.created_at.isoformat() + }} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/templates') +async def get_templates( + notification_type: Optional[str] = Query(None), + channel: Optional[str] = Query(None), + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get notification templates""" + try: + from ..models.notification import NotificationTemplate + query = db.query(NotificationTemplate) + + if notification_type: + query = query.filter(NotificationTemplate.notification_type == NotificationType(notification_type)) + if channel: + query = query.filter(NotificationTemplate.channel == NotificationChannel(channel)) + + templates = query.filter(NotificationTemplate.is_active == True).all() + return {'status': 'success', 'data': [{ + 'id': t.id, + 'name': t.name, + 'notification_type': t.notification_type.value, + 'channel': t.channel.value, + 'subject': t.subject, + 'content': t.content, + 'variables': t.variables, + } for t in templates]} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +# Preferences +@router.get('/preferences') +async def get_preferences( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Get user notification preferences""" + try: + preferences = NotificationService.get_user_preferences(db, current_user.id) + return {'status': 'success', 'data': { + 'email_enabled': preferences.email_enabled, + 'sms_enabled': preferences.sms_enabled, + 'push_enabled': preferences.push_enabled, + 'whatsapp_enabled': preferences.whatsapp_enabled, + 'in_app_enabled': preferences.in_app_enabled, + 'booking_confirmation_email': preferences.booking_confirmation_email, + 'booking_confirmation_sms': preferences.booking_confirmation_sms, + 'payment_receipt_email': preferences.payment_receipt_email, + 'payment_receipt_sms': preferences.payment_receipt_sms, + 'pre_arrival_reminder_email': preferences.pre_arrival_reminder_email, + 'pre_arrival_reminder_sms': preferences.pre_arrival_reminder_sms, + 'check_in_reminder_email': preferences.check_in_reminder_email, + 'check_in_reminder_sms': preferences.check_in_reminder_sms, + 'check_out_reminder_email': preferences.check_out_reminder_email, + 'check_out_reminder_sms': preferences.check_out_reminder_sms, + 'marketing_campaign_email': preferences.marketing_campaign_email, + 'marketing_campaign_sms': preferences.marketing_campaign_sms, + 'loyalty_update_email': preferences.loyalty_update_email, + 'loyalty_update_sms': preferences.loyalty_update_sms, + 'system_alert_email': preferences.system_alert_email, + 'system_alert_push': preferences.system_alert_push, + }} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.put('/preferences') +async def update_preferences( + preferences_data: PreferencesUpdateRequest, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Update user notification preferences""" + try: + prefs_dict = preferences_data.dict(exclude_unset=True) + preferences = NotificationService.update_user_preferences(db, current_user.id, prefs_dict) + return {'status': 'success', 'data': { + 'email_enabled': preferences.email_enabled, + 'sms_enabled': preferences.sms_enabled, + 'push_enabled': preferences.push_enabled, + 'whatsapp_enabled': preferences.whatsapp_enabled, + 'in_app_enabled': preferences.in_app_enabled, + }} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + diff --git a/Backend/src/routes/package_routes.py b/Backend/src/routes/package_routes.py new file mode 100644 index 00000000..4b8fcb48 --- /dev/null +++ b/Backend/src/routes/package_routes.py @@ -0,0 +1,435 @@ +from fastapi import APIRouter, Depends, HTTPException, status, Query +from sqlalchemy.orm import Session +from sqlalchemy import or_, and_ +from typing import Optional, List +from datetime import datetime, date +from decimal import Decimal +from ..config.database import get_db +from ..middleware.auth import get_current_user, authorize_roles +from ..models.user import User +from ..models.package import Package, PackageItem, PackageStatus, PackageItemType +from ..models.rate_plan import RatePlan +from ..models.room_type import RoomType +from pydantic import BaseModel + +router = APIRouter(prefix='/packages', tags=['packages']) + +# Pydantic models +class PackageItemCreate(BaseModel): + item_type: str + item_id: Optional[int] = None + item_name: str + item_description: Optional[str] = None + quantity: int = 1 + unit: Optional[str] = None + price: Optional[float] = None + included: bool = True + price_modifier: Optional[float] = None + display_order: int = 0 + extra_data: Optional[dict] = None + +class PackageCreate(BaseModel): + name: str + code: str + description: Optional[str] = None + status: str = 'active' + base_price: Optional[float] = None + price_modifier: float = 1.0 + discount_percentage: Optional[float] = None + room_type_id: Optional[int] = None + min_nights: Optional[int] = None + max_nights: Optional[int] = None + valid_from: Optional[str] = None + valid_to: Optional[str] = None + image_url: Optional[str] = None + highlights: Optional[List[str]] = None + terms_conditions: Optional[str] = None + extra_data: Optional[dict] = None + items: Optional[List[PackageItemCreate]] = [] + +class PackageUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + status: Optional[str] = None + base_price: Optional[float] = None + price_modifier: Optional[float] = None + discount_percentage: Optional[float] = None + room_type_id: Optional[int] = None + min_nights: Optional[int] = None + max_nights: Optional[int] = None + valid_from: Optional[str] = None + valid_to: Optional[str] = None + image_url: Optional[str] = None + highlights: Optional[List[str]] = None + terms_conditions: Optional[str] = None + extra_data: Optional[dict] = None + +@router.get('/') +async def get_packages( + search: Optional[str] = Query(None), + status_filter: Optional[str] = Query(None, alias='status'), + room_type_id: Optional[int] = Query(None), + page: int = Query(1, ge=1), + limit: int = Query(10, ge=1, le=100), + db: Session = Depends(get_db) +): + try: + query = db.query(Package) + + if search: + query = query.filter( + or_( + Package.name.like(f'%{search}%'), + Package.code.like(f'%{search}%'), + Package.description.like(f'%{search}%') + ) + ) + + if status_filter: + try: + query = query.filter(Package.status == PackageStatus(status_filter)) + except ValueError: + pass + + if room_type_id: + query = query.filter( + or_( + Package.room_type_id == room_type_id, + Package.room_type_id.is_(None) + ) + ) + + total = query.count() + offset = (page - 1) * limit + packages = query.order_by(Package.created_at.desc()).offset(offset).limit(limit).all() + + result = [] + for pkg in packages: + pkg_dict = { + 'id': pkg.id, + 'name': pkg.name, + 'code': pkg.code, + 'description': pkg.description, + 'status': pkg.status.value if isinstance(pkg.status, PackageStatus) else pkg.status, + 'base_price': float(pkg.base_price) if pkg.base_price else None, + 'price_modifier': float(pkg.price_modifier) if pkg.price_modifier else 1.0, + 'discount_percentage': float(pkg.discount_percentage) if pkg.discount_percentage else None, + 'room_type_id': pkg.room_type_id, + 'room_type_name': pkg.room_type.name if pkg.room_type else None, + 'min_nights': pkg.min_nights, + 'max_nights': pkg.max_nights, + 'valid_from': pkg.valid_from.isoformat() if pkg.valid_from else None, + 'valid_to': pkg.valid_to.isoformat() if pkg.valid_to else None, + 'image_url': pkg.image_url, + 'highlights': pkg.highlights, + 'terms_conditions': pkg.terms_conditions, + 'extra_data': pkg.extra_data, + 'created_at': pkg.created_at.isoformat() if pkg.created_at else None, + 'updated_at': pkg.updated_at.isoformat() if pkg.updated_at else None, + } + result.append(pkg_dict) + + return { + 'status': 'success', + 'data': { + 'packages': result, + 'pagination': { + 'total': total, + 'page': page, + 'limit': limit, + 'totalPages': (total + limit - 1) // limit + } + } + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/{id}') +async def get_package(id: int, db: Session = Depends(get_db)): + try: + pkg = db.query(Package).filter(Package.id == id).first() + if not pkg: + raise HTTPException(status_code=404, detail='Package not found') + + items = db.query(PackageItem).filter(PackageItem.package_id == id).order_by(PackageItem.display_order.asc()).all() + + pkg_dict = { + 'id': pkg.id, + 'name': pkg.name, + 'code': pkg.code, + 'description': pkg.description, + 'status': pkg.status.value if isinstance(pkg.status, PackageStatus) else pkg.status, + 'base_price': float(pkg.base_price) if pkg.base_price else None, + 'price_modifier': float(pkg.price_modifier) if pkg.price_modifier else 1.0, + 'discount_percentage': float(pkg.discount_percentage) if pkg.discount_percentage else None, + 'room_type_id': pkg.room_type_id, + 'room_type_name': pkg.room_type.name if pkg.room_type else None, + 'min_nights': pkg.min_nights, + 'max_nights': pkg.max_nights, + 'valid_from': pkg.valid_from.isoformat() if pkg.valid_from else None, + 'valid_to': pkg.valid_to.isoformat() if pkg.valid_to else None, + 'image_url': pkg.image_url, + 'highlights': pkg.highlights, + 'terms_conditions': pkg.terms_conditions, + 'extra_data': pkg.extra_data, + 'items': [ + { + 'id': item.id, + 'item_type': item.item_type.value if isinstance(item.item_type, PackageItemType) else item.item_type, + 'item_id': item.item_id, + 'item_name': item.item_name, + 'item_description': item.item_description, + 'quantity': item.quantity, + 'unit': item.unit, + 'price': float(item.price) if item.price else None, + 'included': item.included, + 'price_modifier': float(item.price_modifier) if item.price_modifier else None, + 'display_order': item.display_order, + 'extra_data': item.extra_data, + } + for item in items + ], + 'created_at': pkg.created_at.isoformat() if pkg.created_at else None, + 'updated_at': pkg.updated_at.isoformat() if pkg.updated_at else None, + } + + return {'status': 'success', 'data': {'package': pkg_dict}} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post('/', dependencies=[Depends(authorize_roles('admin'))]) +async def create_package(package_data: PackageCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + try: + # Check if code already exists + existing = db.query(Package).filter(Package.code == package_data.code).first() + if existing: + raise HTTPException(status_code=400, detail='Package code already exists') + + # Validate room_type_id if provided + if package_data.room_type_id: + room_type = db.query(RoomType).filter(RoomType.id == package_data.room_type_id).first() + if not room_type: + raise HTTPException(status_code=404, detail='Room type not found') + + # Create package + pkg = Package( + name=package_data.name, + code=package_data.code, + description=package_data.description, + status=PackageStatus(package_data.status), + base_price=Decimal(str(package_data.base_price)) if package_data.base_price else None, + price_modifier=Decimal(str(package_data.price_modifier)), + discount_percentage=Decimal(str(package_data.discount_percentage)) if package_data.discount_percentage else None, + room_type_id=package_data.room_type_id, + min_nights=package_data.min_nights, + max_nights=package_data.max_nights, + valid_from=datetime.strptime(package_data.valid_from, '%Y-%m-%d').date() if package_data.valid_from else None, + valid_to=datetime.strptime(package_data.valid_to, '%Y-%m-%d').date() if package_data.valid_to else None, + image_url=package_data.image_url, + highlights=package_data.highlights, + terms_conditions=package_data.terms_conditions, + extra_data=package_data.extra_data, + ) + + db.add(pkg) + db.flush() + + # Create items + if package_data.items: + for item_data in package_data.items: + item = PackageItem( + package_id=pkg.id, + item_type=PackageItemType(item_data.item_type), + item_id=item_data.item_id, + item_name=item_data.item_name, + item_description=item_data.item_description, + quantity=item_data.quantity, + unit=item_data.unit, + price=Decimal(str(item_data.price)) if item_data.price else None, + included=item_data.included, + price_modifier=Decimal(str(item_data.price_modifier)) if item_data.price_modifier else None, + display_order=item_data.display_order, + extra_data=item_data.extra_data, + ) + db.add(item) + + db.commit() + db.refresh(pkg) + + return {'status': 'success', 'message': 'Package created successfully', 'data': {'package_id': pkg.id}} + except HTTPException: + raise + except ValueError as e: + raise HTTPException(status_code=400, detail=f'Invalid enum value: {str(e)}') + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + +@router.put('/{id}', dependencies=[Depends(authorize_roles('admin'))]) +async def update_package(id: int, package_data: PackageUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + try: + pkg = db.query(Package).filter(Package.id == id).first() + if not pkg: + raise HTTPException(status_code=404, detail='Package not found') + + # Update fields + if package_data.name is not None: + pkg.name = package_data.name + if package_data.description is not None: + pkg.description = package_data.description + if package_data.status is not None: + pkg.status = PackageStatus(package_data.status) + if package_data.base_price is not None: + pkg.base_price = Decimal(str(package_data.base_price)) if package_data.base_price else None + if package_data.price_modifier is not None: + pkg.price_modifier = Decimal(str(package_data.price_modifier)) + if package_data.discount_percentage is not None: + pkg.discount_percentage = Decimal(str(package_data.discount_percentage)) if package_data.discount_percentage else None + if package_data.room_type_id is not None: + if package_data.room_type_id: + room_type = db.query(RoomType).filter(RoomType.id == package_data.room_type_id).first() + if not room_type: + raise HTTPException(status_code=404, detail='Room type not found') + pkg.room_type_id = package_data.room_type_id + if package_data.min_nights is not None: + pkg.min_nights = package_data.min_nights + if package_data.max_nights is not None: + pkg.max_nights = package_data.max_nights + if package_data.valid_from is not None: + pkg.valid_from = datetime.strptime(package_data.valid_from, '%Y-%m-%d').date() if package_data.valid_from else None + if package_data.valid_to is not None: + pkg.valid_to = datetime.strptime(package_data.valid_to, '%Y-%m-%d').date() if package_data.valid_to else None + if package_data.image_url is not None: + pkg.image_url = package_data.image_url + if package_data.highlights is not None: + pkg.highlights = package_data.highlights + if package_data.terms_conditions is not None: + pkg.terms_conditions = package_data.terms_conditions + if package_data.extra_data is not None: + pkg.extra_data = package_data.extra_data + + db.commit() + + return {'status': 'success', 'message': 'Package updated successfully'} + except HTTPException: + raise + except ValueError as e: + raise HTTPException(status_code=400, detail=f'Invalid enum value: {str(e)}') + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + +@router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))]) +async def delete_package(id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + try: + pkg = db.query(Package).filter(Package.id == id).first() + if not pkg: + raise HTTPException(status_code=404, detail='Package not found') + + # Check if package is used in rate plans + rate_plan_count = db.query(RatePlan).filter(RatePlan.package_id == id).count() + if rate_plan_count > 0: + raise HTTPException(status_code=400, detail=f'Cannot delete package. It is used in {rate_plan_count} rate plan(s)') + + # Delete items first + db.query(PackageItem).filter(PackageItem.package_id == id).delete() + + db.delete(pkg) + db.commit() + + return {'status': 'success', 'message': 'Package deleted successfully'} + except HTTPException: + raise + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/available/{room_type_id}') +async def get_available_packages( + room_type_id: int, + check_in: str = Query(...), + check_out: str = Query(...), + num_nights: Optional[int] = Query(None), + db: Session = Depends(get_db) +): + """Get available packages for a room type and date range""" + try: + check_in_date = datetime.strptime(check_in, '%Y-%m-%d').date() + check_out_date = datetime.strptime(check_out, '%Y-%m-%d').date() + + if num_nights is None: + num_nights = (check_out_date - check_in_date).days + + # Query packages + query = db.query(Package).filter( + Package.status == PackageStatus.active, + or_( + Package.room_type_id == room_type_id, + Package.room_type_id.is_(None) + ) + ) + + # Filter by date range + query = query.filter( + or_( + Package.valid_from.is_(None), + Package.valid_from <= check_in_date + ), + or_( + Package.valid_to.is_(None), + Package.valid_to >= check_out_date + ) + ) + + # Filter by nights + query = query.filter( + or_( + Package.min_nights.is_(None), + Package.min_nights <= num_nights + ), + or_( + Package.max_nights.is_(None), + Package.max_nights >= num_nights + ) + ) + + packages = query.order_by(Package.created_at.desc()).all() + + result = [] + for pkg in packages: + items = db.query(PackageItem).filter(PackageItem.package_id == pkg.id).order_by(PackageItem.display_order.asc()).all() + + pkg_dict = { + 'id': pkg.id, + 'name': pkg.name, + 'code': pkg.code, + 'description': pkg.description, + 'base_price': float(pkg.base_price) if pkg.base_price else None, + 'price_modifier': float(pkg.price_modifier) if pkg.price_modifier else 1.0, + 'discount_percentage': float(pkg.discount_percentage) if pkg.discount_percentage else None, + 'image_url': pkg.image_url, + 'highlights': pkg.highlights, + 'items': [ + { + 'id': item.id, + 'item_type': item.item_type.value if isinstance(item.item_type, PackageItemType) else item.item_type, + 'item_name': item.item_name, + 'item_description': item.item_description, + 'quantity': item.quantity, + 'unit': item.unit, + 'price': float(item.price) if item.price else None, + 'included': item.included, + } + for item in items + ], + } + result.append(pkg_dict) + + return {'status': 'success', 'data': {'packages': result}} + except ValueError as e: + raise HTTPException(status_code=400, detail=f'Invalid date format: {str(e)}') + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + diff --git a/Backend/src/routes/payment_routes.py b/Backend/src/routes/payment_routes.py index 16334154..daa28189 100644 --- a/Backend/src/routes/payment_routes.py +++ b/Backend/src/routes/payment_routes.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query, Request, Header -from sqlalchemy.orm import Session, joinedload, selectinload +from sqlalchemy.orm import Session, joinedload, selectinload, load_only from typing import Optional from datetime import datetime import os @@ -9,10 +9,14 @@ from ..middleware.auth import get_current_user, authorize_roles from ..models.user import User from ..models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus from ..models.booking import Booking, BookingStatus +from ..utils.role_helpers import can_access_all_payments +from ..utils.currency_helpers import get_currency_symbol +from ..utils.response_helpers import success_response from ..utils.mailer import send_email from ..utils.email_templates import payment_confirmation_email_template, booking_status_changed_email_template from ..services.stripe_service import StripeService from ..services.paypal_service import PayPalService +from ..services.borica_service import BoricaService from ..services.loyalty_service import LoyaltyService router = APIRouter(prefix='/payments', tags=['payments']) @@ -20,7 +24,18 @@ async def cancel_booking_on_payment_failure(booking: Booking, db: Session, reaso if booking.status == BookingStatus.cancelled: return from sqlalchemy.orm import selectinload - booking = db.query(Booking).options(selectinload(Booking.payments)).filter(Booking.id == booking.id).first() + # Use load_only to exclude non-existent columns (rate_plan_id, group_booking_id) + booking = db.query(Booking).options( + load_only( + Booking.id, Booking.booking_number, Booking.user_id, Booking.room_id, + Booking.check_in_date, Booking.check_out_date, Booking.num_guests, + Booking.total_price, Booking.original_price, Booking.discount_amount, + Booking.promotion_code, Booking.status, Booking.deposit_paid, + Booking.requires_deposit, Booking.special_requests, + Booking.created_at, Booking.updated_at + ), + selectinload(Booking.payments) + ).filter(Booking.id == booking.id).first() if booking.payments: for payment in booking.payments: if payment.payment_status == PaymentStatus.pending: @@ -55,10 +70,23 @@ async def get_payments(booking_id: Optional[int]=Query(None), status_filter: Opt query = query.filter(Payment.payment_status == PaymentStatus(status_filter)) except ValueError: pass - if current_user.role_id not in [1, 4]: # admin and accountant can see all payments + if not can_access_all_payments(current_user, db): query = query.join(Booking).filter(Booking.user_id == current_user.id) total = query.count() - query = query.options(selectinload(Payment.booking).selectinload(Booking.user)) + # Use load_only to exclude non-existent columns (rate_plan_id, group_booking_id) + query = query.options( + selectinload(Payment.booking).options( + load_only( + Booking.id, Booking.booking_number, Booking.user_id, Booking.room_id, + Booking.check_in_date, Booking.check_out_date, Booking.num_guests, + Booking.total_price, Booking.original_price, Booking.discount_amount, + Booking.promotion_code, Booking.status, Booking.deposit_paid, + Booking.requires_deposit, Booking.special_requests, + Booking.created_at, Booking.updated_at + ), + joinedload(Booking.user) + ) + ) offset = (page - 1) * limit payments = query.order_by(Payment.created_at.desc()).offset(offset).limit(limit).all() result = [] @@ -69,7 +97,7 @@ async def get_payments(booking_id: Optional[int]=Query(None), status_filter: Opt if payment.booking.user: payment_dict['booking']['user'] = {'id': payment.booking.user.id, 'name': payment.booking.user.full_name, 'full_name': payment.booking.user.full_name, 'email': payment.booking.user.email} result.append(payment_dict) - return {'status': 'success', 'data': {'payments': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}} + return success_response(data={'payments': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}) except HTTPException: raise except Exception as e: @@ -81,12 +109,26 @@ async def get_payments(booking_id: Optional[int]=Query(None), status_filter: Opt @router.get('/booking/{booking_id}') async def get_payments_by_booking_id(booking_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): try: + from ..utils.role_helpers import is_admin booking = db.query(Booking).filter(Booking.id == booking_id).first() if not booking: raise HTTPException(status_code=404, detail='Booking not found') - if current_user.role_id != 1 and booking.user_id != current_user.id: + if not is_admin(current_user, db) and booking.user_id != current_user.id: raise HTTPException(status_code=403, detail='Forbidden') - payments = db.query(Payment).options(joinedload(Payment.booking).joinedload(Booking.user)).filter(Payment.booking_id == booking_id).order_by(Payment.created_at.desc()).all() + # Use load_only to exclude non-existent columns (rate_plan_id, group_booking_id) + payments = db.query(Payment).options( + joinedload(Payment.booking).options( + load_only( + Booking.id, Booking.booking_number, Booking.user_id, Booking.room_id, + Booking.check_in_date, Booking.check_out_date, Booking.num_guests, + Booking.total_price, Booking.original_price, Booking.discount_amount, + Booking.promotion_code, Booking.status, Booking.deposit_paid, + Booking.requires_deposit, Booking.special_requests, + Booking.created_at, Booking.updated_at + ), + joinedload(Booking.user) + ) + ).filter(Payment.booking_id == booking_id).order_by(Payment.created_at.desc()).all() result = [] for payment in payments: payment_dict = {'id': payment.id, 'booking_id': payment.booking_id, 'amount': float(payment.amount) if payment.amount else 0.0, 'payment_method': payment.payment_method.value if isinstance(payment.payment_method, PaymentMethod) else payment.payment_method, 'payment_type': payment.payment_type.value if isinstance(payment.payment_type, PaymentType) else payment.payment_type, 'deposit_percentage': payment.deposit_percentage, 'related_payment_id': payment.related_payment_id, 'payment_status': payment.payment_status.value if isinstance(payment.payment_status, PaymentStatus) else payment.payment_status, 'transaction_id': payment.transaction_id, 'payment_date': payment.payment_date.isoformat() if payment.payment_date else None, 'notes': payment.notes, 'created_at': payment.created_at.isoformat() if payment.created_at else None} @@ -95,7 +137,7 @@ async def get_payments_by_booking_id(booking_id: int, current_user: User=Depends if payment.booking.user: payment_dict['booking']['user'] = {'id': payment.booking.user.id, 'name': payment.booking.user.full_name, 'full_name': payment.booking.user.full_name, 'email': payment.booking.user.email} result.append(payment_dict) - return {'status': 'success', 'data': {'payments': result}} + return success_response(data={'payments': result}) except HTTPException: raise except Exception as e: @@ -107,13 +149,13 @@ async def get_payment_by_id(id: int, current_user: User=Depends(get_current_user payment = db.query(Payment).filter(Payment.id == id).first() if not payment: raise HTTPException(status_code=404, detail='Payment not found') - if current_user.role_id not in [1, 4]: # admin and accountant can see all payments + if not can_access_all_payments(current_user, db): if payment.booking and payment.booking.user_id != current_user.id: raise HTTPException(status_code=403, detail='Forbidden') payment_dict = {'id': payment.id, 'booking_id': payment.booking_id, 'amount': float(payment.amount) if payment.amount else 0.0, 'payment_method': payment.payment_method.value if isinstance(payment.payment_method, PaymentMethod) else payment.payment_method, 'payment_type': payment.payment_type.value if isinstance(payment.payment_type, PaymentType) else payment.payment_type, 'deposit_percentage': payment.deposit_percentage, 'related_payment_id': payment.related_payment_id, 'payment_status': payment.payment_status.value if isinstance(payment.payment_status, PaymentStatus) else payment.payment_status, 'transaction_id': payment.transaction_id, 'payment_date': payment.payment_date.isoformat() if payment.payment_date else None, 'notes': payment.notes, 'created_at': payment.created_at.isoformat() if payment.created_at else None} if payment.booking: payment_dict['booking'] = {'id': payment.booking.id, 'booking_number': payment.booking.booking_number} - return {'status': 'success', 'data': {'payment': payment_dict}} + return success_response(data={'payment': payment_dict}) except HTTPException: raise except Exception as e: @@ -129,7 +171,8 @@ async def create_payment(payment_data: dict, current_user: User=Depends(get_curr booking = db.query(Booking).filter(Booking.id == booking_id).first() if not booking: raise HTTPException(status_code=404, detail='Booking not found') - if current_user.role_id != 1 and booking.user_id != current_user.id: + from ..utils.role_helpers import is_admin + if not is_admin(current_user, db) and booking.user_id != current_user.id: raise HTTPException(status_code=403, detail='Forbidden') payment = Payment(booking_id=booking_id, amount=amount, payment_method=PaymentMethod(payment_method), payment_type=PaymentType(payment_type), payment_status=PaymentStatus.pending, payment_date=datetime.utcnow() if payment_data.get('mark_as_paid') else None, notes=payment_data.get('notes')) if payment_data.get('mark_as_paid'): @@ -139,6 +182,16 @@ async def create_payment(payment_data: dict, current_user: User=Depends(get_curr db.commit() db.refresh(payment) + # Send payment receipt notification + if payment.payment_status == PaymentStatus.completed: + try: + from ..services.notification_service import NotificationService + NotificationService.send_payment_receipt(db, payment) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.warning(f'Failed to send payment receipt notification: {e}') + # Award loyalty points if payment completed and booking is confirmed if payment.payment_status == PaymentStatus.completed and booking: try: @@ -168,15 +221,14 @@ async def create_payment(payment_data: dict, current_user: User=Depends(get_curr client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173') currency_setting = db.query(SystemSettings).filter(SystemSettings.key == 'platform_currency').first() currency = currency_setting.value if currency_setting and currency_setting.value else 'USD' - currency_symbols = {'USD': '$', 'EUR': '€', 'GBP': '£', 'JPY': '¥', 'CNY': '¥', 'KRW': '₩', 'SGD': 'S$', 'THB': '฿', 'AUD': 'A$', 'CAD': 'C$', 'VND': '₫', 'INR': '₹', 'CHF': 'CHF', 'NZD': 'NZ$'} - currency_symbol = currency_symbols.get(currency, currency) + currency_symbol = get_currency_symbol(currency) email_html = payment_confirmation_email_template(booking_number=booking.booking_number, guest_name=booking.user.full_name, amount=float(payment.amount), payment_method=payment.payment_method.value if isinstance(payment.payment_method, PaymentMethod) else str(payment.payment_method), transaction_id=payment.transaction_id, payment_type=payment.payment_type.value if payment.payment_type else None, total_price=float(booking.total_price), client_url=client_url, currency_symbol=currency_symbol) await send_email(to=booking.user.email, subject=f'Payment Confirmed - {booking.booking_number}', html=email_html) except Exception as e: import logging logger = logging.getLogger(__name__) logger.error(f'Failed to send payment confirmation email: {e}') - return {'status': 'success', 'message': 'Payment created successfully', 'data': {'payment': payment}} + return success_response(data={'payment': payment}, message='Payment created successfully') except HTTPException: raise except Exception as e: @@ -195,10 +247,30 @@ async def update_payment_status(id: int, status_data: dict, current_user: User=D try: new_status = PaymentStatus(status_value) payment.payment_status = new_status + # Only cancel booking if it's a full refund or all payments are failed if new_status in [PaymentStatus.failed, PaymentStatus.refunded]: - booking = db.query(Booking).filter(Booking.id == payment.booking_id).first() + # Use load_only to exclude non-existent columns (rate_plan_id, group_booking_id) + booking = db.query(Booking).options( + load_only( + Booking.id, Booking.booking_number, Booking.user_id, Booking.room_id, + Booking.check_in_date, Booking.check_out_date, Booking.num_guests, + Booking.total_price, Booking.original_price, Booking.discount_amount, + Booking.promotion_code, Booking.status, Booking.deposit_paid, + Booking.requires_deposit, Booking.special_requests, + Booking.created_at, Booking.updated_at + ), + selectinload(Booking.payments) + ).filter(Booking.id == payment.booking_id).first() if booking and booking.status != BookingStatus.cancelled: - await cancel_booking_on_payment_failure(booking, db, reason=f'Payment {new_status.value}') + # Check if this is a full refund or if all payments are failed + total_paid = sum(float(p.amount) for p in booking.payments if p.payment_status == PaymentStatus.completed) + total_price = float(booking.total_price) if booking.total_price else 0.0 + all_payments_failed = all(p.payment_status in [PaymentStatus.failed, PaymentStatus.refunded] for p in booking.payments) + is_full_refund = new_status == PaymentStatus.refunded and float(payment.amount) >= total_price + + # Only cancel if it's a full refund or all payments failed + if is_full_refund or (new_status == PaymentStatus.failed and all_payments_failed): + await cancel_booking_on_payment_failure(booking, db, reason=f'Payment {new_status.value}') except ValueError: raise HTTPException(status_code=400, detail='Invalid payment status') if status_data.get('transaction_id'): @@ -209,22 +281,29 @@ async def update_payment_status(id: int, status_data: dict, current_user: User=D db.commit() db.refresh(payment) if payment.payment_status == PaymentStatus.completed and old_status != PaymentStatus.completed: + # Send payment receipt notification + try: + from ..services.notification_service import NotificationService + NotificationService.send_payment_receipt(db, payment) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.warning(f'Failed to send payment receipt notification: {e}') + try: from ..models.system_settings import SystemSettings client_url_setting = db.query(SystemSettings).filter(SystemSettings.key == 'client_url').first() client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173') currency_setting = db.query(SystemSettings).filter(SystemSettings.key == 'platform_currency').first() currency = currency_setting.value if currency_setting and currency_setting.value else 'USD' - currency_symbols = {'USD': '$', 'EUR': '€', 'GBP': '£', 'JPY': '¥', 'CNY': '¥', 'KRW': '₩', 'SGD': 'S$', 'THB': '฿', 'AUD': 'A$', 'CAD': 'C$', 'VND': '₫', 'INR': '₹', 'CHF': 'CHF', 'NZD': 'NZ$'} - currency_symbol = currency_symbols.get(currency, currency) + currency_symbol = get_currency_symbol(currency) payment = db.query(Payment).filter(Payment.id == id).first() if payment.booking and payment.booking.user: client_url_setting = db.query(SystemSettings).filter(SystemSettings.key == 'client_url').first() client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173') currency_setting = db.query(SystemSettings).filter(SystemSettings.key == 'platform_currency').first() currency = currency_setting.value if currency_setting and currency_setting.value else 'USD' - currency_symbols = {'USD': '$', 'EUR': '€', 'GBP': '£', 'JPY': '¥', 'CNY': '¥', 'KRW': '₩', 'SGD': 'S$', 'THB': '฿', 'AUD': 'A$', 'CAD': 'C$', 'VND': '₫', 'INR': '₹', 'CHF': 'CHF', 'NZD': 'NZ$'} - currency_symbol = currency_symbols.get(currency, currency) + currency_symbol = get_currency_symbol(currency) email_html = payment_confirmation_email_template(booking_number=payment.booking.booking_number, guest_name=payment.booking.user.full_name, amount=float(payment.amount), payment_method=payment.payment_method.value if isinstance(payment.payment_method, PaymentMethod) else str(payment.payment_method), transaction_id=payment.transaction_id, client_url=client_url, currency_symbol=currency_symbol) await send_email(to=payment.booking.user.email, subject=f'Payment Confirmed - {payment.booking.booking_number}', html=email_html) if payment.payment_type == PaymentType.deposit and payment.booking: @@ -240,7 +319,7 @@ async def update_payment_status(id: int, status_data: dict, current_user: User=D db.commit() except Exception as e: print(f'Failed to send payment confirmation email: {e}') - return {'status': 'success', 'message': 'Payment status updated successfully', 'data': {'payment': payment}} + return success_response(data={'payment': payment}, message='Payment status updated successfully') except HTTPException: raise except Exception as e: @@ -270,7 +349,8 @@ async def create_stripe_payment_intent(intent_data: dict, current_user: User=Dep booking = db.query(Booking).filter(Booking.id == booking_id).first() if not booking: raise HTTPException(status_code=404, detail='Booking not found') - if current_user.role_id != 1 and booking.user_id != current_user.id: + from ..utils.role_helpers import is_admin + if not is_admin(current_user, db) and booking.user_id != current_user.id: raise HTTPException(status_code=403, detail='Forbidden') if booking.requires_deposit and (not booking.deposit_paid): deposit_payment = db.query(Payment).filter(Payment.booking_id == booking_id, Payment.payment_type == PaymentType.deposit, Payment.payment_status == PaymentStatus.pending).order_by(Payment.created_at.desc()).first() @@ -294,7 +374,7 @@ async def create_stripe_payment_intent(intent_data: dict, current_user: User=Dep logger = logging.getLogger(__name__) logger.error('Payment intent created but client_secret is missing') raise HTTPException(status_code=500, detail='Failed to create payment intent. Client secret is missing.') - return {'status': 'success', 'message': 'Payment intent created successfully', 'data': {'client_secret': intent['client_secret'], 'payment_intent_id': intent['id'], 'publishable_key': publishable_key}} + return success_response(data={'client_secret': intent['client_secret'], 'payment_intent_id': intent['id'], 'publishable_key': publishable_key}, message='Payment intent created successfully') except HTTPException: raise except ValueError as e: @@ -330,15 +410,14 @@ async def confirm_stripe_payment(payment_data: dict, current_user: User=Depends( client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173') currency_setting = db.query(SystemSettings).filter(SystemSettings.key == 'platform_currency').first() currency = currency_setting.value if currency_setting and currency_setting.value else 'USD' - currency_symbols = {'USD': '$', 'EUR': '€', 'GBP': '£', 'JPY': '¥', 'CNY': '¥', 'KRW': '₩', 'SGD': 'S$', 'THB': '฿', 'AUD': 'A$', 'CAD': 'C$', 'VND': '₫', 'INR': '₹', 'CHF': 'CHF', 'NZD': 'NZ$'} - currency_symbol = currency_symbols.get(currency, currency) + currency_symbol = get_currency_symbol(currency) email_html = payment_confirmation_email_template(booking_number=booking.booking_number, guest_name=booking.user.full_name, amount=payment['amount'], payment_method='stripe', transaction_id=payment['transaction_id'], payment_type=payment.get('payment_type'), total_price=float(booking.total_price), client_url=client_url, currency_symbol=currency_symbol) await send_email(to=booking.user.email, subject=f'Payment Confirmed - {booking.booking_number}', html=email_html) except Exception as e: import logging logger = logging.getLogger(__name__) logger.warning(f'Failed to send payment confirmation email: {e}') - return {'status': 'success', 'message': 'Payment confirmed successfully', 'data': {'payment': payment, 'booking': {'id': booking.id if booking else None, 'booking_number': booking.booking_number if booking else None, 'status': booking.status.value if booking else None}}} + return success_response(data={'payment': payment, 'booking': {'id': booking.id if booking else None, 'booking_number': booking.booking_number if booking else None, 'status': booking.status.value if booking else None}}, message='Payment confirmed successfully') except HTTPException: db.rollback() raise @@ -369,7 +448,7 @@ async def stripe_webhook(request: Request, db: Session=Depends(get_db)): if not signature: raise HTTPException(status_code=400, detail='Missing stripe-signature header') result = await StripeService.handle_webhook(payload=payload, signature=signature, db=db) - return {'status': 'success', 'data': result} + return success_response(data=result) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: @@ -395,10 +474,11 @@ async def create_paypal_order(order_data: dict, current_user: User=Depends(get_c raise HTTPException(status_code=400, detail='booking_id and amount are required') if amount > 100000: raise HTTPException(status_code=400, detail=f"Amount ${amount:,.2f} exceeds PayPal's maximum of $100,000. Please contact support for large payments.") + from ..utils.role_helpers import is_admin booking = db.query(Booking).filter(Booking.id == booking_id).first() if not booking: raise HTTPException(status_code=404, detail='Booking not found') - if current_user.role_id != 1 and booking.user_id != current_user.id: + if not is_admin(current_user, db) and booking.user_id != current_user.id: raise HTTPException(status_code=403, detail='Forbidden') if booking.requires_deposit and (not booking.deposit_paid): deposit_payment = db.query(Payment).filter(Payment.booking_id == booking_id, Payment.payment_type == PaymentType.deposit, Payment.payment_status == PaymentStatus.pending).order_by(Payment.created_at.desc()).first() @@ -415,7 +495,7 @@ async def create_paypal_order(order_data: dict, current_user: User=Depends(get_c order = PayPalService.create_order(amount=amount, currency=currency, metadata={'booking_id': str(booking_id), 'booking_number': booking.booking_number, 'user_id': str(current_user.id), 'description': f'Hotel Booking Payment - {booking.booking_number}', 'return_url': return_url, 'cancel_url': cancel_url}, db=db) if not order.get('approval_url'): raise HTTPException(status_code=500, detail='Failed to create PayPal order. Approval URL is missing.') - return {'status': 'success', 'message': 'PayPal order created successfully', 'data': {'order_id': order['id'], 'approval_url': order['approval_url'], 'status': order['status']}} + return success_response(data={'order_id': order['id'], 'approval_url': order['approval_url'], 'status': order['status']}, message='PayPal order created successfully') except HTTPException: raise except ValueError as e: @@ -445,7 +525,7 @@ async def cancel_paypal_payment(payment_data: dict, current_user: User=Depends(g booking = db.query(Booking).filter(Booking.id == booking_id).first() if booking and booking.status != BookingStatus.cancelled: await cancel_booking_on_payment_failure(booking, db, reason='PayPal payment canceled by user') - return {'status': 'success', 'message': 'Payment canceled and booking cancelled'} + return success_response(message='Payment canceled and booking cancelled') except HTTPException: db.rollback() raise @@ -475,15 +555,14 @@ async def capture_paypal_payment(payment_data: dict, current_user: User=Depends( client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173') currency_setting = db.query(SystemSettings).filter(SystemSettings.key == 'platform_currency').first() currency = currency_setting.value if currency_setting and currency_setting.value else 'USD' - currency_symbols = {'USD': '$', 'EUR': '€', 'GBP': '£', 'JPY': '¥', 'CNY': '¥', 'KRW': '₩', 'SGD': 'S$', 'THB': '฿', 'AUD': 'A$', 'CAD': 'C$', 'VND': '₫', 'INR': '₹', 'CHF': 'CHF', 'NZD': 'NZ$'} - currency_symbol = currency_symbols.get(currency, currency) + currency_symbol = get_currency_symbol(currency) email_html = payment_confirmation_email_template(booking_number=booking.booking_number, guest_name=booking.user.full_name, amount=payment['amount'], payment_method='paypal', transaction_id=payment['transaction_id'], payment_type=payment.get('payment_type'), total_price=float(booking.total_price), client_url=client_url, currency_symbol=currency_symbol) await send_email(to=booking.user.email, subject=f'Payment Confirmed - {booking.booking_number}', html=email_html) except Exception as e: import logging logger = logging.getLogger(__name__) logger.warning(f'Failed to send payment confirmation email: {e}') - return {'status': 'success', 'message': 'Payment confirmed successfully', 'data': {'payment': payment, 'booking': {'id': booking.id if booking else None, 'booking_number': booking.booking_number if booking else None, 'status': booking.status.value if booking else None}}} + return success_response(data={'payment': payment, 'booking': {'id': booking.id if booking else None, 'booking_number': booking.booking_number if booking else None, 'status': booking.status.value if booking else None}}, message='Payment confirmed successfully') except HTTPException: db.rollback() raise @@ -498,4 +577,174 @@ async def capture_paypal_payment(payment_data: dict, current_user: User=Depends( logger = logging.getLogger(__name__) logger.error(f'Unexpected error confirming PayPal payment: {str(e)}', exc_info=True) db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + +@router.post('/borica/create-payment') +async def create_borica_payment(payment_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): + try: + from ..services.borica_service import get_borica_terminal_id, get_borica_merchant_id + terminal_id = get_borica_terminal_id(db) + merchant_id = get_borica_merchant_id(db) + if not terminal_id or not merchant_id: + if not settings.BORICA_TERMINAL_ID or not settings.BORICA_MERCHANT_ID: + raise HTTPException(status_code=500, detail='Borica is not configured. Please configure Borica settings in Admin Panel or set BORICA_TERMINAL_ID and BORICA_MERCHANT_ID environment variables.') + booking_id = payment_data.get('booking_id') + amount = float(payment_data.get('amount', 0)) + currency = payment_data.get('currency', 'BGN') + if not booking_id or amount <= 0: + raise HTTPException(status_code=400, detail='booking_id and amount are required') + if amount > 100000: + raise HTTPException(status_code=400, detail=f"Amount {amount:,.2f} exceeds maximum of 100,000. Please contact support for large payments.") + from ..utils.role_helpers import is_admin + booking = db.query(Booking).filter(Booking.id == booking_id).first() + if not booking: + raise HTTPException(status_code=404, detail='Booking not found') + if not is_admin(current_user, db) and booking.user_id != current_user.id: + raise HTTPException(status_code=403, detail='Forbidden') + if booking.requires_deposit and (not booking.deposit_paid): + deposit_payment = db.query(Payment).filter(Payment.booking_id == booking_id, Payment.payment_type == PaymentType.deposit, Payment.payment_status == PaymentStatus.pending).order_by(Payment.created_at.desc()).first() + if deposit_payment: + expected_deposit_amount = float(deposit_payment.amount) + if abs(amount - expected_deposit_amount) > 0.01: + import logging + logger = logging.getLogger(__name__) + logger.warning(f'Amount mismatch for deposit payment: Requested {amount:,.2f}, Expected deposit {expected_deposit_amount:,.2f}, Booking total {float(booking.total_price):,.2f}') + raise HTTPException(status_code=400, detail=f'For pay-on-arrival bookings, only the deposit amount ({expected_deposit_amount:,.2f}) should be charged, not the full booking amount ({float(booking.total_price):,.2f}).') + transaction_id = BoricaService.generate_transaction_id(booking_id) + client_url = settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173') + return_url = payment_data.get('return_url', f'{client_url}/payment/borica/return') + description = f'Hotel Booking Payment - {booking.booking_number}' + payment_request = BoricaService.create_payment_request(amount=amount, currency=currency, order_id=transaction_id, description=description, return_url=return_url, db=db) + payment_type = PaymentType.full + if booking.requires_deposit and (not booking.deposit_paid): + payment_type = PaymentType.deposit + payment = Payment(booking_id=booking_id, amount=amount, payment_method=PaymentMethod.borica, payment_type=payment_type, payment_status=PaymentStatus.pending, transaction_id=transaction_id, notes=f'Borica payment initiated - Order: {transaction_id}') + db.add(payment) + db.commit() + db.refresh(payment) + return success_response(data={'payment_request': payment_request, 'payment_id': payment.id, 'transaction_id': transaction_id}, message='Borica payment request created successfully') + except HTTPException: + raise + except ValueError as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f'Borica payment creation error: {str(e)}') + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f'Unexpected error creating Borica payment: {str(e)}', exc_info=True) + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + +@router.post('/borica/callback') +async def borica_callback(request: Request, db: Session=Depends(get_db)): + """ + Handle Borica payment callback (POST from Borica gateway). + Borica sends POST data with payment response. + """ + try: + form_data = await request.form() + response_data = dict(form_data) + + # Also try to get from JSON if available + try: + json_data = await request.json() + response_data.update(json_data) + except: + pass + + payment = await BoricaService.confirm_payment(response_data=response_data, db=db) + try: + db.commit() + except Exception: + pass + + booking = db.query(Booking).filter(Booking.id == payment['booking_id']).first() + if booking: + db.refresh(booking) + + if booking and booking.user: + try: + from ..models.system_settings import SystemSettings + client_url_setting = db.query(SystemSettings).filter(SystemSettings.key == 'client_url').first() + client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173') + currency_setting = db.query(SystemSettings).filter(SystemSettings.key == 'platform_currency').first() + currency = currency_setting.value if currency_setting and currency_setting.value else 'USD' + currency_symbol = get_currency_symbol(currency) + email_html = payment_confirmation_email_template(booking_number=booking.booking_number, guest_name=booking.user.full_name, amount=payment['amount'], payment_method='borica', transaction_id=payment['transaction_id'], payment_type=payment.get('payment_type'), total_price=float(booking.total_price), client_url=client_url, currency_symbol=currency_symbol) + await send_email(to=booking.user.email, subject=f'Payment Confirmed - {booking.booking_number}', html=email_html) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.warning(f'Failed to send payment confirmation email: {e}') + + # Redirect to return URL with success status + return_url = response_data.get('BACKREF', '') + if return_url: + from fastapi.responses import RedirectResponse + return RedirectResponse(url=f"{return_url}?status=success&order={response_data.get('ORDER', '')}&bookingId={payment['booking_id']}") + + return success_response(data={'payment': payment, 'booking': {'id': booking.id if booking else None, 'booking_number': booking.booking_number if booking else None, 'status': booking.status.value if booking else None}}, message='Payment confirmed successfully') + except HTTPException: + db.rollback() + raise + except ValueError as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f'Borica payment callback error: {str(e)}') + db.rollback() + # Redirect to return URL with error status + return_url = dict(await request.form()).get('BACKREF', '') if hasattr(request, 'form') else '' + if return_url: + from fastapi.responses import RedirectResponse + return RedirectResponse(url=f"{return_url}?status=error&error={str(e)}") + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f'Unexpected error in Borica callback: {str(e)}', exc_info=True) + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + +@router.post('/borica/confirm') +async def confirm_borica_payment(response_data: dict, db: Session=Depends(get_db)): + try: + payment = await BoricaService.confirm_payment(response_data=response_data, db=db) + try: + db.commit() + except Exception: + pass + booking = db.query(Booking).filter(Booking.id == payment['booking_id']).first() + if booking: + db.refresh(booking) + if booking and booking.user: + try: + from ..models.system_settings import SystemSettings + client_url_setting = db.query(SystemSettings).filter(SystemSettings.key == 'client_url').first() + client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173') + currency_setting = db.query(SystemSettings).filter(SystemSettings.key == 'platform_currency').first() + currency = currency_setting.value if currency_setting and currency_setting.value else 'USD' + currency_symbol = get_currency_symbol(currency) + email_html = payment_confirmation_email_template(booking_number=booking.booking_number, guest_name=booking.user.full_name, amount=payment['amount'], payment_method='borica', transaction_id=payment['transaction_id'], payment_type=payment.get('payment_type'), total_price=float(booking.total_price), client_url=client_url, currency_symbol=currency_symbol) + await send_email(to=booking.user.email, subject=f'Payment Confirmed - {booking.booking_number}', html=email_html) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.warning(f'Failed to send payment confirmation email: {e}') + return success_response(data={'payment': payment, 'booking': {'id': booking.id if booking else None, 'booking_number': booking.booking_number if booking else None, 'status': booking.status.value if booking else None}}, message='Payment confirmed successfully') + except HTTPException: + db.rollback() + raise + except ValueError as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f'Borica payment confirmation error: {str(e)}') + db.rollback() + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f'Unexpected error confirming Borica payment: {str(e)}', exc_info=True) + db.rollback() raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/Backend/src/routes/rate_plan_routes.py b/Backend/src/routes/rate_plan_routes.py new file mode 100644 index 00000000..bf9f403a --- /dev/null +++ b/Backend/src/routes/rate_plan_routes.py @@ -0,0 +1,496 @@ +from fastapi import APIRouter, Depends, HTTPException, status, Query +from sqlalchemy.orm import Session +from sqlalchemy import or_, and_ +from typing import Optional, List +from datetime import datetime, date +from decimal import Decimal +from ..config.database import get_db +from ..middleware.auth import get_current_user, authorize_roles +from ..models.user import User +from ..models.rate_plan import RatePlan, RatePlanRule, RatePlanType, RatePlanStatus +from ..models.room_type import RoomType +from ..models.booking import Booking +from pydantic import BaseModel +from typing import Optional as Opt + +router = APIRouter(prefix='/rate-plans', tags=['rate-plans']) + +# Pydantic models for request/response +class RatePlanRuleCreate(BaseModel): + rule_type: str + rule_key: str + rule_value: Optional[dict] = None + price_modifier: Optional[float] = None + discount_percentage: Optional[float] = None + fixed_adjustment: Optional[float] = None + priority: int = 100 + +class RatePlanCreate(BaseModel): + name: str + code: str + description: Optional[str] = None + plan_type: str + status: str = 'active' + base_price_modifier: float = 1.0 + discount_percentage: Optional[float] = None + fixed_discount: Optional[float] = None + room_type_id: Optional[int] = None + min_nights: Optional[int] = None + max_nights: Optional[int] = None + advance_days_required: Optional[int] = None + valid_from: Optional[str] = None + valid_to: Optional[str] = None + is_refundable: bool = True + requires_deposit: bool = False + deposit_percentage: Optional[float] = None + cancellation_hours: Optional[int] = None + corporate_code: Optional[str] = None + requires_verification: bool = False + verification_type: Optional[str] = None + long_stay_nights: Optional[int] = None + is_package: bool = False + package_id: Optional[int] = None + priority: int = 100 + extra_data: Optional[dict] = None + rules: Optional[List[RatePlanRuleCreate]] = [] + +class RatePlanUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + status: Optional[str] = None + base_price_modifier: Optional[float] = None + discount_percentage: Optional[float] = None + fixed_discount: Optional[float] = None + room_type_id: Optional[int] = None + min_nights: Optional[int] = None + max_nights: Optional[int] = None + advance_days_required: Optional[int] = None + valid_from: Optional[str] = None + valid_to: Optional[str] = None + is_refundable: Optional[bool] = None + requires_deposit: Optional[bool] = None + deposit_percentage: Optional[float] = None + cancellation_hours: Optional[int] = None + corporate_code: Optional[str] = None + requires_verification: Optional[bool] = None + verification_type: Optional[str] = None + long_stay_nights: Optional[int] = None + package_id: Optional[int] = None + priority: Optional[int] = None + extra_data: Optional[dict] = None + +@router.get('/') +async def get_rate_plans( + search: Optional[str] = Query(None), + status_filter: Optional[str] = Query(None, alias='status'), + plan_type: Optional[str] = Query(None), + room_type_id: Optional[int] = Query(None), + page: int = Query(1, ge=1), + limit: int = Query(10, ge=1, le=100), + db: Session = Depends(get_db) +): + try: + query = db.query(RatePlan) + + if search: + query = query.filter( + or_( + RatePlan.name.like(f'%{search}%'), + RatePlan.code.like(f'%{search}%'), + RatePlan.description.like(f'%{search}%') + ) + ) + + if status_filter: + try: + query = query.filter(RatePlan.status == RatePlanStatus(status_filter)) + except ValueError: + pass + + if plan_type: + try: + query = query.filter(RatePlan.plan_type == RatePlanType(plan_type)) + except ValueError: + pass + + if room_type_id: + query = query.filter( + or_( + RatePlan.room_type_id == room_type_id, + RatePlan.room_type_id.is_(None) + ) + ) + + total = query.count() + offset = (page - 1) * limit + rate_plans = query.order_by(RatePlan.priority.asc(), RatePlan.created_at.desc()).offset(offset).limit(limit).all() + + result = [] + for plan in rate_plans: + plan_dict = { + 'id': plan.id, + 'name': plan.name, + 'code': plan.code, + 'description': plan.description, + 'plan_type': plan.plan_type.value if isinstance(plan.plan_type, RatePlanType) else plan.plan_type, + 'status': plan.status.value if isinstance(plan.status, RatePlanStatus) else plan.status, + 'base_price_modifier': float(plan.base_price_modifier) if plan.base_price_modifier else 1.0, + 'discount_percentage': float(plan.discount_percentage) if plan.discount_percentage else None, + 'fixed_discount': float(plan.fixed_discount) if plan.fixed_discount else None, + 'room_type_id': plan.room_type_id, + 'room_type_name': plan.room_type.name if plan.room_type else None, + 'min_nights': plan.min_nights, + 'max_nights': plan.max_nights, + 'advance_days_required': plan.advance_days_required, + 'valid_from': plan.valid_from.isoformat() if plan.valid_from else None, + 'valid_to': plan.valid_to.isoformat() if plan.valid_to else None, + 'is_refundable': plan.is_refundable, + 'requires_deposit': plan.requires_deposit, + 'deposit_percentage': float(plan.deposit_percentage) if plan.deposit_percentage else None, + 'cancellation_hours': plan.cancellation_hours, + 'corporate_code': plan.corporate_code, + 'requires_verification': plan.requires_verification, + 'verification_type': plan.verification_type, + 'long_stay_nights': plan.long_stay_nights, + 'is_package': plan.is_package, + 'package_id': plan.package_id, + 'priority': plan.priority, + 'extra_data': plan.extra_data, + 'created_at': plan.created_at.isoformat() if plan.created_at else None, + 'updated_at': plan.updated_at.isoformat() if plan.updated_at else None, + } + result.append(plan_dict) + + return { + 'status': 'success', + 'data': { + 'rate_plans': result, + 'pagination': { + 'total': total, + 'page': page, + 'limit': limit, + 'totalPages': (total + limit - 1) // limit + } + } + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/{id}') +async def get_rate_plan(id: int, db: Session = Depends(get_db)): + try: + plan = db.query(RatePlan).filter(RatePlan.id == id).first() + if not plan: + raise HTTPException(status_code=404, detail='Rate plan not found') + + rules = db.query(RatePlanRule).filter(RatePlanRule.rate_plan_id == id).order_by(RatePlanRule.priority.asc()).all() + + plan_dict = { + 'id': plan.id, + 'name': plan.name, + 'code': plan.code, + 'description': plan.description, + 'plan_type': plan.plan_type.value if isinstance(plan.plan_type, RatePlanType) else plan.plan_type, + 'status': plan.status.value if isinstance(plan.status, RatePlanStatus) else plan.status, + 'base_price_modifier': float(plan.base_price_modifier) if plan.base_price_modifier else 1.0, + 'discount_percentage': float(plan.discount_percentage) if plan.discount_percentage else None, + 'fixed_discount': float(plan.fixed_discount) if plan.fixed_discount else None, + 'room_type_id': plan.room_type_id, + 'room_type_name': plan.room_type.name if plan.room_type else None, + 'min_nights': plan.min_nights, + 'max_nights': plan.max_nights, + 'advance_days_required': plan.advance_days_required, + 'valid_from': plan.valid_from.isoformat() if plan.valid_from else None, + 'valid_to': plan.valid_to.isoformat() if plan.valid_to else None, + 'is_refundable': plan.is_refundable, + 'requires_deposit': plan.requires_deposit, + 'deposit_percentage': float(plan.deposit_percentage) if plan.deposit_percentage else None, + 'cancellation_hours': plan.cancellation_hours, + 'corporate_code': plan.corporate_code, + 'requires_verification': plan.requires_verification, + 'verification_type': plan.verification_type, + 'long_stay_nights': plan.long_stay_nights, + 'is_package': plan.is_package, + 'package_id': plan.package_id, + 'priority': plan.priority, + 'extra_data': plan.extra_data, + 'rules': [ + { + 'id': rule.id, + 'rule_type': rule.rule_type, + 'rule_key': rule.rule_key, + 'rule_value': rule.rule_value, + 'price_modifier': float(rule.price_modifier) if rule.price_modifier else None, + 'discount_percentage': float(rule.discount_percentage) if rule.discount_percentage else None, + 'fixed_adjustment': float(rule.fixed_adjustment) if rule.fixed_adjustment else None, + 'priority': rule.priority, + } + for rule in rules + ], + 'created_at': plan.created_at.isoformat() if plan.created_at else None, + 'updated_at': plan.updated_at.isoformat() if plan.updated_at else None, + } + + return {'status': 'success', 'data': {'rate_plan': plan_dict}} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post('/', dependencies=[Depends(authorize_roles('admin'))]) +async def create_rate_plan(plan_data: RatePlanCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + try: + # Check if code already exists + existing = db.query(RatePlan).filter(RatePlan.code == plan_data.code).first() + if existing: + raise HTTPException(status_code=400, detail='Rate plan code already exists') + + # Validate room_type_id if provided + if plan_data.room_type_id: + room_type = db.query(RoomType).filter(RoomType.id == plan_data.room_type_id).first() + if not room_type: + raise HTTPException(status_code=404, detail='Room type not found') + + # Create rate plan + rate_plan = RatePlan( + name=plan_data.name, + code=plan_data.code, + description=plan_data.description, + plan_type=RatePlanType(plan_data.plan_type), + status=RatePlanStatus(plan_data.status), + base_price_modifier=Decimal(str(plan_data.base_price_modifier)), + discount_percentage=Decimal(str(plan_data.discount_percentage)) if plan_data.discount_percentage else None, + fixed_discount=Decimal(str(plan_data.fixed_discount)) if plan_data.fixed_discount else None, + room_type_id=plan_data.room_type_id, + min_nights=plan_data.min_nights, + max_nights=plan_data.max_nights, + advance_days_required=plan_data.advance_days_required, + valid_from=datetime.strptime(plan_data.valid_from, '%Y-%m-%d').date() if plan_data.valid_from else None, + valid_to=datetime.strptime(plan_data.valid_to, '%Y-%m-%d').date() if plan_data.valid_to else None, + is_refundable=plan_data.is_refundable, + requires_deposit=plan_data.requires_deposit, + deposit_percentage=Decimal(str(plan_data.deposit_percentage)) if plan_data.deposit_percentage else None, + cancellation_hours=plan_data.cancellation_hours, + corporate_code=plan_data.corporate_code, + requires_verification=plan_data.requires_verification, + verification_type=plan_data.verification_type, + long_stay_nights=plan_data.long_stay_nights, + is_package=plan_data.is_package, + package_id=plan_data.package_id, + priority=plan_data.priority, + extra_data=plan_data.extra_data, + ) + + db.add(rate_plan) + db.flush() + + # Create rules + if plan_data.rules: + for rule_data in plan_data.rules: + rule = RatePlanRule( + rate_plan_id=rate_plan.id, + rule_type=rule_data.rule_type, + rule_key=rule_data.rule_key, + rule_value=rule_data.rule_value, + price_modifier=Decimal(str(rule_data.price_modifier)) if rule_data.price_modifier else None, + discount_percentage=Decimal(str(rule_data.discount_percentage)) if rule_data.discount_percentage else None, + fixed_adjustment=Decimal(str(rule_data.fixed_adjustment)) if rule_data.fixed_adjustment else None, + priority=rule_data.priority, + ) + db.add(rule) + + db.commit() + db.refresh(rate_plan) + + return {'status': 'success', 'message': 'Rate plan created successfully', 'data': {'rate_plan_id': rate_plan.id}} + except HTTPException: + raise + except ValueError as e: + raise HTTPException(status_code=400, detail=f'Invalid enum value: {str(e)}') + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + +@router.put('/{id}', dependencies=[Depends(authorize_roles('admin'))]) +async def update_rate_plan(id: int, plan_data: RatePlanUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + try: + rate_plan = db.query(RatePlan).filter(RatePlan.id == id).first() + if not rate_plan: + raise HTTPException(status_code=404, detail='Rate plan not found') + + # Update fields + if plan_data.name is not None: + rate_plan.name = plan_data.name + if plan_data.description is not None: + rate_plan.description = plan_data.description + if plan_data.status is not None: + rate_plan.status = RatePlanStatus(plan_data.status) + if plan_data.base_price_modifier is not None: + rate_plan.base_price_modifier = Decimal(str(plan_data.base_price_modifier)) + if plan_data.discount_percentage is not None: + rate_plan.discount_percentage = Decimal(str(plan_data.discount_percentage)) + if plan_data.fixed_discount is not None: + rate_plan.fixed_discount = Decimal(str(plan_data.fixed_discount)) + if plan_data.room_type_id is not None: + if plan_data.room_type_id: + room_type = db.query(RoomType).filter(RoomType.id == plan_data.room_type_id).first() + if not room_type: + raise HTTPException(status_code=404, detail='Room type not found') + rate_plan.room_type_id = plan_data.room_type_id + if plan_data.min_nights is not None: + rate_plan.min_nights = plan_data.min_nights + if plan_data.max_nights is not None: + rate_plan.max_nights = plan_data.max_nights + if plan_data.advance_days_required is not None: + rate_plan.advance_days_required = plan_data.advance_days_required + if plan_data.valid_from is not None: + rate_plan.valid_from = datetime.strptime(plan_data.valid_from, '%Y-%m-%d').date() if plan_data.valid_from else None + if plan_data.valid_to is not None: + rate_plan.valid_to = datetime.strptime(plan_data.valid_to, '%Y-%m-%d').date() if plan_data.valid_to else None + if plan_data.is_refundable is not None: + rate_plan.is_refundable = plan_data.is_refundable + if plan_data.requires_deposit is not None: + rate_plan.requires_deposit = plan_data.requires_deposit + if plan_data.deposit_percentage is not None: + rate_plan.deposit_percentage = Decimal(str(plan_data.deposit_percentage)) if plan_data.deposit_percentage else None + if plan_data.cancellation_hours is not None: + rate_plan.cancellation_hours = plan_data.cancellation_hours + if plan_data.corporate_code is not None: + rate_plan.corporate_code = plan_data.corporate_code + if plan_data.requires_verification is not None: + rate_plan.requires_verification = plan_data.requires_verification + if plan_data.verification_type is not None: + rate_plan.verification_type = plan_data.verification_type + if plan_data.long_stay_nights is not None: + rate_plan.long_stay_nights = plan_data.long_stay_nights + if plan_data.package_id is not None: + rate_plan.package_id = plan_data.package_id + if plan_data.priority is not None: + rate_plan.priority = plan_data.priority + if plan_data.extra_data is not None: + rate_plan.extra_data = plan_data.extra_data + + db.commit() + + return {'status': 'success', 'message': 'Rate plan updated successfully'} + except HTTPException: + raise + except ValueError as e: + raise HTTPException(status_code=400, detail=f'Invalid enum value: {str(e)}') + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + +@router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))]) +async def delete_rate_plan(id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + try: + rate_plan = db.query(RatePlan).filter(RatePlan.id == id).first() + if not rate_plan: + raise HTTPException(status_code=404, detail='Rate plan not found') + + # Check if rate plan is used in bookings + booking_count = db.query(Booking).filter(Booking.rate_plan_id == id).count() + if booking_count > 0: + raise HTTPException(status_code=400, detail=f'Cannot delete rate plan. It is used in {booking_count} booking(s)') + + # Delete rules first + db.query(RatePlanRule).filter(RatePlanRule.rate_plan_id == id).delete() + + db.delete(rate_plan) + db.commit() + + return {'status': 'success', 'message': 'Rate plan deleted successfully'} + except HTTPException: + raise + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/available/{room_type_id}') +async def get_available_rate_plans( + room_type_id: int, + check_in: str = Query(...), + check_out: str = Query(...), + num_nights: Optional[int] = Query(None), + db: Session = Depends(get_db) +): + """Get available rate plans for a room type and date range""" + try: + check_in_date = datetime.strptime(check_in, '%Y-%m-%d').date() + check_out_date = datetime.strptime(check_out, '%Y-%m-%d').date() + + if num_nights is None: + num_nights = (check_out_date - check_in_date).days + + today = date.today() + advance_days = (check_in_date - today).days + + # Query rate plans + query = db.query(RatePlan).filter( + RatePlan.status == RatePlanStatus.active, + or_( + RatePlan.room_type_id == room_type_id, + RatePlan.room_type_id.is_(None) + ) + ) + + # Filter by date range + query = query.filter( + or_( + RatePlan.valid_from.is_(None), + RatePlan.valid_from <= check_in_date + ), + or_( + RatePlan.valid_to.is_(None), + RatePlan.valid_to >= check_out_date + ) + ) + + # Filter by advance days + query = query.filter( + or_( + RatePlan.advance_days_required.is_(None), + RatePlan.advance_days_required <= advance_days + ) + ) + + # Filter by nights + query = query.filter( + or_( + RatePlan.min_nights.is_(None), + RatePlan.min_nights <= num_nights + ), + or_( + RatePlan.max_nights.is_(None), + RatePlan.max_nights >= num_nights + ) + ) + + rate_plans = query.order_by(RatePlan.priority.asc()).all() + + result = [] + for plan in rate_plans: + plan_dict = { + 'id': plan.id, + 'name': plan.name, + 'code': plan.code, + 'description': plan.description, + 'plan_type': plan.plan_type.value if isinstance(plan.plan_type, RatePlanType) else plan.plan_type, + 'base_price_modifier': float(plan.base_price_modifier) if plan.base_price_modifier else 1.0, + 'discount_percentage': float(plan.discount_percentage) if plan.discount_percentage else None, + 'fixed_discount': float(plan.fixed_discount) if plan.fixed_discount else None, + 'is_refundable': plan.is_refundable, + 'requires_deposit': plan.requires_deposit, + 'deposit_percentage': float(plan.deposit_percentage) if plan.deposit_percentage else None, + 'cancellation_hours': plan.cancellation_hours, + 'requires_verification': plan.requires_verification, + 'verification_type': plan.verification_type, + } + result.append(plan_dict) + + return {'status': 'success', 'data': {'rate_plans': result}} + except ValueError as e: + raise HTTPException(status_code=400, detail=f'Invalid date format: {str(e)}') + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + diff --git a/Backend/src/routes/report_routes.py b/Backend/src/routes/report_routes.py index 7e7d6df4..3b24f84f 100644 --- a/Backend/src/routes/report_routes.py +++ b/Backend/src/routes/report_routes.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, load_only, joinedload from sqlalchemy import func, and_ from typing import Optional from datetime import datetime, timedelta @@ -8,9 +8,10 @@ from ..middleware.auth import get_current_user, authorize_roles from ..models.user import User from ..models.booking import Booking, BookingStatus from ..models.payment import Payment, PaymentStatus -from ..models.room import Room +from ..models.room import Room, RoomStatus from ..models.service_usage import ServiceUsage from ..models.service import Service +from ..utils.response_helpers import success_response router = APIRouter(prefix='/reports', tags=['reports']) @router.get('') @@ -37,7 +38,8 @@ async def get_reports(from_date: Optional[str]=Query(None, alias='from'), to_dat if end_date: booking_query = booking_query.filter(Booking.created_at <= end_date) payment_query = payment_query.filter(Payment.payment_date <= end_date) - total_bookings = booking_query.count() + # Use func.count() to avoid loading all columns (including non-existent rate_plan_id) + total_bookings = booking_query.with_entities(func.count(Booking.id)).scalar() or 0 total_revenue = payment_query.with_entities(func.sum(Payment.amount)).scalar() or 0.0 total_customers = db.query(func.count(func.distinct(Booking.user_id))).scalar() or 0 if start_date or end_date: @@ -47,7 +49,7 @@ async def get_reports(from_date: Optional[str]=Query(None, alias='from'), to_dat if end_date: customer_query = customer_query.filter(Booking.created_at <= end_date) total_customers = customer_query.scalar() or 0 - available_rooms = db.query(Room).filter(Room.status == 'available').count() + available_rooms = db.query(Room).filter(Room.status == RoomStatus.available).count() occupied_rooms = db.query(func.count(func.distinct(Booking.room_id))).filter(Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in])).scalar() or 0 revenue_by_date = [] if start_date and end_date: @@ -61,7 +63,8 @@ async def get_reports(from_date: Optional[str]=Query(None, alias='from'), to_dat revenue_by_date = [{'date': str(date), 'revenue': float(revenue or 0), 'bookings': int(bookings or 0)} for date, revenue, bookings in daily_data] bookings_by_status = {} for status in BookingStatus: - count = booking_query.filter(Booking.status == status).count() + # Use func.count() to avoid loading all columns (including non-existent rate_plan_id) + count = booking_query.filter(Booking.status == status).with_entities(func.count(Booking.id)).scalar() or 0 status_name = status.value if hasattr(status, 'value') else str(status) bookings_by_status[status_name] = count top_rooms_query = db.query(Room.id, Room.room_number, func.count(Booking.id).label('bookings'), func.sum(Payment.amount).label('revenue')).join(Booking, Room.id == Booking.room_id).join(Payment, Booking.id == Payment.booking_id).filter(Payment.payment_status == PaymentStatus.completed) @@ -78,24 +81,25 @@ async def get_reports(from_date: Optional[str]=Query(None, alias='from'), to_dat service_usage_query = service_usage_query.filter(ServiceUsage.usage_date <= end_date) service_usage_data = service_usage_query.group_by(Service.id, Service.name).order_by(func.sum(ServiceUsage.total_price).desc()).limit(10).all() service_usage = [{'service_id': service_id, 'service_name': service_name, 'usage_count': int(usage_count or 0), 'total_revenue': float(total_revenue or 0)} for service_id, service_name, usage_count, total_revenue in service_usage_data] - return {'status': 'success', 'success': True, 'data': {'total_bookings': total_bookings, 'total_revenue': float(total_revenue), 'total_customers': int(total_customers), 'available_rooms': available_rooms, 'occupied_rooms': occupied_rooms, 'revenue_by_date': revenue_by_date if revenue_by_date else None, 'bookings_by_status': bookings_by_status, 'top_rooms': top_rooms if top_rooms else None, 'service_usage': service_usage if service_usage else None}} + return success_response(data={'total_bookings': total_bookings, 'total_revenue': float(total_revenue), 'total_customers': int(total_customers), 'available_rooms': available_rooms, 'occupied_rooms': occupied_rooms, 'revenue_by_date': revenue_by_date if revenue_by_date else None, 'bookings_by_status': bookings_by_status, 'top_rooms': top_rooms if top_rooms else None, 'service_usage': service_usage if service_usage else None}) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get('/dashboard') async def get_dashboard_stats(current_user: User=Depends(authorize_roles('admin', 'staff', 'accountant')), db: Session=Depends(get_db)): try: - total_bookings = db.query(Booking).count() - active_bookings = db.query(Booking).filter(Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])).count() + # Use func.count() to avoid loading all columns (including non-existent rate_plan_id) + total_bookings = db.query(Booking).with_entities(func.count(Booking.id)).scalar() or 0 + active_bookings = db.query(Booking).filter(Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])).with_entities(func.count(Booking.id)).scalar() or 0 total_revenue = db.query(func.sum(Payment.amount)).filter(Payment.payment_status == PaymentStatus.completed).scalar() or 0.0 today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) today_revenue = db.query(func.sum(Payment.amount)).filter(and_(Payment.payment_status == PaymentStatus.completed, Payment.payment_date >= today_start)).scalar() or 0.0 total_rooms = db.query(Room).count() - available_rooms = db.query(Room).filter(Room.status == 'available').count() + available_rooms = db.query(Room).filter(Room.status == RoomStatus.available).count() week_ago = datetime.utcnow() - timedelta(days=7) - recent_bookings = db.query(Booking).filter(Booking.created_at >= week_ago).count() + recent_bookings = db.query(Booking).filter(Booking.created_at >= week_ago).with_entities(func.count(Booking.id)).scalar() or 0 pending_payments = db.query(Payment).filter(Payment.payment_status == PaymentStatus.pending).count() - return {'status': 'success', 'data': {'total_bookings': total_bookings, 'active_bookings': active_bookings, 'total_revenue': float(total_revenue), 'today_revenue': float(today_revenue), 'total_rooms': total_rooms, 'available_rooms': available_rooms, 'recent_bookings': recent_bookings, 'pending_payments': pending_payments}} + return success_response(data={'total_bookings': total_bookings, 'active_bookings': active_bookings, 'total_revenue': float(total_revenue), 'today_revenue': float(today_revenue), 'total_rooms': total_rooms, 'available_rooms': available_rooms, 'recent_bookings': recent_bookings, 'pending_payments': pending_payments}) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -103,19 +107,28 @@ async def get_dashboard_stats(current_user: User=Depends(authorize_roles('admin' async def get_customer_dashboard_stats(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): try: from datetime import datetime, timedelta - total_bookings = db.query(Booking).filter(Booking.user_id == current_user.id).count() + # Use func.count() to avoid loading all columns (including non-existent rate_plan_id) + total_bookings = db.query(Booking).filter(Booking.user_id == current_user.id).with_entities(func.count(Booking.id)).scalar() or 0 user_bookings = db.query(Booking.id).filter(Booking.user_id == current_user.id).subquery() total_spending = db.query(func.sum(Payment.amount)).filter(and_(Payment.booking_id.in_(db.query(user_bookings.c.id)), Payment.payment_status == PaymentStatus.completed)).scalar() or 0.0 now = datetime.utcnow() - currently_staying = db.query(Booking).filter(and_(Booking.user_id == current_user.id, Booking.status == BookingStatus.checked_in, Booking.check_in_date <= now, Booking.check_out_date >= now)).count() - upcoming_bookings_query = db.query(Booking).filter(and_(Booking.user_id == current_user.id, Booking.status.in_([BookingStatus.confirmed, BookingStatus.pending]), Booking.check_in_date > now)).order_by(Booking.check_in_date.asc()).limit(5).all() + currently_staying = db.query(Booking).filter(and_(Booking.user_id == current_user.id, Booking.status == BookingStatus.checked_in, Booking.check_in_date <= now, Booking.check_out_date >= now)).with_entities(func.count(Booking.id)).scalar() or 0 + # Use load_only to exclude non-existent columns and eagerly load room relationships + upcoming_bookings_query = db.query(Booking).options( + load_only(Booking.id, Booking.booking_number, Booking.check_in_date, Booking.check_out_date, Booking.status, Booking.total_price, Booking.user_id, Booking.room_id, Booking.created_at), + joinedload(Booking.room).joinedload(Room.room_type) + ).filter(and_(Booking.user_id == current_user.id, Booking.status.in_([BookingStatus.confirmed, BookingStatus.pending]), Booking.check_in_date > now)).order_by(Booking.check_in_date.asc()).limit(5).all() upcoming_bookings = [] for booking in upcoming_bookings_query: booking_dict = {'id': booking.id, 'booking_number': booking.booking_number, 'check_in_date': booking.check_in_date.isoformat() if booking.check_in_date else None, 'check_out_date': booking.check_out_date.isoformat() if booking.check_out_date else None, 'status': booking.status.value if isinstance(booking.status, BookingStatus) else booking.status, 'total_price': float(booking.total_price) if booking.total_price else 0.0} if booking.room: booking_dict['room'] = {'id': booking.room.id, 'room_number': booking.room.room_number, 'room_type': {'name': booking.room.room_type.name if booking.room.room_type else None}} upcoming_bookings.append(booking_dict) - recent_bookings_query = db.query(Booking).filter(Booking.user_id == current_user.id).order_by(Booking.created_at.desc()).limit(5).all() + # Use load_only to exclude non-existent columns and eagerly load room relationships + recent_bookings_query = db.query(Booking).options( + load_only(Booking.id, Booking.booking_number, Booking.status, Booking.user_id, Booking.room_id, Booking.created_at), + joinedload(Booking.room) + ).filter(Booking.user_id == current_user.id).order_by(Booking.created_at.desc()).limit(5).all() recent_activity = [] for booking in recent_bookings_query: activity_type = None @@ -135,8 +148,9 @@ async def get_customer_dashboard_stats(current_user: User=Depends(get_current_us recent_activity.append(activity_dict) last_month_start = (now - timedelta(days=30)).replace(day=1, hour=0, minute=0, second=0) last_month_end = now.replace(day=1, hour=0, minute=0, second=0) - timedelta(seconds=1) - last_month_bookings = db.query(Booking).filter(and_(Booking.user_id == current_user.id, Booking.created_at >= last_month_start, Booking.created_at <= last_month_end)).count() - this_month_bookings = db.query(Booking).filter(and_(Booking.user_id == current_user.id, Booking.created_at >= now.replace(day=1, hour=0, minute=0, second=0), Booking.created_at <= now)).count() + # Use func.count() to avoid loading all columns (including non-existent rate_plan_id) + last_month_bookings = db.query(Booking).filter(and_(Booking.user_id == current_user.id, Booking.created_at >= last_month_start, Booking.created_at <= last_month_end)).with_entities(func.count(Booking.id)).scalar() or 0 + this_month_bookings = db.query(Booking).filter(and_(Booking.user_id == current_user.id, Booking.created_at >= now.replace(day=1, hour=0, minute=0, second=0), Booking.created_at <= now)).with_entities(func.count(Booking.id)).scalar() or 0 booking_change_percentage = 0 if last_month_bookings > 0: booking_change_percentage = (this_month_bookings - last_month_bookings) / last_month_bookings * 100 @@ -145,7 +159,7 @@ async def get_customer_dashboard_stats(current_user: User=Depends(get_current_us spending_change_percentage = 0 if last_month_spending > 0: spending_change_percentage = (this_month_spending - last_month_spending) / last_month_spending * 100 - return {'status': 'success', 'success': True, 'data': {'total_bookings': total_bookings, 'total_spending': float(total_spending), 'currently_staying': currently_staying, 'upcoming_bookings': upcoming_bookings, 'recent_activity': recent_activity, 'booking_change_percentage': round(booking_change_percentage, 1), 'spending_change_percentage': round(spending_change_percentage, 1)}} + return success_response(data={'total_bookings': total_bookings, 'total_spending': float(total_spending), 'currently_staying': currently_staying, 'upcoming_bookings': upcoming_bookings, 'recent_activity': recent_activity, 'booking_change_percentage': round(booking_change_percentage, 1), 'spending_change_percentage': round(spending_change_percentage, 1)}) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -167,6 +181,6 @@ async def get_revenue_report(start_date: Optional[str]=Query(None), end_date: Op method_breakdown[method_name] = float(total or 0) daily_revenue = db.query(func.date(Payment.payment_date).label('date'), func.sum(Payment.amount).label('total')).filter(Payment.payment_status == PaymentStatus.completed).group_by(func.date(Payment.payment_date)).order_by(func.date(Payment.payment_date).desc()).limit(30).all() daily_breakdown = [{'date': date.isoformat() if isinstance(date, datetime) else str(date), 'revenue': float(total or 0)} for date, total in daily_revenue] - return {'status': 'success', 'data': {'total_revenue': float(total_revenue), 'revenue_by_method': method_breakdown, 'daily_breakdown': daily_breakdown}} + return success_response(data={'total_revenue': float(total_revenue), 'revenue_by_method': method_breakdown, 'daily_breakdown': daily_breakdown}) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/Backend/src/routes/room_routes.py b/Backend/src/routes/room_routes.py index 8128ac6d..c891029f 100644 --- a/Backend/src/routes/room_routes.py +++ b/Backend/src/routes/room_routes.py @@ -17,7 +17,7 @@ from pathlib import Path router = APIRouter(prefix='/rooms', tags=['rooms']) @router.get('/') -async def get_rooms(request: Request, type: Optional[str]=Query(None), minPrice: Optional[float]=Query(None), maxPrice: Optional[float]=Query(None), capacity: Optional[int]=Query(None), page: int=Query(1, ge=1), limit: int=Query(10, ge=1, le=100), sort: Optional[str]=Query(None), featured: Optional[bool]=Query(None), db: Session=Depends(get_db)): +async def get_rooms(request: Request, type: Optional[str]=Query(None), minPrice: Optional[float]=Query(None), maxPrice: Optional[float]=Query(None), capacity: Optional[int]=Query(None), page: int=Query(1, ge=1), limit: int=Query(10, ge=1, le=1000), sort: Optional[str]=Query(None), featured: Optional[bool]=Query(None), db: Session=Depends(get_db)): try: where_clause = {} room_type_where = {} @@ -90,6 +90,32 @@ async def search_available_rooms(request: Request, from_date: str=Query(..., ali overlapping = db.query(Booking).filter(and_(Booking.room_id == roomId, Booking.status != BookingStatus.cancelled, Booking.check_in_date < check_out, Booking.check_out_date > check_in)).first() if overlapping: return {'status': 'success', 'data': {'available': False, 'message': 'Room is already booked for the selected dates', 'room_id': roomId}} + + # Check for maintenance blocks + from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus + maintenance_block = db.query(RoomMaintenance).filter( + and_( + RoomMaintenance.room_id == roomId, + RoomMaintenance.blocks_room == True, + RoomMaintenance.status.in_([MaintenanceStatus.scheduled, MaintenanceStatus.in_progress]), + or_( + and_( + RoomMaintenance.block_start.isnot(None), + RoomMaintenance.block_end.isnot(None), + RoomMaintenance.block_start < check_out, + RoomMaintenance.block_end > check_in + ), + and_( + RoomMaintenance.scheduled_start < check_out, + RoomMaintenance.scheduled_end.isnot(None), + RoomMaintenance.scheduled_end > check_in + ) + ) + ) + ).first() + if maintenance_block: + return {'status': 'success', 'data': {'available': False, 'message': f'Room is blocked for maintenance: {maintenance_block.title}', 'room_id': roomId}} + return {'status': 'success', 'data': {'available': True, 'message': 'Room is available', 'room_id': roomId}} if check_in >= check_out: raise HTTPException(status_code=400, detail='Check-out date must be after check-in date') @@ -100,6 +126,29 @@ async def search_available_rooms(request: Request, from_date: str=Query(..., ali query = query.filter(RoomType.capacity >= capacity) overlapping_rooms = db.query(Booking.room_id).filter(and_(Booking.status != BookingStatus.cancelled, Booking.check_in_date < check_out, Booking.check_out_date > check_in)).subquery() query = query.filter(~Room.id.in_(db.query(overlapping_rooms.c.room_id))) + + # Exclude rooms blocked by maintenance + from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus + blocked_rooms = db.query(RoomMaintenance.room_id).filter( + and_( + RoomMaintenance.blocks_room == True, + RoomMaintenance.status.in_([MaintenanceStatus.scheduled, MaintenanceStatus.in_progress]), + or_( + and_( + RoomMaintenance.block_start.isnot(None), + RoomMaintenance.block_end.isnot(None), + RoomMaintenance.block_start < check_out, + RoomMaintenance.block_end > check_in + ), + and_( + RoomMaintenance.scheduled_start < check_out, + RoomMaintenance.scheduled_end.isnot(None), + RoomMaintenance.scheduled_end > check_in + ) + ) + ) + ).subquery() + query = query.filter(~Room.id.in_(db.query(blocked_rooms.c.room_id))) total = query.count() query = query.order_by(Room.featured.desc(), Room.created_at.desc()) offset = (page - 1) * limit diff --git a/Backend/src/routes/security_routes.py b/Backend/src/routes/security_routes.py new file mode 100644 index 00000000..151f4555 --- /dev/null +++ b/Backend/src/routes/security_routes.py @@ -0,0 +1,744 @@ +from fastapi import APIRouter, Depends, HTTPException, status, Request, Query +from sqlalchemy.orm import Session +from typing import Optional, List +from datetime import datetime, timedelta +from pydantic import BaseModel, EmailStr +import logging +from ..config.logging_config import get_logger + +logger = get_logger(__name__) + +from ..config.database import get_db +from ..middleware.auth import get_current_user, authorize_roles +from ..models.user import User +from ..models.security_event import ( + SecurityEvent, + SecurityEventType, + SecurityEventSeverity, + IPWhitelist, + IPBlacklist +) +from ..services.security_monitoring_service import security_monitoring_service +from ..services.gdpr_service import gdpr_service +from ..services.encryption_service import encryption_service +from ..services.security_scan_service import security_scan_service + +# OAuth service is optional - only import if httpx is available +try: + from ..services.oauth_service import oauth_service + OAUTH_AVAILABLE = True +except ImportError: + OAUTH_AVAILABLE = False + oauth_service = None + +router = APIRouter(prefix="/security", tags=["Security"]) + +# Security Events +class SecurityEventResponse(BaseModel): + id: int + user_id: Optional[int] + event_type: str + severity: str + ip_address: Optional[str] + description: Optional[str] + created_at: datetime + + class Config: + from_attributes = True + +@router.get("/events", response_model=List[SecurityEventResponse]) +async def get_security_events( + user_id: Optional[int] = Query(None), + event_type: Optional[str] = Query(None), + severity: Optional[str] = Query(None), + ip_address: Optional[str] = Query(None), + resolved: Optional[bool] = Query(None), + days: int = Query(7, ge=1, le=90), + limit: int = Query(100, ge=1, le=1000), + offset: int = Query(0, ge=0), + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Get security events""" + + event_type_enum = None + if event_type: + try: + event_type_enum = SecurityEventType(event_type) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid event type") + + severity_enum = None + if severity: + try: + severity_enum = SecurityEventSeverity(severity) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid severity") + + start_date = datetime.utcnow() - timedelta(days=days) + + events = security_monitoring_service.get_security_events( + db=db, + user_id=user_id, + event_type=event_type_enum, + severity=severity_enum, + ip_address=ip_address, + resolved=resolved, + start_date=start_date, + limit=limit, + offset=offset + ) + + return events + +@router.get("/events/stats") +async def get_security_stats( + days: int = Query(7, ge=1, le=90), + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Get security statistics""" + + stats = security_monitoring_service.get_security_stats(db=db, days=days) + return stats + +@router.post("/events/{event_id}/resolve") +async def resolve_security_event( + event_id: int, + resolution_notes: Optional[str] = None, + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Mark a security event as resolved""" + + try: + event = security_monitoring_service.resolve_event( + db=db, + event_id=event_id, + resolved_by=current_user.id, + resolution_notes=resolution_notes + ) + return {"status": "success", "message": "Event resolved", "event_id": event.id} + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + +# IP Whitelist/Blacklist +class IPWhitelistCreate(BaseModel): + ip_address: str + description: Optional[str] = None + +class IPBlacklistCreate(BaseModel): + ip_address: str + reason: Optional[str] = None + blocked_until: Optional[datetime] = None + +@router.post("/ip/whitelist") +async def add_ip_to_whitelist( + data: IPWhitelistCreate, + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Add IP address to whitelist""" + + # Check if already exists + existing = db.query(IPWhitelist).filter( + IPWhitelist.ip_address == data.ip_address + ).first() + + if existing: + existing.is_active = True + existing.description = data.description + db.commit() + return {"status": "success", "message": "IP whitelist updated"} + + whitelist = IPWhitelist( + ip_address=data.ip_address, + description=data.description, + created_by=current_user.id + ) + db.add(whitelist) + db.commit() + + return {"status": "success", "message": "IP added to whitelist"} + +@router.delete("/ip/whitelist/{ip_address}") +async def remove_ip_from_whitelist( + ip_address: str, + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Remove IP address from whitelist""" + + whitelist = db.query(IPWhitelist).filter( + IPWhitelist.ip_address == ip_address + ).first() + + if not whitelist: + raise HTTPException(status_code=404, detail="IP not found in whitelist") + + whitelist.is_active = False + db.commit() + + return {"status": "success", "message": "IP removed from whitelist"} + +@router.get("/ip/whitelist") +async def get_whitelisted_ips( + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Get all whitelisted IPs""" + + whitelist = db.query(IPWhitelist).filter( + IPWhitelist.is_active == True + ).all() + + return [{"id": w.id, "ip_address": w.ip_address, "description": w.description} for w in whitelist] + +@router.post("/ip/blacklist") +async def add_ip_to_blacklist( + data: IPBlacklistCreate, + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Add IP address to blacklist""" + + existing = db.query(IPBlacklist).filter( + IPBlacklist.ip_address == data.ip_address + ).first() + + if existing: + existing.is_active = True + existing.reason = data.reason + existing.blocked_until = data.blocked_until + db.commit() + return {"status": "success", "message": "IP blacklist updated"} + + blacklist = IPBlacklist( + ip_address=data.ip_address, + reason=data.reason, + blocked_until=data.blocked_until, + created_by=current_user.id + ) + db.add(blacklist) + db.commit() + + return {"status": "success", "message": "IP added to blacklist"} + +@router.delete("/ip/blacklist/{ip_address}") +async def remove_ip_from_blacklist( + ip_address: str, + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Remove IP address from blacklist""" + + blacklist = db.query(IPBlacklist).filter( + IPBlacklist.ip_address == ip_address + ).first() + + if not blacklist: + raise HTTPException(status_code=404, detail="IP not found in blacklist") + + blacklist.is_active = False + db.commit() + + return {"status": "success", "message": "IP removed from blacklist"} + +@router.get("/ip/blacklist") +async def get_blacklisted_ips( + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Get all blacklisted IPs""" + + blacklist = db.query(IPBlacklist).filter( + IPBlacklist.is_active == True + ).all() + + return [{"id": b.id, "ip_address": b.ip_address, "reason": b.reason, "blocked_until": b.blocked_until} for b in blacklist] + +# OAuth Provider Management +class OAuthProviderCreate(BaseModel): + name: str + display_name: str + client_id: str + client_secret: str + authorization_url: str + token_url: str + userinfo_url: str + scopes: Optional[str] = None + is_active: bool = True + is_sso_enabled: bool = False + +class OAuthProviderUpdate(BaseModel): + display_name: Optional[str] = None + client_id: Optional[str] = None + client_secret: Optional[str] = None + authorization_url: Optional[str] = None + token_url: Optional[str] = None + userinfo_url: Optional[str] = None + scopes: Optional[str] = None + is_active: Optional[bool] = None + is_sso_enabled: Optional[bool] = None + +@router.get("/oauth/providers") +async def get_oauth_providers( + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Get all OAuth providers""" + from ..models.security_event import OAuthProvider + providers = db.query(OAuthProvider).all() + return [{ + "id": p.id, + "name": p.name, + "display_name": p.display_name, + "is_active": p.is_active, + "is_sso_enabled": p.is_sso_enabled, + "created_at": p.created_at.isoformat() if p.created_at else None + } for p in providers] + +@router.post("/oauth/providers") +async def create_oauth_provider( + data: OAuthProviderCreate, + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Create a new OAuth provider""" + from ..models.security_event import OAuthProvider + from ..services.encryption_service import encryption_service + + # Encrypt client secret + encrypted_secret = encryption_service.encrypt(data.client_secret) + + provider = OAuthProvider( + name=data.name, + display_name=data.display_name, + client_id=data.client_id, + client_secret=encrypted_secret, + authorization_url=data.authorization_url, + token_url=data.token_url, + userinfo_url=data.userinfo_url, + scopes=data.scopes, + is_active=data.is_active, + is_sso_enabled=data.is_sso_enabled + ) + db.add(provider) + db.commit() + db.refresh(provider) + + return { + "id": provider.id, + "name": provider.name, + "display_name": provider.display_name, + "is_active": provider.is_active, + "is_sso_enabled": provider.is_sso_enabled + } + +@router.put("/oauth/providers/{provider_id}") +async def update_oauth_provider( + provider_id: int, + data: OAuthProviderUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Update an OAuth provider""" + from ..models.security_event import OAuthProvider + from ..services.encryption_service import encryption_service + + provider = db.query(OAuthProvider).filter(OAuthProvider.id == provider_id).first() + if not provider: + raise HTTPException(status_code=404, detail="OAuth provider not found") + + if data.display_name is not None: + provider.display_name = data.display_name + if data.client_id is not None: + provider.client_id = data.client_id + if data.client_secret is not None: + provider.client_secret = encryption_service.encrypt(data.client_secret) + if data.authorization_url is not None: + provider.authorization_url = data.authorization_url + if data.token_url is not None: + provider.token_url = data.token_url + if data.userinfo_url is not None: + provider.userinfo_url = data.userinfo_url + if data.scopes is not None: + provider.scopes = data.scopes + if data.is_active is not None: + provider.is_active = data.is_active + if data.is_sso_enabled is not None: + provider.is_sso_enabled = data.is_sso_enabled + + db.commit() + db.refresh(provider) + + return { + "id": provider.id, + "name": provider.name, + "display_name": provider.display_name, + "is_active": provider.is_active, + "is_sso_enabled": provider.is_sso_enabled + } + +@router.delete("/oauth/providers/{provider_id}") +async def delete_oauth_provider( + provider_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Delete an OAuth provider""" + from ..models.security_event import OAuthProvider + + provider = db.query(OAuthProvider).filter(OAuthProvider.id == provider_id).first() + if not provider: + raise HTTPException(status_code=404, detail="OAuth provider not found") + + db.delete(provider) + db.commit() + + return {"status": "success", "message": "OAuth provider deleted"} + +# GDPR Request Management +@router.get("/gdpr/requests") +async def get_gdpr_requests( + status: Optional[str] = Query(None), + request_type: Optional[str] = Query(None), + limit: int = Query(50, ge=1, le=100), + offset: int = Query(0, ge=0), + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Get all GDPR requests""" + from ..models.gdpr_compliance import DataSubjectRequest, DataSubjectRequestStatus, DataSubjectRequestType + + query = db.query(DataSubjectRequest) + + if status: + try: + status_enum = DataSubjectRequestStatus(status) + query = query.filter(DataSubjectRequest.status == status_enum) + except ValueError: + pass + + if request_type: + try: + type_enum = DataSubjectRequestType(request_type) + query = query.filter(DataSubjectRequest.request_type == type_enum) + except ValueError: + pass + + requests = query.order_by(DataSubjectRequest.created_at.desc()).offset(offset).limit(limit).all() + + return [{ + "id": r.id, + "user_id": r.user_id, + "email": r.email, + "request_type": r.request_type.value, + "status": r.status.value, + "description": r.description, + "verified": r.verified, + "verified_at": r.verified_at.isoformat() if r.verified_at else None, + "assigned_to": r.assigned_to, + "completed_at": r.completed_at.isoformat() if r.completed_at else None, + "created_at": r.created_at.isoformat() if r.created_at else None + } for r in requests] + +@router.get("/gdpr/requests/{request_id}") +async def get_gdpr_request( + request_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Get a specific GDPR request""" + from ..models.gdpr_compliance import DataSubjectRequest + + request = db.query(DataSubjectRequest).filter(DataSubjectRequest.id == request_id).first() + if not request: + raise HTTPException(status_code=404, detail="GDPR request not found") + + return { + "id": request.id, + "user_id": request.user_id, + "email": request.email, + "request_type": request.request_type.value, + "status": request.status.value, + "description": request.description, + "verified": request.verified, + "verified_at": request.verified_at.isoformat() if request.verified_at else None, + "assigned_to": request.assigned_to, + "notes": request.notes, + "response_data": request.response_data, + "completed_at": request.completed_at.isoformat() if request.completed_at else None, + "created_at": request.created_at.isoformat() if request.created_at else None + } + +@router.post("/gdpr/requests/{request_id}/assign") +async def assign_gdpr_request( + request_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Assign a GDPR request to the current admin""" + from ..models.gdpr_compliance import DataSubjectRequest + + request = db.query(DataSubjectRequest).filter(DataSubjectRequest.id == request_id).first() + if not request: + raise HTTPException(status_code=404, detail="GDPR request not found") + + request.assigned_to = current_user.id + db.commit() + + return {"status": "success", "message": "Request assigned"} + +@router.post("/gdpr/requests/{request_id}/complete") +async def complete_gdpr_request( + request_id: int, + notes: Optional[str] = None, + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Mark a GDPR request as completed""" + from ..models.gdpr_compliance import DataSubjectRequest, DataSubjectRequestStatus + + request = db.query(DataSubjectRequest).filter(DataSubjectRequest.id == request_id).first() + if not request: + raise HTTPException(status_code=404, detail="GDPR request not found") + + request.status = DataSubjectRequestStatus.completed + request.completed_at = datetime.utcnow() + request.completed_by = current_user.id + if notes: + request.notes = notes + + db.commit() + + return {"status": "success", "message": "Request completed"} + +# OAuth Routes +@router.get("/oauth/{provider_name}/authorize") +async def oauth_authorize( + provider_name: str, + redirect_uri: str = Query(...), + state: Optional[str] = None, + db: Session = Depends(get_db) +): + """Get OAuth authorization URL""" + if not OAUTH_AVAILABLE: + raise HTTPException(status_code=503, detail="OAuth service is not available. Please install httpx: pip install httpx") + + try: + auth_url = oauth_service.get_authorization_url( + db=db, + provider_name=provider_name, + redirect_uri=redirect_uri, + state=state + ) + return {"authorization_url": auth_url} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/oauth/{provider_name}/callback") +async def oauth_callback( + provider_name: str, + code: str = Query(...), + redirect_uri: str = Query(...), + state: Optional[str] = None, + db: Session = Depends(get_db) +): + """Handle OAuth callback""" + if not OAUTH_AVAILABLE: + raise HTTPException(status_code=503, detail="OAuth service is not available. Please install httpx: pip install httpx") + + try: + # Exchange code for token + token_data = await oauth_service.exchange_code_for_token( + db=db, + provider_name=provider_name, + code=code, + redirect_uri=redirect_uri + ) + + # Get user info + user_info = await oauth_service.get_user_info( + db=db, + provider_name=provider_name, + access_token=token_data['access_token'] + ) + + # Find or create user + user = oauth_service.find_or_create_user_from_oauth( + db=db, + provider_name=provider_name, + user_info=user_info + ) + + # Save OAuth token + from ..models.security_event import OAuthProvider + provider = db.query(OAuthProvider).filter( + OAuthProvider.name == provider_name + ).first() + + oauth_service.save_oauth_token( + db=db, + user_id=user.id, + provider_id=provider.id, + provider_user_id=user_info.get('sub') or user_info.get('id'), + access_token=token_data['access_token'], + refresh_token=token_data.get('refresh_token'), + expires_in=token_data.get('expires_in'), + scopes=token_data.get('scope') + ) + + # Generate JWT tokens for the user + from ..services.auth_service import auth_service + tokens = auth_service.generate_tokens(user.id) + + return { + "status": "success", + "token": tokens["accessToken"], + "refreshToken": tokens["refreshToken"], + "user": { + "id": user.id, + "email": user.email, + "full_name": user.full_name + } + } + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +# GDPR Routes +class DataSubjectRequestCreate(BaseModel): + email: EmailStr + request_type: str + description: Optional[str] = None + +@router.post("/gdpr/request") +async def create_data_subject_request( + data: DataSubjectRequestCreate, + request: Request, + db: Session = Depends(get_db) +): + """Create a GDPR data subject request""" + + try: + request_type = DataSubjectRequestType(data.request_type) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid request type") + + try: + gdpr_request = gdpr_service.create_data_subject_request( + db=db, + email=data.email, + request_type=request_type, + description=data.description, + ip_address=request.client.host if request.client else None, + user_agent=request.headers.get("User-Agent") + ) + + return { + "status": "success", + "message": "Request created. Please check your email for verification.", + "verification_token": gdpr_request.verification_token + } + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/gdpr/verify/{verification_token}") +async def verify_data_subject_request( + verification_token: str, + db: Session = Depends(get_db) +): + """Verify a data subject request""" + + verified = gdpr_service.verify_request(db=db, verification_token=verification_token) + + if not verified: + raise HTTPException(status_code=404, detail="Invalid verification token") + + return {"status": "success", "message": "Request verified"} + +@router.get("/gdpr/data/{user_id}") +async def get_user_data( + user_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """Get user data (GDPR access request)""" + # Users can only access their own data, unless admin + if current_user.id != user_id: + # Check if user is admin + from ..models.role import Role + role = db.query(Role).filter(Role.id == current_user.role_id).first() + if not role or role.name != "admin": + raise HTTPException(status_code=403, detail="Access denied") + + try: + data = gdpr_service.get_user_data(db=db, user_id=user_id) + return {"status": "success", "data": data} + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + +@router.delete("/gdpr/data/{user_id}") +async def delete_user_data( + user_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Delete user data (GDPR erasure request)""" + + success = gdpr_service.delete_user_data(db=db, user_id=user_id) + + if not success: + raise HTTPException(status_code=404, detail="User not found") + + return {"status": "success", "message": "User data deleted"} + +@router.get("/gdpr/export/{user_id}") +async def export_user_data( + user_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """Export user data (GDPR portability request)""" + if current_user.id != user_id: + # Check if user is admin + from ..models.role import Role + role = db.query(Role).filter(Role.id == current_user.role_id).first() + if not role or role.name != "admin": + raise HTTPException(status_code=403, detail="Access denied") + + try: + data = gdpr_service.export_user_data(db=db, user_id=user_id) + return {"status": "success", "data": data} + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + +# Security Scanning +@router.post("/scan/run") +async def run_security_scan( + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Run a manual security scan""" + try: + results = security_scan_service.run_full_scan(db=db) + return {"status": "success", "results": results} + except Exception as e: + import traceback + error_details = traceback.format_exc() + logger.error(f"Security scan failed: {str(e)}\n{error_details}") + raise HTTPException(status_code=500, detail=f"Scan failed: {str(e)}") + +@router.post("/scan/schedule") +async def schedule_security_scan( + interval_hours: int = Query(24, ge=1, le=168), # 1 hour to 1 week + db: Session = Depends(get_db), + current_user: User = Depends(authorize_roles("admin")) +): + """Schedule automatic security scans""" + try: + schedule = security_scan_service.schedule_scan(db=db, interval_hours=interval_hours) + return {"status": "success", "schedule": schedule} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to schedule scan: {str(e)}") + diff --git a/Backend/src/routes/service_booking_routes.py b/Backend/src/routes/service_booking_routes.py index 5c9bfb33..937ddcf2 100644 --- a/Backend/src/routes/service_booking_routes.py +++ b/Backend/src/routes/service_booking_routes.py @@ -7,6 +7,7 @@ import random from ..config.database import get_db from ..middleware.auth import get_current_user from ..models.user import User +from ..utils.role_helpers import is_admin from ..models.service import Service from ..models.service_booking import ( ServiceBooking, @@ -212,8 +213,7 @@ async def get_service_booking_by_id( if not booking: raise HTTPException(status_code=404, detail="Service booking not found") - - if booking.user_id != current_user.id and current_user.role_id != 1: + if not is_admin(current_user, db) and booking.user_id != current_user.id: raise HTTPException(status_code=403, detail="Forbidden") booking_dict = { @@ -281,10 +281,9 @@ async def create_service_stripe_payment_intent( if not booking: raise HTTPException(status_code=404, detail="Service booking not found") - if booking.user_id != current_user.id and current_user.role_id != 1: + if not is_admin(current_user, db) and booking.user_id != current_user.id: raise HTTPException(status_code=403, detail="Forbidden") - if abs(float(booking.total_amount) - amount) > 0.01: raise HTTPException( status_code=400, @@ -341,10 +340,9 @@ async def confirm_service_stripe_payment( if not booking: raise HTTPException(status_code=404, detail="Service booking not found") - if booking.user_id != current_user.id and current_user.role_id != 1: + if not is_admin(current_user, db) and booking.user_id != current_user.id: raise HTTPException(status_code=403, detail="Forbidden") - intent_data = StripeService.retrieve_payment_intent(payment_intent_id, db) if intent_data["status"] != "succeeded": diff --git a/Backend/src/routes/system_settings_routes.py b/Backend/src/routes/system_settings_routes.py index 5e53ec59..5da042b7 100644 --- a/Backend/src/routes/system_settings_routes.py +++ b/Backend/src/routes/system_settings_routes.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, status, Request, UploadFile, File +from fastapi import APIRouter, Depends, HTTPException, status, Request, UploadFile, File, Form from sqlalchemy.orm import Session from typing import Optional from datetime import datetime @@ -462,6 +462,366 @@ async def update_paypal_settings( db.rollback() raise HTTPException(status_code=500, detail=str(e)) +@router.get("/borica") +async def get_borica_settings( + current_user: User = Depends(authorize_roles("admin")), + db: Session = Depends(get_db) +): + try: + terminal_id_setting = db.query(SystemSettings).filter( + SystemSettings.key == "borica_terminal_id" + ).first() + + merchant_id_setting = db.query(SystemSettings).filter( + SystemSettings.key == "borica_merchant_id" + ).first() + + private_key_path_setting = db.query(SystemSettings).filter( + SystemSettings.key == "borica_private_key_path" + ).first() + + certificate_path_setting = db.query(SystemSettings).filter( + SystemSettings.key == "borica_certificate_path" + ).first() + + gateway_url_setting = db.query(SystemSettings).filter( + SystemSettings.key == "borica_gateway_url" + ).first() + + mode_setting = db.query(SystemSettings).filter( + SystemSettings.key == "borica_mode" + ).first() + + def mask_key(key_value: str) -> str: + if not key_value or len(key_value) < 4: + return "" + return "*" * (len(key_value) - 4) + key_value[-4:] + + result = { + "borica_terminal_id": "", + "borica_merchant_id": "", + "borica_private_key_path": "", + "borica_certificate_path": "", + "borica_gateway_url": "", + "borica_mode": "test", + "borica_terminal_id_masked": "", + "borica_merchant_id_masked": "", + "has_terminal_id": False, + "has_merchant_id": False, + "has_private_key_path": False, + "has_certificate_path": False, + } + + if terminal_id_setting: + result["borica_terminal_id"] = terminal_id_setting.value + result["borica_terminal_id_masked"] = mask_key(terminal_id_setting.value) if terminal_id_setting.value else "" + result["has_terminal_id"] = bool(terminal_id_setting.value) + result["updated_at"] = terminal_id_setting.updated_at.isoformat() if terminal_id_setting.updated_at else None + result["updated_by"] = terminal_id_setting.updated_by.full_name if terminal_id_setting.updated_by else None + + if merchant_id_setting: + result["borica_merchant_id"] = merchant_id_setting.value + result["borica_merchant_id_masked"] = mask_key(merchant_id_setting.value) if merchant_id_setting.value else "" + result["has_merchant_id"] = bool(merchant_id_setting.value) + + if private_key_path_setting: + result["borica_private_key_path"] = private_key_path_setting.value + result["has_private_key_path"] = bool(private_key_path_setting.value) + + if certificate_path_setting: + result["borica_certificate_path"] = certificate_path_setting.value + result["has_certificate_path"] = bool(certificate_path_setting.value) + + if gateway_url_setting: + result["borica_gateway_url"] = gateway_url_setting.value or "" + + if mode_setting: + result["borica_mode"] = mode_setting.value or "test" + + return { + "status": "success", + "data": result + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.put("/borica") +async def update_borica_settings( + borica_data: dict, + current_user: User = Depends(authorize_roles("admin")), + db: Session = Depends(get_db) +): + try: + terminal_id = borica_data.get("borica_terminal_id", "").strip() + merchant_id = borica_data.get("borica_merchant_id", "").strip() + private_key_path = borica_data.get("borica_private_key_path", "").strip() + certificate_path = borica_data.get("borica_certificate_path", "").strip() + gateway_url = borica_data.get("borica_gateway_url", "").strip() + mode = borica_data.get("borica_mode", "test").strip().lower() + + if mode and mode not in ["test", "production"]: + raise HTTPException( + status_code=400, + detail="Invalid Borica mode. Must be 'test' or 'production'" + ) + + if terminal_id: + setting = db.query(SystemSettings).filter( + SystemSettings.key == "borica_terminal_id" + ).first() + + if setting: + setting.value = terminal_id + setting.updated_by_id = current_user.id + else: + setting = SystemSettings( + key="borica_terminal_id", + value=terminal_id, + description="Borica Terminal ID for processing payments", + updated_by_id=current_user.id + ) + db.add(setting) + + if merchant_id: + setting = db.query(SystemSettings).filter( + SystemSettings.key == "borica_merchant_id" + ).first() + + if setting: + setting.value = merchant_id + setting.updated_by_id = current_user.id + else: + setting = SystemSettings( + key="borica_merchant_id", + value=merchant_id, + description="Borica Merchant ID for processing payments", + updated_by_id=current_user.id + ) + db.add(setting) + + if private_key_path: + setting = db.query(SystemSettings).filter( + SystemSettings.key == "borica_private_key_path" + ).first() + + if setting: + setting.value = private_key_path + setting.updated_by_id = current_user.id + else: + setting = SystemSettings( + key="borica_private_key_path", + value=private_key_path, + description="Path to Borica private key file", + updated_by_id=current_user.id + ) + db.add(setting) + + if certificate_path: + setting = db.query(SystemSettings).filter( + SystemSettings.key == "borica_certificate_path" + ).first() + + if setting: + setting.value = certificate_path + setting.updated_by_id = current_user.id + else: + setting = SystemSettings( + key="borica_certificate_path", + value=certificate_path, + description="Path to Borica certificate file", + updated_by_id=current_user.id + ) + db.add(setting) + + if gateway_url: + setting = db.query(SystemSettings).filter( + SystemSettings.key == "borica_gateway_url" + ).first() + + if setting: + setting.value = gateway_url + setting.updated_by_id = current_user.id + else: + setting = SystemSettings( + key="borica_gateway_url", + value=gateway_url, + description="Borica gateway URL (test or production)", + updated_by_id=current_user.id + ) + db.add(setting) + + if mode: + setting = db.query(SystemSettings).filter( + SystemSettings.key == "borica_mode" + ).first() + + if setting: + setting.value = mode + setting.updated_by_id = current_user.id + else: + setting = SystemSettings( + key="borica_mode", + value=mode, + description="Borica mode: test or production", + updated_by_id=current_user.id + ) + db.add(setting) + + db.commit() + + def mask_key(key_value: str) -> str: + if not key_value or len(key_value) < 4: + return "" + return "*" * (len(key_value) - 4) + key_value[-4:] + + return { + "status": "success", + "message": "Borica settings updated successfully", + "data": { + "borica_terminal_id": terminal_id if terminal_id else "", + "borica_merchant_id": merchant_id if merchant_id else "", + "borica_private_key_path": private_key_path if private_key_path else "", + "borica_certificate_path": certificate_path if certificate_path else "", + "borica_gateway_url": gateway_url if gateway_url else "", + "borica_mode": mode, + "borica_terminal_id_masked": mask_key(terminal_id) if terminal_id else "", + "borica_merchant_id_masked": mask_key(merchant_id) if merchant_id else "", + "has_terminal_id": bool(terminal_id), + "has_merchant_id": bool(merchant_id), + "has_private_key_path": bool(private_key_path), + "has_certificate_path": bool(certificate_path), + } + } + except HTTPException: + raise + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/borica/upload-certificate") +async def upload_borica_certificate( + file: UploadFile = File(...), + file_type: str = Form("private_key"), # "private_key" or "certificate" + current_user: User = Depends(authorize_roles("admin")), + db: Session = Depends(get_db) +): + """ + Upload Borica certificate or private key file. + file_type: "private_key" or "certificate" + """ + try: + if not file: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No file provided" + ) + + if not file.filename: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Filename is required" + ) + + # Validate file type + allowed_extensions = ['.pem', '.key', '.crt', '.cer', '.p12', '.pfx'] + file_ext = Path(file.filename).suffix.lower() + + if file_ext not in allowed_extensions: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid file type. Allowed extensions: {', '.join(allowed_extensions)}" + ) + + # Validate file_type parameter + if file_type not in ["private_key", "certificate"]: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="file_type must be 'private_key' or 'certificate'" + ) + + # Create upload directory + upload_dir = Path(__file__).parent.parent.parent / "uploads" / "certificates" / "borica" + upload_dir.mkdir(parents=True, exist_ok=True) + + # Generate unique filename + file_type_prefix = "private_key" if file_type == "private_key" else "certificate" + filename = f"borica_{file_type_prefix}_{uuid.uuid4()}{file_ext}" + file_path = upload_dir / filename + + # Read and save file + content = await file.read() + if not content: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="File is empty" + ) + + # Validate file size (max 1MB for certificate files) + max_size = 1024 * 1024 # 1MB + if len(content) > max_size: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"File size exceeds maximum allowed size of {max_size / 1024}KB" + ) + + # Save file + async with aiofiles.open(file_path, 'wb') as f: + await f.write(content) + + # Get absolute path + absolute_path = str(file_path.resolve()) + + # Update system settings with the new path + setting_key = "borica_private_key_path" if file_type == "private_key" else "borica_certificate_path" + + # Delete old file if exists + old_setting = db.query(SystemSettings).filter( + SystemSettings.key == setting_key + ).first() + + if old_setting and old_setting.value: + old_file_path = Path(old_setting.value) + # Only delete if it's in our uploads directory (safety check) + if old_file_path.exists() and str(old_file_path).startswith(str(upload_dir)): + try: + old_file_path.unlink() + except Exception as e: + logger.warning(f"Could not delete old file {old_setting.value}: {e}") + + # Update or create setting + if old_setting: + old_setting.value = absolute_path + old_setting.updated_by_id = current_user.id + else: + setting = SystemSettings( + key=setting_key, + value=absolute_path, + description=f"Path to Borica {file_type.replace('_', ' ')} file", + updated_by_id=current_user.id + ) + db.add(setting) + + db.commit() + + return { + "status": "success", + "message": f"Borica {file_type.replace('_', ' ')} uploaded successfully", + "data": { + "file_path": absolute_path, + "file_type": file_type, + "filename": filename + } + } + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error uploading Borica certificate: {e}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error uploading file: {str(e)}" + ) + @router.get("/smtp") async def get_smtp_settings( current_user: User = Depends(authorize_roles("admin")), diff --git a/Backend/src/routes/task_routes.py b/Backend/src/routes/task_routes.py new file mode 100644 index 00000000..cf3d94a5 --- /dev/null +++ b/Backend/src/routes/task_routes.py @@ -0,0 +1,418 @@ +from fastapi import APIRouter, Depends, HTTPException, Query, Body +from sqlalchemy.orm import Session +from typing import Optional, List, Dict, Any +from ..config.database import get_db +from ..middleware.auth import authorize_roles, get_current_user +from ..models.user import User +from ..models.workflow import TaskStatus, TaskPriority +from ..services.task_service import TaskService +from pydantic import BaseModel +from datetime import datetime + +router = APIRouter(prefix='/tasks', tags=['tasks']) + +# Request/Response Models +class TaskCreate(BaseModel): + title: str + description: Optional[str] = None + task_type: str = 'general' + priority: Optional[str] = 'medium' + workflow_instance_id: Optional[int] = None + booking_id: Optional[int] = None + room_id: Optional[int] = None + assigned_to: Optional[int] = None + due_date: Optional[str] = None + estimated_duration_minutes: Optional[int] = None + metadata: Optional[Dict[str, Any]] = None + +class TaskUpdate(BaseModel): + title: Optional[str] = None + description: Optional[str] = None + status: Optional[str] = None + priority: Optional[str] = None + assigned_to: Optional[int] = None + due_date: Optional[str] = None + notes: Optional[str] = None + actual_duration_minutes: Optional[int] = None + +class TaskCommentCreate(BaseModel): + comment: str + +# Task CRUD +@router.post('/') +async def create_task( + task_data: TaskCreate, + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Create a new task""" + try: + due_date = None + if task_data.due_date: + try: + due_date = datetime.fromisoformat(task_data.due_date.replace('Z', '+00:00')) + except: + due_date = datetime.strptime(task_data.due_date, '%Y-%m-%dT%H:%M:%S') + + task = TaskService.create_task( + db=db, + title=task_data.title, + created_by=current_user.id, + task_type=task_data.task_type, + description=task_data.description, + priority=TaskPriority(task_data.priority) if task_data.priority else TaskPriority.medium, + workflow_instance_id=task_data.workflow_instance_id, + booking_id=task_data.booking_id, + room_id=task_data.room_id, + assigned_to=task_data.assigned_to, + due_date=due_date, + estimated_duration_minutes=task_data.estimated_duration_minutes, + metadata=task_data.metadata + ) + return {'status': 'success', 'data': { + 'id': task.id, + 'title': task.title, + 'status': task.status.value, + 'priority': task.priority.value, + 'created_at': task.created_at.isoformat() + }} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/') +async def get_tasks( + assigned_to: Optional[int] = Query(None), + created_by: Optional[int] = Query(None), + status: Optional[str] = Query(None), + priority: Optional[str] = Query(None), + task_type: Optional[str] = Query(None), + booking_id: Optional[int] = Query(None), + room_id: Optional[int] = Query(None), + workflow_instance_id: Optional[int] = Query(None), + overdue_only: bool = Query(False), + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=1000), + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get tasks""" + try: + tasks = TaskService.get_tasks( + db=db, + assigned_to=assigned_to, + created_by=created_by, + status=TaskStatus(status) if status else None, + priority=TaskPriority(priority) if priority else None, + task_type=task_type, + booking_id=booking_id, + room_id=room_id, + workflow_instance_id=workflow_instance_id, + overdue_only=overdue_only, + skip=skip, + limit=limit + ) + + # Build response data safely + result = [] + for t in tasks: + try: + result.append({ + 'id': t.id, + 'title': t.title, + 'description': t.description, + 'task_type': t.task_type, + 'status': t.status.value, + 'priority': t.priority.value, + 'workflow_instance_id': t.workflow_instance_id, + 'booking_id': t.booking_id, + 'room_id': t.room_id, + 'assigned_to': t.assigned_to, + 'assigned_to_name': t.assignee.full_name if t.assignee else None, + 'created_by': t.created_by, + 'due_date': t.due_date.isoformat() if t.due_date else None, + 'completed_at': t.completed_at.isoformat() if t.completed_at else None, + 'estimated_duration_minutes': t.estimated_duration_minutes, + 'actual_duration_minutes': t.actual_duration_minutes, + 'notes': t.notes, + 'created_at': t.created_at.isoformat() if t.created_at else None, + 'updated_at': t.updated_at.isoformat() if t.updated_at else None + }) + except Exception as task_error: + # Log the error for this specific task but continue with others + import logging + logger = logging.getLogger(__name__) + logger.error(f"Error serializing task {t.id}: {str(task_error)}") + continue + + return {'status': 'success', 'data': result} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f"Error in get_tasks: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/my-tasks') +async def get_my_tasks( + status: Optional[str] = Query(None), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Get tasks assigned to current user""" + try: + tasks = TaskService.get_my_tasks( + db=db, + user_id=current_user.id, + status=TaskStatus(status) if status else None + ) + return {'status': 'success', 'data': [{ + 'id': t.id, + 'title': t.title, + 'description': t.description, + 'task_type': t.task_type, + 'status': t.status.value, + 'priority': t.priority.value, + 'due_date': t.due_date.isoformat() if t.due_date else None, + 'created_at': t.created_at.isoformat() + } for t in tasks]} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/{task_id}') +async def get_task( + task_id: int, + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get task by ID""" + try: + task = TaskService.get_task_by_id(db, task_id) + if not task: + raise HTTPException(status_code=404, detail='Task not found') + + comments = TaskService.get_task_comments(db, task_id) + + return {'status': 'success', 'data': { + 'id': task.id, + 'title': task.title, + 'description': task.description, + 'task_type': task.task_type, + 'status': task.status.value, + 'priority': task.priority.value, + 'workflow_instance_id': task.workflow_instance_id, + 'booking_id': task.booking_id, + 'room_id': task.room_id, + 'assigned_to': task.assigned_to, + 'assigned_to_name': task.assignee.full_name if task.assignee else None, + 'created_by': task.created_by, + 'created_by_name': task.creator_user.full_name if task.creator_user else None, + 'due_date': task.due_date.isoformat() if task.due_date else None, + 'completed_at': task.completed_at.isoformat() if task.completed_at else None, + 'estimated_duration_minutes': task.estimated_duration_minutes, + 'actual_duration_minutes': task.actual_duration_minutes, + 'notes': task.notes, + 'metadata': task.meta_data, + 'comments': [{ + 'id': c.id, + 'user_id': c.user_id, + 'user_name': c.user.full_name if c.user else None, + 'comment': c.comment, + 'created_at': c.created_at.isoformat() + } for c in comments], + 'created_at': task.created_at.isoformat(), + 'updated_at': task.updated_at.isoformat() + }} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.put('/{task_id}') +async def update_task( + task_id: int, + task_data: TaskUpdate, + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Update task""" + try: + due_date = None + if task_data.due_date: + try: + due_date = datetime.fromisoformat(task_data.due_date.replace('Z', '+00:00')) + except: + due_date = datetime.strptime(task_data.due_date, '%Y-%m-%dT%H:%M:%S') + + task = TaskService.update_task( + db=db, + task_id=task_id, + title=task_data.title, + description=task_data.description, + status=TaskStatus(task_data.status) if task_data.status else None, + priority=TaskPriority(task_data.priority) if task_data.priority else None, + assigned_to=task_data.assigned_to, + due_date=due_date, + notes=task_data.notes, + actual_duration_minutes=task_data.actual_duration_minutes + ) + if not task: + raise HTTPException(status_code=404, detail='Task not found') + + return {'status': 'success', 'data': { + 'id': task.id, + 'status': task.status.value, + 'updated_at': task.updated_at.isoformat() + }} + except HTTPException: + raise + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post('/{task_id}/assign') +async def assign_task( + task_id: int, + user_id: int = Body(..., embed=True), + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Assign task to a user""" + try: + task = TaskService.assign_task(db, task_id, user_id) + if not task: + raise HTTPException(status_code=404, detail='Task not found') + + return {'status': 'success', 'data': { + 'id': task.id, + 'assigned_to': task.assigned_to, + 'status': task.status.value + }} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post('/{task_id}/start') +async def start_task( + task_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Start task (mark as in progress)""" + try: + task = TaskService.start_task(db, task_id) + if not task: + raise HTTPException(status_code=404, detail='Task not found') + + return {'status': 'success', 'data': { + 'id': task.id, + 'status': task.status.value + }} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post('/{task_id}/complete') +async def complete_task( + task_id: int, + notes: Optional[str] = Body(None, embed=True), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Complete task""" + try: + task = TaskService.complete_task(db, task_id, notes) + if not task: + raise HTTPException(status_code=404, detail='Task not found') + + return {'status': 'success', 'data': { + 'id': task.id, + 'status': task.status.value, + 'completed_at': task.completed_at.isoformat() if task.completed_at else None + }} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post('/{task_id}/cancel') +async def cancel_task( + task_id: int, + reason: Optional[str] = Body(None, embed=True), + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Cancel task""" + try: + task = TaskService.cancel_task(db, task_id, reason) + if not task: + raise HTTPException(status_code=404, detail='Task not found') + + return {'status': 'success', 'data': { + 'id': task.id, + 'status': task.status.value + }} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post('/{task_id}/comments') +async def add_task_comment( + task_id: int, + comment_data: TaskCommentCreate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Add comment to task""" + try: + comment = TaskService.add_task_comment( + db=db, + task_id=task_id, + user_id=current_user.id, + comment=comment_data.comment + ) + return {'status': 'success', 'data': { + 'id': comment.id, + 'user_id': comment.user_id, + 'user_name': comment.user.full_name if comment.user else None, + 'comment': comment.comment, + 'created_at': comment.created_at.isoformat() + }} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/statistics/') +async def get_task_statistics( + assigned_to: Optional[int] = Query(None), + start_date: Optional[str] = Query(None), + end_date: Optional[str] = Query(None), + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get task statistics""" + try: + start = None + end = None + if start_date: + start = datetime.fromisoformat(start_date.replace('Z', '+00:00')) + if end_date: + end = datetime.fromisoformat(end_date.replace('Z', '+00:00')) + + stats = TaskService.get_task_statistics( + db=db, + assigned_to=assigned_to or (current_user.id if current_user.role.name != 'admin' else None), + start_date=start, + end_date=end + ) + return {'status': 'success', 'data': stats} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + diff --git a/Backend/src/routes/user_routes.py b/Backend/src/routes/user_routes.py index 7c4ae2ed..a0300fd1 100644 --- a/Backend/src/routes/user_routes.py +++ b/Backend/src/routes/user_routes.py @@ -8,6 +8,8 @@ from ..middleware.auth import get_current_user, authorize_roles from ..models.user import User from ..models.role import Role from ..models.booking import Booking, BookingStatus +from ..utils.role_helpers import can_manage_users +from ..utils.response_helpers import success_response router = APIRouter(prefix='/users', tags=['users']) @router.get('/', dependencies=[Depends(authorize_roles('admin'))]) @@ -30,7 +32,7 @@ async def get_users(search: Optional[str]=Query(None), role: Optional[str]=Query for user in users: user_dict = {'id': user.id, 'email': user.email, 'full_name': user.full_name, 'phone': user.phone, 'phone_number': user.phone, 'address': user.address, 'avatar': user.avatar, 'currency': getattr(user, 'currency', 'VND'), 'is_active': user.is_active, 'status': 'active' if user.is_active else 'inactive', 'role_id': user.role_id, 'role': user.role.name if user.role else 'customer', 'created_at': user.created_at.isoformat() if user.created_at else None, 'updated_at': user.updated_at.isoformat() if user.updated_at else None} result.append(user_dict) - return {'status': 'success', 'data': {'users': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}} + return success_response(data={'users': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -42,7 +44,7 @@ async def get_user_by_id(id: int, current_user: User=Depends(authorize_roles('ad raise HTTPException(status_code=404, detail='User not found') bookings = db.query(Booking).filter(Booking.user_id == id).order_by(Booking.created_at.desc()).limit(5).all() user_dict = {'id': user.id, 'email': user.email, 'full_name': user.full_name, 'phone': user.phone, 'phone_number': user.phone, 'address': user.address, 'avatar': user.avatar, 'currency': getattr(user, 'currency', 'VND'), 'is_active': user.is_active, 'status': 'active' if user.is_active else 'inactive', 'role_id': user.role_id, 'role': user.role.name if user.role else 'customer', 'created_at': user.created_at.isoformat() if user.created_at else None, 'updated_at': user.updated_at.isoformat() if user.updated_at else None, 'bookings': [{'id': b.id, 'booking_number': b.booking_number, 'status': b.status.value if isinstance(b.status, BookingStatus) else b.status, 'created_at': b.created_at.isoformat() if b.created_at else None} for b in bookings]} - return {'status': 'success', 'data': {'user': user_dict}} + return success_response(data={'user': user_dict}) except HTTPException: raise except Exception as e: @@ -70,7 +72,7 @@ async def create_user(user_data: dict, current_user: User=Depends(authorize_role db.commit() db.refresh(user) user_dict = {'id': user.id, 'email': user.email, 'full_name': user.full_name, 'phone': user.phone, 'phone_number': user.phone, 'currency': getattr(user, 'currency', 'VND'), 'role_id': user.role_id, 'is_active': user.is_active} - return {'status': 'success', 'message': 'User created successfully', 'data': {'user': user_dict}} + return success_response(data={'user': user_dict}, message='User created successfully') except HTTPException: raise except Exception as e: @@ -80,7 +82,7 @@ async def create_user(user_data: dict, current_user: User=Depends(authorize_role @router.put('/{id}') async def update_user(id: int, user_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): try: - if current_user.role_id != 1 and current_user.id != id: + if not can_manage_users(current_user, db) and current_user.id != id: raise HTTPException(status_code=403, detail='Forbidden') user = db.query(User).filter(User.id == id).first() if not user: @@ -93,13 +95,13 @@ async def update_user(id: int, user_data: dict, current_user: User=Depends(get_c role_map = {'admin': 1, 'staff': 2, 'customer': 3, 'accountant': 4} if 'full_name' in user_data: user.full_name = user_data['full_name'] - if 'email' in user_data and current_user.role_id == 1: + if 'email' in user_data and can_manage_users(current_user, db): user.email = user_data['email'] if 'phone_number' in user_data: user.phone = user_data['phone_number'] - if 'role' in user_data and current_user.role_id == 1: + if 'role' in user_data and can_manage_users(current_user, db): user.role_id = role_map.get(user_data['role'], 3) - if 'status' in user_data and current_user.role_id == 1: + if 'status' in user_data and can_manage_users(current_user, db): user.is_active = user_data['status'] == 'active' if 'currency' in user_data: currency = user_data['currency'] @@ -112,7 +114,7 @@ async def update_user(id: int, user_data: dict, current_user: User=Depends(get_c db.commit() db.refresh(user) user_dict = {'id': user.id, 'email': user.email, 'full_name': user.full_name, 'phone': user.phone, 'phone_number': user.phone, 'currency': getattr(user, 'currency', 'VND'), 'role_id': user.role_id, 'is_active': user.is_active} - return {'status': 'success', 'message': 'User updated successfully', 'data': {'user': user_dict}} + return success_response(data={'user': user_dict}, message='User updated successfully') except HTTPException: raise except Exception as e: @@ -130,7 +132,7 @@ async def delete_user(id: int, current_user: User=Depends(authorize_roles('admin raise HTTPException(status_code=400, detail='Cannot delete user with active bookings') db.delete(user) db.commit() - return {'status': 'success', 'message': 'User deleted successfully'} + return success_response(message='User deleted successfully') except HTTPException: raise except Exception as e: diff --git a/Backend/src/routes/workflow_routes.py b/Backend/src/routes/workflow_routes.py new file mode 100644 index 00000000..1a1ef555 --- /dev/null +++ b/Backend/src/routes/workflow_routes.py @@ -0,0 +1,314 @@ +from fastapi import APIRouter, Depends, HTTPException, Query, Body +from sqlalchemy.orm import Session +from typing import Optional, List, Dict, Any +from ..config.database import get_db +from ..middleware.auth import authorize_roles, get_current_user +from ..models.user import User +from ..models.workflow import WorkflowType, WorkflowStatus, WorkflowTrigger +from ..services.workflow_service import WorkflowService +from pydantic import BaseModel + +router = APIRouter(prefix='/workflows', tags=['workflows']) + +# Request/Response Models +class WorkflowCreate(BaseModel): + name: str + description: Optional[str] = None + workflow_type: str + trigger: str + steps: List[Dict[str, Any]] + trigger_config: Optional[Dict[str, Any]] = None + sla_hours: Optional[int] = None + +class WorkflowUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + steps: Optional[List[Dict[str, Any]]] = None + status: Optional[str] = None + trigger_config: Optional[Dict[str, Any]] = None + sla_hours: Optional[int] = None + +class WorkflowTriggerRequest(BaseModel): + booking_id: Optional[int] = None + room_id: Optional[int] = None + user_id: Optional[int] = None + metadata: Optional[Dict[str, Any]] = None + +# Workflow CRUD +@router.post('/') +async def create_workflow( + workflow_data: WorkflowCreate, + current_user: User = Depends(authorize_roles('admin')), + db: Session = Depends(get_db) +): + """Create a new workflow""" + try: + workflow = WorkflowService.create_workflow( + db=db, + name=workflow_data.name, + workflow_type=WorkflowType(workflow_data.workflow_type), + trigger=WorkflowTrigger(workflow_data.trigger), + steps=workflow_data.steps, + created_by=current_user.id, + description=workflow_data.description, + trigger_config=workflow_data.trigger_config, + sla_hours=workflow_data.sla_hours + ) + return {'status': 'success', 'data': { + 'id': workflow.id, + 'name': workflow.name, + 'workflow_type': workflow.workflow_type.value, + 'trigger': workflow.trigger.value, + 'status': workflow.status.value, + 'created_at': workflow.created_at.isoformat() + }} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/') +async def get_workflows( + workflow_type: Optional[str] = Query(None), + status: Optional[str] = Query(None), + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=1000), + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get workflows""" + try: + workflows = WorkflowService.get_workflows( + db=db, + workflow_type=WorkflowType(workflow_type) if workflow_type else None, + status=WorkflowStatus(status) if status else None, + skip=skip, + limit=limit + ) + return {'status': 'success', 'data': [{ + 'id': w.id, + 'name': w.name, + 'description': w.description, + 'workflow_type': w.workflow_type.value, + 'trigger': w.trigger.value, + 'status': w.status.value, + 'sla_hours': w.sla_hours, + 'steps': w.steps, + 'trigger_config': w.trigger_config, + 'created_at': w.created_at.isoformat(), + 'updated_at': w.updated_at.isoformat() + } for w in workflows]} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/{workflow_id}') +async def get_workflow( + workflow_id: int, + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get workflow by ID""" + try: + workflow = WorkflowService.get_workflow_by_id(db, workflow_id) + if not workflow: + raise HTTPException(status_code=404, detail='Workflow not found') + + return {'status': 'success', 'data': { + 'id': workflow.id, + 'name': workflow.name, + 'description': workflow.description, + 'workflow_type': workflow.workflow_type.value, + 'trigger': workflow.trigger.value, + 'status': workflow.status.value, + 'sla_hours': workflow.sla_hours, + 'steps': workflow.steps, + 'trigger_config': workflow.trigger_config, + 'created_at': workflow.created_at.isoformat(), + 'updated_at': workflow.updated_at.isoformat() + }} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.put('/{workflow_id}') +async def update_workflow( + workflow_id: int, + workflow_data: WorkflowUpdate, + current_user: User = Depends(authorize_roles('admin')), + db: Session = Depends(get_db) +): + """Update workflow""" + try: + workflow = WorkflowService.update_workflow( + db=db, + workflow_id=workflow_id, + name=workflow_data.name, + description=workflow_data.description, + steps=workflow_data.steps, + status=WorkflowStatus(workflow_data.status) if workflow_data.status else None, + trigger_config=workflow_data.trigger_config, + sla_hours=workflow_data.sla_hours + ) + if not workflow: + raise HTTPException(status_code=404, detail='Workflow not found') + + return {'status': 'success', 'data': { + 'id': workflow.id, + 'name': workflow.name, + 'status': workflow.status.value, + 'updated_at': workflow.updated_at.isoformat() + }} + except HTTPException: + raise + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.delete('/{workflow_id}') +async def delete_workflow( + workflow_id: int, + current_user: User = Depends(authorize_roles('admin')), + db: Session = Depends(get_db) +): + """Delete workflow""" + try: + success = WorkflowService.delete_workflow(db, workflow_id) + if not success: + raise HTTPException(status_code=404, detail='Workflow not found') + + return {'status': 'success', 'message': 'Workflow deleted successfully'} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +# Workflow Instances +@router.post('/{workflow_id}/trigger') +async def trigger_workflow( + workflow_id: int, + trigger_data: WorkflowTriggerRequest, + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Trigger a workflow""" + try: + instance = WorkflowService.trigger_workflow( + db=db, + workflow_id=workflow_id, + booking_id=trigger_data.booking_id, + room_id=trigger_data.room_id, + user_id=trigger_data.user_id, + metadata=trigger_data.metadata + ) + if not instance: + raise HTTPException(status_code=404, detail='Workflow not found or inactive') + + return {'status': 'success', 'data': { + 'id': instance.id, + 'workflow_id': instance.workflow_id, + 'status': instance.status, + 'started_at': instance.started_at.isoformat(), + 'due_date': instance.due_date.isoformat() if instance.due_date else None + }} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/instances/') +async def get_workflow_instances( + workflow_id: Optional[int] = Query(None), + booking_id: Optional[int] = Query(None), + status: Optional[str] = Query(None), + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=1000), + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get workflow instances""" + try: + instances = WorkflowService.get_workflow_instances( + db=db, + workflow_id=workflow_id, + booking_id=booking_id, + status=status, + skip=skip, + limit=limit + ) + return {'status': 'success', 'data': [{ + 'id': i.id, + 'workflow_id': i.workflow_id, + 'booking_id': i.booking_id, + 'room_id': i.room_id, + 'user_id': i.user_id, + 'status': i.status, + 'started_at': i.started_at.isoformat(), + 'completed_at': i.completed_at.isoformat() if i.completed_at else None, + 'due_date': i.due_date.isoformat() if i.due_date else None + } for i in instances]} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post('/instances/{instance_id}/complete') +async def complete_workflow_instance( + instance_id: int, + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Complete workflow instance""" + try: + instance = WorkflowService.complete_workflow_instance(db, instance_id) + if not instance: + raise HTTPException(status_code=404, detail='Workflow instance not found') + + return {'status': 'success', 'data': { + 'id': instance.id, + 'status': instance.status, + 'completed_at': instance.completed_at.isoformat() if instance.completed_at else None + }} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +# Predefined workflow types +@router.get('/types/pre-arrival') +async def get_pre_arrival_workflows( + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get pre-arrival workflows""" + try: + workflows = WorkflowService.get_pre_arrival_workflows(db) + return {'status': 'success', 'data': [{ + 'id': w.id, + 'name': w.name, + 'description': w.description, + 'trigger': w.trigger.value, + 'sla_hours': w.sla_hours + } for w in workflows]} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get('/types/room-preparation') +async def get_room_preparation_workflows( + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get room preparation workflows""" + try: + workflows = WorkflowService.get_room_preparation_workflows(db) + return {'status': 'success', 'data': [{ + 'id': w.id, + 'name': w.name, + 'description': w.description, + 'trigger': w.trigger.value, + 'sla_hours': w.sla_hours + } for w in workflows]} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + diff --git a/Backend/src/services/__pycache__/analytics_service.cpython-312.pyc b/Backend/src/services/__pycache__/analytics_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e21e33874bb7abdb6923013178e112aac4e0ebf9 GIT binary patch literal 40144 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 Ohf*~5Ck(peNB=*coIZvC literal 0 HcmV?d00001 diff --git a/Backend/src/services/__pycache__/borica_service.cpython-312.pyc b/Backend/src/services/__pycache__/borica_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ee49d46f8977b54f14cd0ffc383b44ac085fb89 GIT binary patch literal 18549 zcmeHvYfxKPn&7>9za=Drgm?)s7=ZzUpK%8qUIvT}ZbSSa53S6-fMf}gD`5;H(wS^( zifntU@pN`fD(Q(`liHAqXNGLg?y$AJRi18Fx2tA;=vvfDz9H4=n*7-O*eW@8RkF_3 z)_&iS?v=o@{ibk29r`Of>h-|L*~zc(6n6a?;n*7*O!QHuIUd=P^&3p_YQ zQ`9wzp%^+yjb~DNoF>n*aT!VF<8qQJ#+8uDf~t^uTpiMkYeL#_Esb^LL0w2Ut^-)X zD1-WtVcbAqRnQnRjhjN|adXHrZV45Q7m<8*&>FIh+d}qnJ9*Xwi$f*jB>+>5Hs}aB z$DIVGgQer8GK!@F`^AChQnu_97(0gXaz@8G89l3JD@x&cQo$JBQjAwVmTQFEs>gCo zkn4Ud*9^JU+1v_f(E`skdCx`gTq~pcDaQI1#n?WPk)crIyB27V{VRMJ_Q(az$ZXUf zp78|*MX!G%D#*KL7Cf||VSG_G>JPDBVKvN%u+gZ0W-8)Q2dtE+(0V0%AG*9=Hj4XsH+#$S)^I6cx=*nxe$52#AE0#lR$o6;S16XF-O( z*pOFW`#8oY*{mfxMsv;>9Ux@A9gi~<&L5?sCJ{4W5uselS)N`#3M4s7)lyM|SZ5c- z$!Y35b)|Wpx-36W&C?#)uwcCq=KK>rZvTB8woc7lZtZ0w7o*|X)`4)84K`W8dAz zzAJ~5`g&epA6Jar*$>gT50#Xm_-l$*8t-WgX*r~i9$2C8H@nKaY3i4>w%aKGrGbX{ z4+L9?pnqS24YAzBv~MO#2$m~`l_uoMl_2E6$cYiMRnCRcHVQr2ce-bwYj_MOlq-kQ zNQNxAN`SY~R9-h+9TbwL+yHRZ5dC2*|2bRf$iBC;oc|Jt6)!Lt@2oF6?a@B-emb%o z=hW%JGhJhS-l4wp-cwy;1CMWNSstU<*&*kFy{v`bgUb*-1Gn(;?Y@NdLee_TTc;D& zz>~Pe-owe(k-M!U$Ss<9eN$XPK8RBace@ zO8_O}j&O?J{gr5^n=a4*4}$Bsf_hTSfn`hn`scAfXWc>08(=GfVwZ-Vv6i6+HW zz)A=TtP?>^*X?#cF17MT$sGoy1MrLd0-|T&ZO^I+8Om1M6Z*_knul_F|7W7#ejPxWZqHHAkdq`h> zR{EYC>Fs+=;&N#ElBf%*@6xkS-yI|0{$j#XQWPu06@xLw9Qrplj-8*W_9DzsoO zGb&cYs96=Gfu9zBI{4{XEvrLK6YA+%6>FH35p&)I=DbpB!3b^J7$e}D0AH2GS52xI za~_t7U|I6p+19=p@Qd>BE!lDHtf7N>Xjx@}QE9;N&yA{RLFuuh?Tu#OXvWZ5jOl=_ zxHEa~*^E{1&N3Bu*7|Z!hB-HIK(NaqV7|MP@u!ETpkZgJ}2ds0ktbldT(4y_X zL^HA&?A?2c82ujgGx;dxQ7ks~vokCQ{M_wx&&~M3Aa+MN-%P|u_`Q3um&`x2M2`%0 z=j%dsOx-};$+}TN(LFLUB*^z3WPoR*=Ri=+MJHy$^WZIsa+Byj@yIyvY*B(1A0km- zXm*I?#=5HmSeGQ+dj2C0qqnt9chK=4GK)w;)-h{S4R@|x}?2@x3{dlp0IZ= zYg0wVD}n2_cyZlz|Mi#R=C-(|?EzGwh6R-`6rKY&j%H$xT9enFSTNg$9574jm?lyIBhnY@WyYM>gXBudbpvxzaWd#d zAC=S~MkdbZO*)D`2oj<;5kgX1gc*rgV!EV_X&J?PiX~lCQg{Mr&h7{jQRBP4BQUwO z)W_tUGA3teM&+Vr6pT711LBq~85kJzd*H9cXGRy(L9CDIp-i)6Byr z^Nz4&ilXUELZDiNa%s#MGl}Zjk~s$Z5|hPr;QFe~;fRnpznCGWkC|hd7245&zT-xM4X~ z(C4d+C$Uf!<)YxQ^;n57jW|DuE=e?14rzqjg%RqOg6<4D1p7EH430uS@D3#hEQBH4 zkLyLWK8(=L=D;eTxK4-!n^$6BxsfR6W@lN>W62vanE)9!jx3`FpF!v>M(439LxyL0 z!HEe&&}E;1-3rC|EL38*LQ#gL<+C%BGzc$}hG2QjPk=Hfg0=?IRes2-;1h%=< zaBr3r7k6#v+J&)jLJZ`dHl2xU2(&YR^aJ8|H+&E$KeOyN_&iCfvv4nu_}>s=Z@% zXic$ZKIL?$O56~CV0zb-a5kjM_M}Q%Ql&Lm=Gv7iDZTG7SafO1pw)2$NHhxE zW{w4LB42q>Lk5vt%S3X4^ZsrWvNEBNno;QRASS67+|NOA1QjDF4=I*pA?1=B&WLC@ zdxG<*B?W;MK{%6IQqok8^T%l5<=Igkpg@wz1L)P<5n`%90VyDIP(~%AfQM)6+*E&( zm98L#RrpVApQe7K4oF--*99D&ptZRpFd9)}#S{Vbv+oFEe=%7hNg$R=W6?w<84y5* zvLnQ#-U7RK6v1O|EMVlL#zR72uL$MR@0*J_r@+2k9T5`83~Q!ykzCh| z;op_(nFAGYBOr4l0ghX0mb6iH|Bw)97NJ}UGFXc;S?n9zV^mAp*R@xsGU+@mIU)3m zpXx-GBk86R$I8=2iOAU|0S7_hmRuEkK;(fw9+?i$1sV5T1oT=^%|Wf5^92EoxMJPl zigoAtv~q&)7%2T{C4$Dk$34&P4hBgRK6k_)nhmn8rQ)+?~hLB&tMM>4J{KKFN`lM>W;X-!cFqH1X(yDD9}A9sMyQ?NHii; zPH+pe(eM=Ko1I>0nf5J)e9;zI6Ho~Jm)HpBhl*SPBEj=?%obl{ArxYxU{rBI#4j&| zpPf&Hn?V#sc$S?J?7^^)@y@a#Z=UoK6d+g_L4lKi!Ol#ewXQie)HB+4sqKKE7#-+p z6I2&`5%%DIL3`og{w#`$VG)%afC#E!cxnm`^3?M_ZU)vxP~)xy{cw^RnfA3GI4EeR z*~^T73YN>G@I56~hmA_woAC`0*&vX9g^)E}8{nBuKRH{CC!WyQu<>2Fk2(GTg*Ehj&>418H! z8h16W^(Knj(v-5OWc^&KrZrj9%h&XxG%&tve9xS!uDj=M1-X@0Q?-5c14>a_vNH6I z9MIDys-huTzK1X0ld?On4P70&u1?q+QueZ>eHU-vm9W>R>@Ljz>B$FbMTK=m`%p(Y zE0d1hykqy8HsNSbI-cVl&u#2ZIJ)DOZYZ@le)9U;udgy6TzvQ9+U`VUd&0agY3|_7 z9SQT%Tk>1hxVbm3>HVe_`^PbV_s~VzE8zSP=C)-ixn{a*idUbDpC6B(dnsP^a?UXT|)V2Aq@Im{>FDD!X> zPiRM#hogJ~jyexV^#mME9*+76INCfM%@c6Its<>}7VIWDSWkwS0%EY8AT|-(${bSz zY>8es150sj07&mn=eZy0+-;;mnAc3B9df}=^2MOXV63}F+-q` zg=cx1EeCptQIw)$TT4ZZ6aR_NpoJ6#>K>nmSZ~Q1l{A2WBxiH@F-eXt*<#i}Q?5jW zVz!vBpd2n0S1M)>NLqwgE9XXvnF4LOToDq-_epjaX==e0eNHURjSAhdJM@MHNpENu z@rmAGk4pE30*AzsT*{Qi?2qXU&L#Wn_60dZ(wyD^I?3ttk1?BL_ za8I8gJKe*bhE-YAh=%f_I%_F&qma*yVWD~t!R}a8>^a@jk!O-siXwAb9AE zfV+q_!VnE}9K>UbRimQH;J$=&!{bEITHKRF>p0?GEF<=Arf>lj4Qbv-+@1ss2=B{8V zIn4UC+^W+q7HMy5p+BQ0WOGzkTQ z9Lj&eEG?z?!UE`T+>aqQf^M;#R*?r~%} zb^EoYUpVWMP7m+&tTn86C!B|t2k+U+Q;u@5Im;T7t|s2qw03+$mT>)Gc_;;@X>mPY zTz~yCSn9?5mye|!yONFu-qEn;Sg%Pq4lEB8z>#`rTIUkZBg=y+dv(%Y$J^_!PpxqY zd;4-OyIWkZ+@$wn|aC3_K*5!o3`D$mN6hry8Vq5J5~sv{fin^m3v(2zVH z;ExC5hVtK;t3K@hsQ*U)N5eOULhna_&w!>yyrA-r2l1m2e(RIjfS+2Hx3_ za5ko#l^^Ke)xT$as8tr1K#S$>hX%^-T-E;cXxvcqO%wKmWBTr)n{rlvP05O?w?M5s zarH#Jw);fQKzRg4i?EaEnE7D5v#Grd>=9PiC=LKOXnBm@e)e|e?jB3>??l)+8HD|t9C z=gLF~M$ja}Gnc-1E(K_s%;kJcpQo2-0>FM%r>bYMcPolC`@XzESqXUX`wah{GcVmZ`2wq%S>iq8SGzjp+tG-gDn z5>tHBCFj7|Gqu?nTR~?1ehXk>uKeKM}^WG`NBSek2XKC`LRjf+^de7fzP&!#acbj2AU> zrEs4NKCKf>`4=DH>Xw9!-|z|GumQiywySkGUp)PM@CHN~voc1#au9t8Ky;9|Gvs|B zClPcGhpX{TY3YhLhC83TlBuviO)D_9(f(0#pu+T<~E@ivk~^ zfdec`mITfRWi5LJdfWB6N80D{UdiQIu5)qkcE`^|hQ7ob8ZFNU!xO$>q_ah;@e&Yj zWIy!yU#V~3puVQ$)z(ylC*^Qvj(cu)B%8YVrtZ6r?uT-!*tJ^yLBqQZ*GBHk0r9^_ za2eiwzLOr?wy-M5wZZQJimhv4i@!lHjnYoZRd?66Q*ED;9+=N%sNdft~Z}Gr0 zF5UKOL0rEkX!Q*$Wh;x@YExxZTegbT>6;b5(BFO~-Y~jp8%vede$e@DXWR}8TeV^) zw=sr28bJm(KJi{;#<6%4p})k4kilOgv<}f?b;j|U-AwenZhwK`wZS=Fp78o-Cc}%a z$I!Fy4d5LlH<7r1i*3Jz(JDs&9wI>pOUOkdc&f~yE+Oa!XBhS}X)JTaAN>iL56;kU znEBug{S}!xknxf^;3mNFEV*Kvp=MR)-Oo(yQ&J4{4adxQ-G35T*EAfB!{?89Oq;!Y zd0|1&Wo`~JUSAaMuHZ$?zro&+P%@XSaCI_khg&WvCg*}d!IEWH5ii5eh9l62Aq&83 z0DM6$X3OC$UXVxTLU`s*ZZJj2^27Ns;V7I58`VxAVFlsf9p6d9t^w3X!%_Hp6wZRd z3#|1=!g#|DhNGvzeGzxEHx!x5FhDhU zrOCI#aBqMNFf%?~wsV*b_Y182V~8R+T2ZcK(u|wfyn_iWRy>u^zs0sXz|1EMA#LrJ zX?y7}ZSJJ)2yZ*G(Q&6{({^&J#2v5hOOzaoTaJNm7JRxp{Jy32$c!8Iy-B^lXsiIO96%aJcioY$tWPQO2&DA}DXY2iy+ zZqEPp;>U{{<9us>qGTXhGR&6@CrVB&_usR*NX`9;k^^ze0hp7ecvbPQmcDS6Bg6af zP{P%ebhY!Y_ANXoujlRcYmK~pKY0S6ac!EnAC#VMhLSDaJp9?a;RwCV`i+LNIj`3J zbaz@y6*=#Nqqc0T`AD+4i*N2)>EEEUOkv7IUcthhao#kcT9K7;z(L*cuURo z0B_k#p6Yo^{aOQWX_uaEHYXc9dHAz*!dM+97^~U-Hv8i#7^~4v#+rK8Rmhw7wzcTt zTY7kVkF*wL@LBSqWLYy`)|@PB=gZm?W&0D>1AohY%KjYtC2M4HY#$;71U)8!| zfbW@;wx+wbruF{h{(-yu2exWklC=l<+Jh@6zI0WmsywN(>aE(n@P!3mdni@iOyDDY z?U7V%(-vF+%sA>(<=~)SzZiFQKP=LgSy$9)J5^-6rn;(HnSaOpg~9Zb!M6vm45a`# z{`T>e`AtI=QjsHRuH(&hH!Cs|Ntj2i zKA_~4))ie^2V|18?dEN}Z~h?J)X6t>-nDh6np%-!8d8qRYp-8@9e3|2-+1(PMY8K0 z-*qnTInO)JOAEKE{+TINT%IiU@Wq~+FJ(HpTilUyRDby5+M!IViMpeA9Y<4TH7KNT z3vs({zmn{GneTf!-uwz*_6niaUcR|^wg3I$FJK1RtJ-(;`BV8?p)|VF77B(HxEe@R z)+H{-nE;cf)PW7w#8n?WQ!PHJYGvFFir$ zGQNGdB%?}eKRh0Hw8B-BqEf-`gexUQrQba~C8sPdJnXRIh7G+vbme5qQ1soF*$FBT zFg)o9`NXE-l!T2JNvodwMeT;~Kh%;AAw6PRKnDKXS$QE$Y5ZuG9cQtE_2&+Shv06eR0Cu-eM@2XZk#gJGho z>+T!7X{9b@DNf4~_`+J8Rw4lJl&vJKCZL8YDNSn$sH2LVX*~fAl(Bf_EN`etlP2K0 zlg_j}v9fP@`t8cE&4`lik6;hTpzr&#D!4Lqy*5o@y5YQ4`yVSZaQyU(Uz5yi9V}17 ze{g_x4l7I7?dua8`_`vFtxQw!c!Y^w%T*uwmNY2^|YMid||Mql>~x`7FVo}UOk!4;6lB;d1wg^Y5P7LbwXR24Zvmh z)Cp?5`v%Gs_b2cyD5gWciOkWs0lr;d%c|$t6{|1ks#8Z)fgb3IJO@Bt2$e;c*%J>;&`z=+^Q}v%y6`xb}zokYJ)W~nC zeV^lB&1aPM3$1I}k<_|)t!wpgLc8k?Ra&8@PtmLMX$n)&fOl)ig7P-0(;K)hO zRJO`gGZVWr)xdVPAk&!*RM>^g^lqHqu8DgmJ4yF+Z}}o;uX4*>!`60HZ~t2H=<2TY zpZ&gbAG#J7l9{dUnu9sd?|kRn^Z35+eCIp*$C45!1+KHBVch);Mg4C~D9$WL9^8S* zB`QwE4GAh~7&aI%ZcG@HreRakJZw%{hAm0!u$APS61Jpm*p{>p+aYa^TM~|>bJ&@5 z4ZD)`Fr6$JE=iUSmnPlA?xbhflPnu9OL~XBN#C$9=^yqd%ZJNJJ!_&O85jjtqXg_#{gQ8d>VifK17{_qq zNk(*_A7>I=?AM4tLW}mDvE-T9_^C;;REZtoV%$_(q?Pp1^Jf^*^>i{eo{)>&O3@MK zRFawG#1c|^lu4dRKvQCgQcki(Ckb{Y#z#(z^e%S%%n|05DQ0qn5k1<}5so<{R%mmc zo@7&r1Zlzb{A7CSWO{@hKgqzSo1afJEDphrRO|l zO|uH)kzqVc8DnR>qeg;N3tgsx_Q}v*#t9fTH2|wm1Lp@gKlHzxaRZzkN-H#_Ae5{lIG`+E1$e3fk2F>_N^H~_uhn3y zWXulALCN)SoL7H!ol-BZ9o5h%9S>`;RLOm+XF|}Y22G!;CH&CBdUYlnRh;_RYXEzb z279f7gYHY7&`oBhM^n;t-Wb^^`lUT7Z~h|;`|9`zGuoNtIiv?P$DHHTREbhG z${{I%tx2dxbUZH7>5(xeK9ykN(HJK>M_2~vM?8A+yl7<^Zi<~8acH(9ZcP{bfAAgf zE>WCJH(~o3Glq;YW6GE_mW(xH%h=>)yqrPjjg=-Fs6UlUO5@*X|v$k<&m>yVLAQH&9+&Ea`5tE}{z3sGQ$*=Dg^Z zCQ>b%Zqi$BT}4nn=vDe<@V-Sotfu_cdCKW`=E}-*{;FJMGya8IbAft{1nTdVL1ErY zxy$eQAU12yS5OYuThlVGV9xG*%W=VR(S;G`1?SwRWqWnb9hh}LfCeab4R|9m zkHriMr3PFj4}Juz>JpVPOki%2V^jKTiBn__N?B7AI_sqXHDND83`zYKPq$G8oH%Vo zPFbTf#*C2-b1L(ia2EkWGDBsI6P`!&6_X#o?;eH=CqN&7P_>v! z$igRMX(k>{O@?KHbTr~78&R}?c*34%aR^20=y-x-SkVa7$}&36ra876^8?Y?nKOy= zu<9jZ29gPYjmKb@$uVhXmmshcs6jfxZ~(yuFvBsPj>bl~@mCqx;)z7WB<*Nil|WM^ zlxbX!uu0i2%nhJ;qwclBM@@I?Zmq{auLV@;>)?Hh%F`6>7hJV>T(!B1;H9zov3K6c zR&)y$y?jOQil$`e=#bO?c+ zd|>C|j?Znv-sAk<bg_#!Yc^0!|P!Cc+(bTF;oC)N010 zCun92aU$TYTJbtNSy}Nj=8T!`gkAmQ@6y@x8MEF_*R~7aY9Zfxf-6_XLvQ~pec@0( zAr8L6f{B|}Wdx?-v^HabepbmKnt;RYM4SSx`!y=m1hTy%m$96FsX!;8=Qh&w=C2)@ za1kO%Lho(Z`=YV16O0p3N`zRp=^KAdV-)90WYb^NZX+dsKAj_C#wDQb@)f z$|x%#a*l~7^l>8cC>EvvKy97Vor6dS6(Sl|$X}RCg@Foz7|D%{p%^&@0z&$zXofQ# z!{YHB&vAz#A(ka#X)bzFI?6@kvGZvx!cepVp?r1x49J(Uv;vVHij=eK00E(;?0SrC z22XS)Q*gLVcaavhV>S_yHe#6#GeP8JH();UU(q_njZCJ_vM6GT=84q!B#WG=Acn>` zb_ZtIM6gU0=F= zxnRvz`U?Ht(f40^_odrMKRWi|F`(hu;I=t}x$asU?`mHOHC*k!(wz-;+#FvxzO?%U zzioKA>qS2F;_UvMzw*+)`F-!4&H7sfe;4oXx_MUU9^|_R7hmMNpUe6W%|1=I#N0XF zRfo_A<`2B*f4}P8s@wL3Am6ituiugN@06fIm*GpAL4>la?n zy6XgYBkyj^x|^5m&4ks!jLGL5SSzbo4z5_jat^~h{vlaTX0^bhJpe%$&?(yww{F_S zxujg!+y-S+{}~*I^cW0Rs=(?Af|^ntax!Xs)xZWdts&u11x^JoMSYL@_r^j$G{PfF zK1B{iQbqinF=<$c$~-g1D2h-Y6 zs5!&SPXT+fYI<5FJ0)ixb3kqUI?wlcQgt3Gp zw>pRRuf>CeyU1zkJ3z3qCPFh#2bFP7AW1E9N{?q;8P^07+9JmdDjAQNFGt1>UxReL z$lbIl<5?Ir%?u#zEpi!KVa0KKl){OkR!mJ!ibQ zMT(s@C|?~ZnXWhjDwc3;vPf? z-}i@EBlG|iNzXzc+EDsJjhlsNZA7aCB~n8-%%YJ?5v`PHMlGIbM(y3N@klcr+zkqJ zI3$BCM_MA4G4++PKP;B2yprTcOrjZ}I5;ztTB3zZac~3CB}qjtKqv`oL=G0i0iK)+Je1VU$fxrzjOyyywyuJYqQ>UOZ2+CF5g?*FKmBrKxpja8~c7# zD{MZ*Z$7ltaF}-;M!>qk#kSA7f84z^czCJiNY;CFi9Y(!N>#KA<%4|r;9|q#ShoD& ztaGKQ^}6%>2WRce_IfCk;;XtIIxG#|xl&NlfRt~Yx6Zx3>}kn)yeRSir4u?5-XPTN=IeIn{MCX#!uun4 z{N2|N=lsD37E{OfI zRqN+=<(*Vnx!`H#J*_w4i>fY7&rj#78;Sb0Ua0KiE3s!0??cK`=6hI5MK%a+oA|a( zbEV6k=9S3WTXi?;vXOpFLT^KtWApCsIP=F0FeG_U(bPq79IIABZ9-@bA6kQ>xKh=4 zb^Dd=*{V(~D=2Q^WKdq0t7up$4_*$v9ld8Wdz^X7>~Q|>;btn>be}R7cOLY_g1?>j zw_o4zKm9#<3jqB7VJ+paxldV28df|Nm*{zVsjhFqj7NgonZaMu-rKMIZ7;@qa-QmI2HsQuUHbQV7hwG= zjWWomB|Dn-bs7I*o#&YW<7dXfW=Q;GpnYG5<)>{%i2tj@tp7KTEi;{9` z(hxVqP4Ae9;sE{k($`)i z%G{__6tRkq(Wyj&Bnh(+E$K4}xOB0AS~r=-yBES5*jEs#h0Q=fW=o{y61=8Pi>A}e zdC~GJanfR8esj{rE$fd^#JPF2=iExl5DC%E={YYyU}E1}$$ z?Vk+%aNu(M>g1J4p>`c#yDnS1;r5|BwSAwTg}oAV&eA|Us#=9mCm-s}hPv)k21nn8 zeX~2~%5xPpms0boYa=(;U!VG*KU>jzVNc!!#jxi*f!R_?>543KL_XUDrxR%*g5R_^2$9j;F&~nd<0n zxQHZbIJ^~-Bs*L)qFVzMh1(4lMLi;h66Krd(U#+tuyj-MZviS|lOz-(uHjjTG$49U zB~mA239U#c`c>gmgAOD^z^8>vBZbn+BWqzXNFcC*W!IZBv{`BX5zWv#3&5%%Yl&?9Ot$AU| z9{SQ%lk)}O_JsE58r$CA`tH^hPnE=UuW!1w<;E7?(+ig^P1?(rdc16D0*(w@G~9eO zb0tISU@valg`VAf&u*dT01tnj15h`tty_b2!%!FWUXqflOdUHy4~tb@X82;lban#? z{o`$PJqDp}QoP?>fvtvWxFV;y^?b}mWdBYBqq~u!DuLmA3VOYbr1S@l>MBXe7|$6d z)H_V2Hmc+Bct2u{szl=(<3@3sIQU{6RTq*(XjA7VL~T>OH3GsCmXT?KxbgHZ*j@Ed zs)I$p&NO^++%yd%l`%!}EJYkJ_GTqs2$gX%Q0L9~FJhVY9A&|ZK&B-rIDoOo0G@E^ zU&kRf6!A&qgdEe>W0`tTln#eDyDT1&L@U;3;+mt9L&_m$Bp8*CE~VsZK`oO`J+yQr zl5$12l!BWBxQ=C+I4JqCJ_{;)a+-uAG%>fhh^>=D5E7;|ZW5#`()Fx|!;g~;&Z1cV zF%+g4>@K4E6?lZggsQIwuLR#)ldbF#D*O1#z7@JspsRVhdMUh?r`N*H4mp42bYE!x zb}L-LIs*6X6dk$@0>FB{cKr`t6Z-b>eS7ZI?pbN-7Mgnbrrz11Wx9T)wfp@y-hCt6 zx(Sm_IXWcJ&3EYL>t#IM{srB<(z)T*!5as&o!hWH0wgBbA_ThlK-cWv99$C9^>^s{ zYlnEc#>o>+-phFftIs~d~8?@OVjf>V- z+bmET+eDKVLW;*tG9|O=Vae#$A(YHaD48v)(tDUSR0kC~Wt@PLS}B+>pr?{I_Ael*URvsrH@t3Pv2hKddkd0qGQwfn;Exs4Z}#dA%q;BMmG zO*dUaTR-2{f5+Xw(zae`+rqbPk+c^o5yWKVE)vO>J9NwSdY)WX>B0E0@4JONK1z-u%fjGIH-XxW6{u|f0^Z@ z1_P%azVwbcaBXH$j`K+Mh<#SgDl17v7F8?=U^kI#`|m?niRBNmiG*8rBqh=&K_xHS zfgTboF$fyQ#K;udo)_$~P>-ox%@cfm2v3lryu#v*Me&t9Xtas_rh;X9H@xMR>76JH@)17WK>%$sFPMMY&Bd4+qxXqMVRK^fh z>lB!s-hHn^AfZM94>h5Z?LxB(B#=@AnCW%yRAvn>J>uc1GiW8Dtb#CTaK^!Q1TG(l zN_r^bkZu9cYC>8T{{~{BTZNOTphhJ0O7u`^1Wk-3fljuz!(kOID0!q;+1jyx4F%I{ zbf^AtE3FF`!BbEV76DjhzyWb9@7cO=crhRh9^wZNWj%*yEh`Pp*O>3O&)Sylbt}zl z-jBW;%{KR9vO(4*|G7)r{v{kcjxQOcn*@CVFbl2yYajsfrVg#z)N!C)FqP6fk5xMw z7lE{+%`CFsq8+_z%Mw_1%Zhdd;UoLjFK~utT3iUvHar$;h)90o9VR;V| zdt=zc#J*VF!%wy^mg%?B3A}T2!r3Bro)|WXE-5~inqoohO~ad)Xo`)4fvHQ;YvO5K zt149uPreJCgwwe7TYn04UEZe9Wz`iFV#+4|8nqcp*G2B@jZX@#=$*y&LIqSmgRusZKjMJzS&0M=R{wZ$ebe}X%#n!YzYZB%Ww%dF zsPdnJv0z|B%qGeU`ATdR2FdcjW-tzSw{ZE&oWc9iSa{VAAhIAl0JsGh*uO!K+@Xar zR*&A_qIVm;55ZFuYbfALiZ!e#+Q~|R*AWB-SjeG_K$L4lbX2*b@<4#JR%l2?FZDoe zS9B0UiCu>aT=0wE2S`$02^`j<(gp*V$K2(ug4qX)H2t5U=OEpMD7s?FRdY8Ox@x~- zU#?%94XzV{oB8186;D`Fr`>k(o-J_1tpw#&#f7ouK&uc~#|PF4fj&Oamkn&rmiB+L z@#BsEwGYjYD?qEYY0z*PF66rSP}l9UJE3)pvDtmUbk`yKg?9q#vjHg?Y*xCh%KJRrU_NuMGg+2FzSjl^~4bc z?fR;3P;b2c%uukm(R&1;i~^@Ve*l{oJ;j}MB3#rxNZwRMa3r8JwpGN_7{>=wCa`{z zXK%(1RdnmQd?T?%Hx071xb`i7AmgjPihVYbqBphY6V(yktvVp+6#Ae4`u#5Zfi z76IhMA40{5U(!b+11L~oi6S@@JG8ckuK!l4=Kq;`LKJ zy^*9kdAjqq1yT=eX20{dcFN@;noD>)S#}SfpX7X1vj^|WPeKl4eG$Re!~1%EQ2kNe zhjokl`LzeZwnOl}!24dv`i>WjJbJU}a4g9mOD=az@`1@&`j_P$$Yzkogot@W^Fx=l!a3{Amnz(4 zdf6U&guV1H+iP>~$|ZXx5fn#YcvYuLdMvP&KO{4en1F^+=rEIScIZ1D(vUv4`Xy>a zAqg{JoB*b_#+N?@0%Qc^PTiMijMB$I8>sashZn4Zok zrj1}g+raqR0k%AUaXk9Rhn5Mp+{7OY=#sRy@@kCQ;Cm7&vS1=SpZcvHqj}4rsZ15F$EG}~Jvdb)GDeD1P zKAJD90_S7ak-B4)dVkFQDA5YC;@~z3-4j{kq%p@n_Qw}UzNGhohrKuJ%aq*LVLXr8Y_alxG1!5vb7A7{=D>+XmGQwR$b z4ot8u#01-PagP+!m|#1nyPjiH@U%U>S_VxM{UnNpVYt~vGh^u%JH4C8pht;lImBYa z?2o{kt|zv@;WH94geNj>L0Aj3#{n+VUMyfPA_|o33TjO9#*B!65()t;(f}wo`f}hXXC?Pe(BJ9D1YZh0#OCLkxP^F^r9)H-X-1^b+Xdj7X{q zlruGQ>?9^r=$%3D74Sqm3Z*CELR_{u7F~r;PqTlIa73PUOENczvyu{wUBIGq=!MZM z^y@q%MTZK`;*NQo(Q$=5?y|B%)&{1hJ9GtKec)q?D6rvjDuK6<%sBbdqw z72EgE`|2x3}~D9VCVC?rwwiZ!ZYpK`#j5 zzTkZG+pT#ASo(qx9<09G(LJ|s+26L()q5*-BbDvij>(Q32>j2?Kl7LHj1PMg3}Nf< zncse02z2s+&YMSX9lLRC!6|Io$HU*6ec8Z%A#jKf9Lffc%$dIoR9)Nf{=i=ieCe+S ziQ3;L_%`vrO$*+Iqn`|aJiP4No0FSW+mRp@yXHks*mIoUb9`y|PqKBfS4Rmzy_c zt9ym&ZG82%Cl2%CA%6XC-oG1WyT%Jg3wd;HR9Og)7XF&TsD2vB)wBpTYx$bBx!P9A zWaFX5w9`;=k1};v%?tKjh`1sPH6H7J2%bpj}(A+iex>WL=lHUV{ zlG?A*NUwg}9elRQ_}}b1)xgfi2ponz<520H#vrSEk9o~Ftpb4Kh|0xE;^OBzeR5@RqfgWY{x(3fv89ig(R-X zDJ+D%xkP(SeVvmk06kH_QK9q%cQDNYQI;#fsCu?Smx^>wPwFq2im}`Dk6UaHr;5`P zsO2bfaCvLe)r#pJ-s0wrVcY<^CRvQZTR!{;CKIiD;es#0gkvWmOo8E6Se|hFb&eER zU7eR0>X7J#-vb&s4X>=DD(q4AXV5RWLn2mKgy|7(_Cw5-El`6HmsB)_A0jK1E|+lH zB)(bTfS+PT86Bb)ZE|l2Qz|m-3XYMk7rLXoLVwH!;p4#nFEW&mpfzIY04k5R`L=iZ zvff6)+s=F2Z}#2lztO)C;yZR`y}JbOKHj@8>pd`Q`_f&JqrE_v~O%eJfTf9GDI!vej5r#HZ>@LYA%)%{oY z=Yn92 zj}r6wXjF7Yqwo|Te%1-%bTsngqw&-TX5$02@sT9Mjiur&Dt_5b=;7&> z2ob0Qf#+W1lbj?Axv-!cJzQUsRI?RhsJ3LS==Gxa8hR+&h-R491Tptzzl)iFg`OL| z|A5|Kqvu2KKcPoNs*f>7^nJKU*ndQC2|YYqk|B_6`SH9@#3>ZG(>2heH>oepU3V>I zaMj-mR&ncumTi2?wp>#i@Hz1B`Km0z9PP`SAiP^zmbX9%v+6C++b{+)Mkw#Vn3D?B zT~5y@^DayQgKo-~V62oXuep3={@J`6Qyzpm$5*uF%P{Fx3b7gZB_yZ!egI+*OqA7| zH)38tiFqtu(g_IfdP8{&gwP3R>1_N$=)Mh7at+LHlIyly+molNEG@UWg`JDOg}ooX zPLiKDea`)){XUkc&9unP95z^-ms|4`hPO>pxL{s5ve>wA{NvL5BwwzCAo(#PY0G5^ zT-%waAh^zvV8L`BQ!*&T;2O#5y=M$s$}b;ZJ1>b!PWC)1m+X&uTfH;+>AXsQ5!6I}AQcKUjy!0nA{`r$q>g*eNiW)TT zGTx)8bB3pl_Yp=$2C@8825iqswY1~RyUHyAGUyPJnTHSt-4~ckU*0))ZoVsT!yG$R zQG0o0{^h&_Q%+?<-i0xms;JDDV63ptF-6un?uIfFqUS6=5Uy7E{7 zy5tPscR@@>ih08Xsqv7(QhTlMK82xNiqZV2w0gakQk)7178-GRl@geHMulrbiGzAMEh3CR9Yc3f>V&KTcIDH==MwWUgX4JW(4RKh=e= zPobY+Y=cR*63gf*c+Q&c!ae~Ga#G~6xfFW%)ndi!4#PDI`3<=uvig;MtHd@2*t<{* zI3|^T9z1xPWH9`KYWOFrGfQ>;6V>)xquF5mEv0x5F!mcKWvKfF)$|Lh>lak>|8n@| g_GTS5-?IMJ%-Rg4_bB3zxv5ZBwz8Y2D#)DtKc=l{5C8xG literal 0 HcmV?d00001 diff --git a/Backend/src/services/__pycache__/encryption_service.cpython-312.pyc b/Backend/src/services/__pycache__/encryption_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d80143f4a0bb2ce9981c6d2cb33631dc75c2de6b GIT binary patch literal 4880 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 literal 0 HcmV?d00001 diff --git a/Backend/src/services/__pycache__/gdpr_service.cpython-312.pyc b/Backend/src/services/__pycache__/gdpr_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af2e463499f0bf8c265e657c9c4ff9c77bf88549 GIT binary patch literal 10300 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 literal 0 HcmV?d00001 diff --git a/Backend/src/services/__pycache__/guest_profile_service.cpython-312.pyc b/Backend/src/services/__pycache__/guest_profile_service.cpython-312.pyc index 1e7a985afff46320062bfdf4394f60c4b43428f7..024f6f939e6bacd952a01be5eb6301a71e27604e 100644 GIT binary patch delta 5104 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 fg>PaDPFgBS=n7CmcPOPq#yLZ^nF6z delta 3710 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 D63>fJ diff --git a/Backend/src/services/__pycache__/notification_service.cpython-312.pyc b/Backend/src/services/__pycache__/notification_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3135b6b78718b096224d0009dd1f9f6c930bc022 GIT binary patch literal 17092 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 WpBq^JJ%*;c0Kfw8Ise*0`}uDdwhmnY literal 0 HcmV?d00001 diff --git a/Backend/src/services/__pycache__/room_assignment_service.cpython-312.pyc b/Backend/src/services/__pycache__/room_assignment_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a4873c1b8bb7e6eb7dfbdc6ed26aafffd62a0a33 GIT binary patch literal 11015 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 literal 0 HcmV?d00001 diff --git a/Backend/src/services/__pycache__/security_monitoring_service.cpython-312.pyc b/Backend/src/services/__pycache__/security_monitoring_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..efc636303b5b2dc0bda5ddfff83a999519c19157 GIT binary patch literal 8977 zcmcgSTX0iHmaWItTh`mQVq2DsF(@`R$%NPN2pCKV20IhOF41gIgzhz#(8Jt&1=uBL zGO2vn6{^5L1Jp1TQkANZf~w@xRl6U|)KtyK{^&;Dm2!(zJhfZ3#h-xe&TjT+&uQsi z$+Ae;Y<3!}@9FN-r_bp=ea`7_{?Tr?5J(^Q_a*jq6Y?)uP?Ej|JpBy-Hwa5uO^T#7 z5se06ZAzQgMRaL>M4x6NOxh4J(0X0Um^Ma?X;Z`mWj)KJ%xO!+lD0;yXkKJ- zx-HU1VPndXc1D~Uq9vS@bKTQaM~%3(pwwX%tgiJX{7bFvw8mP?7TM@T=c zlkHPnJjW-*xv@)JMwDIail^pgIoY?aYKptWAvz4NKP_-Pd@biWF`CMrKhN>;8lUvj zRVzIG4aj$ca3rE(Nkq$PB05fQChPyP0anK`te!Kl3}<8woQX60fdFd+$U-5L3Yk^N zqC!>`vZ;_AAS-9#Y|~n#K&iBG_UTU6@d2aGX&a}fV>_Wm&$>R)t1XU=e3}W{OR?)5 z>R}WErOg0jq)?q}H`F<4od+Nng}f@{O4_AASKGMh<=Sw4|DKgIEv5^-++ zl^PJ7&ho*u+8m5!SopEB{CZVP?vM~o56HTzP zB{3U~u`CbzD_dw~>^!QUjpr`rI6;ig#>5%9O(~^0aV87Blrm7TvYZg-6IAPEBg=`g zL`skiJSXP(Ox#*iJ^a|b z>L>iJ1)JEWl#?z6s()Fn=ut{}o5J~T$?voZOu6(aIQ4H`iuVbtTd=)ryR5lPuISz( zmo;Jiy!S-*d@$Cu{9y}^lz1GH_hQzLSqEmFn4ymIFJOk|fd~6b<^y9q4;qFhWTQg; zJU9`y@;EiJ4pc@q#IxyiLX?d>Hx2qT!=udd3(@!t7rzh{a>8sPp2+3|R3VY$GlFa$ zyAtQBiejjmDVgDLL|G?@ylmpLsnoex{6g5yyOF0l?^Ki@768amu4c7q&HuHn$p=ZBuTk%u>8BcH-;$lfEBK&RRL&>Jdu_LjRgm;E8k;jbrL?%jq! z_ZDdS)@Sv#Esj+>iQRGS=+e>aJFcEA+uE+ZzV!OlaRiPp9lxGiwe@^!AohWh?Vw~k z_`v<(^r~%q&C|Jf;y)icI-d|7jAV0PJF#@4(6#3uHm$_|eqg1u;2&MJ9Yz}COXEn@ z_1_gEV16f{#=L(ha41B6A2N@IbiaS`<8L{lZKT*E6|k0fKrhRDp38~dLCGPUi(|ZGOb&|sb*zQz&*`5` ze+KGnqd=|afwi`-2c5Fzb&%VC$FOaA9cYlFu0eCo>c6mSe&8@VAsQ~#9Z@hA7ZaBt zWDY7G>5&8Y!1z6#$mB#$c!VLueDGv0B_?K5TyPp<8IDDNn8*ati=}5pA-Hwk5)MxD z*>v#rH-fSqk#am2jc0QiF|4clS=FaL!WajJU}my8UU-BN%{+tgdT^cz&cuY^e1CA3 zsK^RY*;ex4{AO^9gY^%1Yqfn2ijxrJUR=p(}CD~3Hj@B5H8HnWg zxvJ+jOea#{j46|{CC+njAYr31kvCySLr#omvX^-?78oiJ_3Hc}R(R@D8C9l|NB<@3 zXAmbvlb=l%hxY%` zTzK=;>cP{}&}qqe`l(Uh+x8a|ad?-w56@T3#NJ)85ohn6(3g%)|LP2voJghYZGY(9 zT=MRbyn7yc!zJ&qe_Lq7`q~4Ko-{^x^3!_toskaMf1?L(cD4|lE%yxF z%@(>3RP4mH|65{oxhvlF5{^o}qebiJgWR7M{;*K)>HX-;tuvpbi#@NDdiF{^dsUIB zoRiYfq~x5Wm768!=DWVndp_%toI6#tMk(L;#p=N~rJ*+^=bIp9V7-)GC}rS|8>D>r zOewcZ-tFb?&_^e4ohB7NJjG-$aM`^$*WaivTIlG|@3U2S3iH$$?_tKtrY4 zYQlOMeR;WX(UrW^Z%O1SeT7Sk8=C?iD z8MHt@3v`8!su$=cF3%0qvgvkD(Ya-j`Loqs_WEy}SU&MdPbsuh3hi9kB83hWy`zg` zUs-((ZF{89o|T9cdadX^hHajPwp~(a*UD}wbhzj}^8NkFR!_m&`=h$9ACj_jMhcA; zy+;?v)*Rkz?=HQ2J6#Iwlma_TfxS{-Z!xgH=r~Yvj7g5M2h&Bz$&zDAa!eIYy;XEX zN{+LV<80CKPQmmJe+VWB)-pCHtUJk}0i))z6Y?7k7dA=NBmzb)DJmzb7cqGaT;kNW zAV!~XD*0(nbgc}KwsFHJF|un)3V#c_1X<9-kNK-{qn`TeEUDZW@`fYiZP3sKW8T>6 zRG7r;o2Im20$kjbO{pcWIj?VRKv=DErN!#zff8K6nfBqAX^C&%fY0R3h=27RJ-W`H z;;i06CJ}F`XiBy?pQ5{HIBTh=RizKyTvON7fb(qaL0yORz1m}e%Sybcgb;vbZZlwD z4fBKR3@ij8BHaBa;sQN^sDb=M5I0y@!=HdLc-%q!OOPE6GqO$0im_CbULXmw@!VYX z#?}gedTHy)Wt2cC3OBAmA6^-(5OgQ980Y|HCk^BIJxCd2_-3PywwwPsX2&r@^DZ-& zVktNuppMBpFrC$i-lPn`qtRCkI^~-h?+NTbiP;<2(NJZaUIjH=+x-ICgqAklz0OGE zgbCkwZ?+NpS4jFCwdmj4NPFN$bU9k|4czyy=vTx0CExx<%UXy3#+BtOpV*5X!=;WL zQpb*mHh;<1FWLI-`vWC^Sn`KU{t?MPvUt4Q(S76H z<#$URgHp%f;<2^9@TY?x58h3#go=IpRxV0?2Nz$veq6HkmitFOo&0$6i(@OXV*kMh zFG>AJ7RPUzzO;oZR-|2NBfX*K^9qX9C)xT|2lkc*j!5ulJ3`sT$~&B78*E}8&LtK& zmsp)tc6b2l2D)(1*byjq4U{+SEO+%jwi=w43Ne^1Rp(%$ON?&@R#gkZ|2t_Rhi_id z3|yxQaSW-Iq=wf=V5Ocvy#Bw2)k#$Trli_ieO@nEMbt`~Sk*(luVLW{BB}=MqUuAE zErL3Q9`sO0!zYZW8f2rLh^pg&eQXiLDOBjye~U0KuUGshVCcct=;lKwWBi3+tSO!g z!c~H@F7usAa8!K?3c|vGCCX*kQyT(3Sqm%5Oec7FtF-(LngaR;K(~F}nl!9A+!ee9 z6URp|3t|?6EX-6xzB5=+Z^yH~fZ+fQ=&IqIy~bwM*v)TIc-?raUx)Z9l6ns^ILJF2 zL%5Q2SaJ^EUnq^dDviAQV7D|fUUZ%)Iq_&(be>vdzH)dUy1NTKyNd4J1>5d5f6pRw zwe4Yd|3{OzCX3zM7MWF(A1>Fc-_T3=4Xl|g*UU@itJX3AmL<#e9f;)cF52jJ2%9Eq zDQvC*k62)iTJ^Bz5xT;#W+L1!o1)Q7EX_ruvLySU>ny48*gwVnoP9+%MeRwA@oMW645%JvXP8Pgbm0vMD7ymfA>1?@eJj zU2$N^N&KNZo?G^c3rlVV(}N{1SooXq<+m$7EVbhZ{Wpa>hi_iFH7vQp6=VrsL@XUo z`~W@G5rey;#kw~%6x__X?_91B_F%1<1o!n6df>`tq++BM;6%C#UE3x1OBFM%s80bc+MxaB zE6c+bJ1w@Mpl2lSK*d4JPHf&Sx%(?FT6Pnsr{bZYm$2I#>pxqZSqeNcLXA2XmfBoc$l%ChvzzhJzVKah z`zj24p>MmtVnoP9x;EYU*{y9A#Di6askZ_|X+72gRMj$6u!*owZWAu1VkvkUOwSEx z`7~8+*#NK7@bE?zo2RN;*J-@+sNO^RVAF6ZVR-!su&VUx=8&y5EPTV}@m@jp#j}~| z#Cd#k!`Do>ImIH6MxFlm2)-X7WKix>X>WYxzo zR?%rRny*RUH)N$ zIp_9cMxs$U&X1%ub^G?c=bpaZeZTX%M}O~hmQry1X-gn_u#=+x4KLKgtW+NVB~)%w zEX5jPRNOFVFksynGsaDWrnq^~9JdTw;?_YcX*b16;w6J6aoeDcT$^L|K|5<HR}NMhC?myHavMH2 z$g>)(GE&b|tm7?;r9U<*ZPh?i{joCb!J2V%(D@Lrf(EH%fD?phG7&UO=HpQzC7Ju8 zk(6ZWO^i#nlVd693de#*$;O6LTq+vpBs-QY7fXdtJj5H^F-gt=E|TV>sqsUXxI{`S zTXlJ6e2kMSS2YcAmpCMc`7h7E>vd zmLf{Wx|G%;N_t(&k|Ijyx|FseO4quS_99C6x|F3wl%91d9YvI7>r&E1l;sMgv-oYo zJ9ib)Q~*sm`7&x~f9IS}V{E+a`?@#y;x&KTy=9X~HBJu3IMztQk>0 zip82^!lb@Y0|j)JE5<8Cc1)N?YuAv%C2M7ks(8)7`;3?(#vW*Y3UvuiD-6K2+a zv4x^|KisGE+ui4S47fNnF7&tLas#+HH7>4i$>k^T9PV#9HfyH78G0ry6PERG2y^g| zFJNHH@bgYu?G#%+VZB80-U;jIMxAe1u6}WreCbImTS4ey)M&j9eFf{)zM_?_T$gv{ zTHZxZ$vnw>!m83wmP}YCN;06~MmIf4r%D}o+6~Ink`)c>{D8d9bysVN%AKvcL;(`2 zp0ZAhRyAw9*he94YmtQ6{N#)#V9l#<#Xw!}GXr`Cs75LkP-&Ye-U67iOqQIZt|u>3 zS4`)q%Z6ZeruuX`!GxKibS$=7hD#Qdfta4~3&Kcz0kdLfFFcWj<5UMN0M=_eK>KYy^j+vrjldrN0KQn)~Z;s?FYgUcn@s5z(?BQ zBcSynKt;Ba<>@HRZDZq-lTRl?2m{iSX&6ul2MV@N+bR+dOh))KfX7v8#YXw+-@9`A z&0BBg>bEV`?-A?wZ!S>DwdHd**lK=zZj> zSEPFv0y+AS&ba49`nd&Tjy_2G_KWoXh4LKTw^C*=FP*ZkY@jyOJ)&s)QNz?B$?eU1 zs-|tCr(wy{B6?b8EAM;SvB{P5Y@Y4>r29_y-1&vx+%tzo&tYiF*D|-eZ*|WM&Q<1W zcfW1RSJX~t?o|Zy-r8(kYtGv?x9O+B9|!0A7q{nj^o!p9``)&E<;Ix}_bN97&2)dx zyJeR9Bzh-0?_792*LGC&9=-3~f+W@VDv{0f$((oVoaLwXAKT|!7t3o(m!dF$lt!Cc+8xiYb?<86Ds(6v<8 zD%Q2mM&@?p>UOQ_Sv%-KO~X=6yI9jcXV2B_nR)@1O3Tii2TmCcVRqrDeCMBeSaF@9})+Lv=TWm|ggxqB8We^K*k&Em7!!B=y= zuVq8u$@PBcfv0iF(nnVa;Vg!I!1JXYLr+D;LMVM0&jjPb7)>so7S*n^G)iIdWXP7jm zG&6ZZdj{5w%9uZb@d6f%Nj)09ws$Gk{=ShdoiMTv*6gGDs2gCY4pR~1c~E$07LTGr zS#v<^hBZxa$2PHBp)Or_S;4c`oOl2uk=nQe^Bek6Q}V-m>} z6B!97hQSk5#|ca{!6YHb0OrLw7v?j}p#&(<2&YjnJeUora+tAXEE*X<0Wppeg&afo4nc7t0; zuJF(h7ePxP3R+hP(nP)UtuT;B=T5S`p62**9vv=t$Y>07B;1eZ(7i=t2MhpND|3=5 z9E*W5!X+eQl;w}Xh@e?kXpRVY#6v?NjMPXLS@#>#f*D9qZ}A79!nZ?_3F;{rMIo=Y zDP9C};ZrECQeT!*RrNQM?<8{@Hs1}-SKQz7thnLXDM#K@e$)1j?LF@gTuYu!qG!_s z+H>RNwUg5qMLLM}6W2~mvmzaMY%!IUer2T`u4|3o+q6R+JJbW+kD(Ew>YlX5qT`OM7nJDWb`kC!|P&CPnmU>?xgAkRzqjWufdz*?izwKG3uVLyuuv zls;_y1U9Sz0^bk>pESgclb{mx)$N2qQ?q(8EyJi9*Tvf@$AnQ&>7qKFFpTP`+QRZ3 zEmwII&MH7sT8xP`-8HMTLQ~=5YekA$C&Hw1K;_9=h+1~ZN}07!KpTMysM}Rg2^OHj zfu{lycTiliGGmB8#;E=nkFNlHK{PaYw1OU1Ljn5I0&wC5J_J{pDs*>bCWnbr#~eqe zP5?!dPK}@|6(Nd;IFtNQ9P*6*?NCp`2qqR5Qp}E>47o|BQvs0f3<<2jWfh_<=+~pC zmso`n4Yp-uC5h_6_9BpG0cEvb}rg;1$VPT4cXo2Ge=e+l~6AQQYa z^vV^5S70c)LE*w&nwLOLUV+gN59^)bhLtKl*vC@viL?_Z@exrTgbc7Z2R;IVrZB6zP+o z25X5L^jzEYz0Ifwt5-k`)=rXMNK+gsSu<6;pMm-2B;6x{N%^_xV@(RiL< zS*E!F<3hNEQNtx*Tgt%=f??hP^EmtvoerY!NH8noe@P3aFauh|h3Un34Mto%sS&jL zLM%i?pe>nrh(1jrG9-h!n<+$O7`>Uu!I@Ry-p!!Fo#`&pjY?2wT@GNyvQg07v2g|7 zE`Aheq{$~9WpDw}{6)BwDhiV-M3Zc45J?Z(d>qCFEwXN1&0H!g;wC?tfNup9t!mX0 zQ=)AhfERI4Ki5rQA^ZgtGQ3%XXp1b@`EMV&b!6szuC8OLZjV^EM*}lKc)!@vFVg*_ zF#sm*3@6ghXe7mW-+sSGA%0w9-dOsTjdI9%w^MfC3Qf@)r(eJ4V3z&+^8Ri4Eqmv~ z_qIHD>xG4v3Glb&10C6ho!Oo7Y<(ghIR8Ha{vI9R|6gik4fHw!q2RRYIif0LQ<@%L z7xbEFfRCyiDhHyiWzf42^sF76Vp&@|0OmBnTy|((L3gIL0P`Z8siEIOs9Ii&t28!( z;V?f2WfACoMW8>Z#HX1IF-^ETmlqPClgk^Zz9WVW7`!?VoOD46II zICd7{cnyRDczZI36xMVUv5Z#P@03LWvXCkYuPdZZWsNHC0J==)I!G;IvD$tRK7paa z9VleLzGztXGq*=>jm%8u{GChw9?{>U0W;gYcd5Bggdg3fHGV_1)~`lu9ip{GTltDqrm03AC(r&Nvw1?kL= z0(6ue0Y=^t04sBqPZ)`D;RHl%(ql4y3=(~s8v+e6at3!0qW=k+JmWef?=oBUZNXCy79x2kCGoGbIrR&diP`5V{{Wc z6g@_W+tR-2KBAW(7PC!u7W4J3`M|bp!}jd<)7kogd;k{vS_g3*JM{V*b@a5AUf5>T zR;7-WTAaR>?tF{4f*`y-;MTUs4z=R1n=&cK+OiOToe= zpsa-P6VBjOM$R5h`$Q$GI%lUMFKt& zt#pz|Y7vHUIg&t-1_}#M$TFYjRKT{27J5~AL6pm^=PW;km*qWe%LsBYdLS3W#5ZWdqf+Hs$aP{% z3vPFz6~u=F!o4&^xC&OW;{2jEWwZ@@W%qL zdDBILjS_X|pLnM=IEc|kF$Xb)LQV&853^%@QGS3N+d|dPUw|UhN>umSnMFB4k!0a< zCc%Nh!IRiU8mw$uh(tZBS&$+8KQpC!-kxByKD@FijDJ9VUXv zLFDfv)ov&xJKmhczE;T&WFy(g8-kfgRynnWzYfi7rNu62%j{f7T97*wUUK0xeTCfq z5eivc^ct2cDsOhZ)0M4j&sA(&s@N@7?9O8bKoE9uX3vQ9c5?L$_`9#2$7aG zk!oUy@cZGxA^ zlo_aTC!}Dmhxi{{Yl*-9*}FIwJTeb*|LXW^_0Py=-i-114&8>Vrme7=Br-@Pv0B0KOTw3K0fVYW6UEKen2fR)#+XHU0z^4OqO0#c3!Y z$gJOb5h;)>87h@Ps1735LZ-7nVee^Aplo1PmK+xO>cdh`C)S#IvQ z*w&kEJ+RQYz%BNRhX=EVUd`6Mwj9{}QSgJ{?D1TnJKxyyQTGSkv*&Y-d-KiDd=&j4 zn%%ZP*W6pUfJ~w0Ltoe}HRWI0ODkPdhad^a>ArFJ+Tpi5uS0yzb>rx@qu)RF+XwDy z%pdVRpv!L@zji!ZyZh&+`QE>>&RxmY?7vU;ErIFY$dX;C{&;9H>4} zOMO;rKUi=2tZDl}pXqOW7O3Nf9mwHJkda1qI#$UD+#n`00t1}rn@@bh^zcI7$wYD9 z32@QNIZ;pSa)FFZ%kIYR-x^XLB`!`Kl3EI-b%M%3?1i0{=wNz@q~a+#uOPAxzf@ zg{0C@2oyEERjAXU(Cd(gsPvSCLToaE&1k_X3o6ewy1BfPY?sDCzZ zU$&I3824GKrZ=xpaGGIfJLXKYT^}SK;pJl!Wi2QDEBlS4T50jl04bbiN9I!V2Nx>l zkNo%zvF-39Y*Xl=w$et(y)rqRo*;)dg&b-tTUGMesuc*rp19p+o$`)J*V}_E9AMu z>4~{Rn=%=wt!yE4uL8Q~S135m+ZMbFk;RUMkx%_%*QrO?rjSEzWd|Ym1Nkc}6rAR( z7n&A?#e)l1K5Y}bUwnjZ3OUqP_8SO&xy3gPlZVqRJJ&I9n(Mj)-v}-hQmC!80Oc1( zyQLJrCYK#08a@= zpja^(42I9Cz^|#TIcn>#sg{4CT)(DX6{%N0r~IE&P2|`1ul9F4zX2GWQJ4S# literal 0 HcmV?d00001 diff --git a/Backend/src/services/__pycache__/task_service.cpython-312.pyc b/Backend/src/services/__pycache__/task_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0cb4f518a6b42917d8b69e0c7bf8afa084f85260 GIT binary patch literal 17877 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 Y**-s9aQ1y{e_~BpO@T)Y#$?R@2I(dU1^@s6 literal 0 HcmV?d00001 diff --git a/Backend/src/services/analytics_service.py b/Backend/src/services/analytics_service.py new file mode 100644 index 00000000..3bad04cb --- /dev/null +++ b/Backend/src/services/analytics_service.py @@ -0,0 +1,739 @@ +from sqlalchemy.orm import Session, load_only +from sqlalchemy import func, and_, or_, case, extract, distinct +from typing import Optional, Dict, List, Any +from datetime import datetime, timedelta, date +from ..models.booking import Booking, BookingStatus +from ..models.payment import Payment, PaymentStatus, PaymentMethod +from ..models.room import Room, RoomStatus +from ..models.room_type import RoomType +from ..models.user import User +from ..models.service_usage import ServiceUsage +from ..models.service import Service +from ..models.review import Review, ReviewStatus +from ..models.invoice import Invoice +import logging + +logger = logging.getLogger(__name__) + +class AnalyticsService: + """Advanced Analytics & Business Intelligence Service""" + + @staticmethod + def parse_date_range(start_date: Optional[str], end_date: Optional[str]) -> tuple[Optional[datetime], Optional[datetime]]: + """Parse date range strings to datetime objects""" + start = None + end = None + + if start_date: + try: + start = datetime.strptime(start_date, '%Y-%m-%d') + except ValueError: + start = datetime.fromisoformat(start_date.replace('Z', '+00:00')) + + if end_date: + try: + end = datetime.strptime(end_date, '%Y-%m-%d') + end = end.replace(hour=23, minute=59, second=59) + except ValueError: + end = datetime.fromisoformat(end_date.replace('Z', '+00:00')) + + return start, end + + # ==================== REVENUE ANALYTICS ==================== + + @staticmethod + def get_revpar(db: Session, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None) -> Dict[str, Any]: + """Calculate RevPAR (Revenue Per Available Room)""" + total_rooms = db.query(Room).count() + + if not start_date or not end_date: + # Default to last 30 days + end_date = datetime.utcnow() + start_date = end_date - timedelta(days=30) + + days = (end_date - start_date).days + 1 + available_room_nights = total_rooms * days + + # Calculate total revenue from completed payments + revenue_query = db.query(func.sum(Payment.amount)).filter( + Payment.payment_status == PaymentStatus.completed + ) + if start_date: + revenue_query = revenue_query.filter(Payment.payment_date >= start_date) + if end_date: + revenue_query = revenue_query.filter(Payment.payment_date <= end_date) + + total_revenue = revenue_query.scalar() or 0.0 + + revpar = float(total_revenue) / available_room_nights if available_room_nights > 0 else 0.0 + + return { + 'revpar': round(revpar, 2), + 'total_revenue': float(total_revenue), + 'available_room_nights': available_room_nights, + 'period_days': days, + 'total_rooms': total_rooms + } + + @staticmethod + def get_adr(db: Session, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None) -> Dict[str, Any]: + """Calculate ADR (Average Daily Rate)""" + try: + # Use load_only to exclude non-existent columns (rate_plan_id, group_booking_id) + query = db.query(Booking).options( + load_only(Booking.id, Booking.check_in_date, Booking.check_out_date, Booking.total_price, Booking.status) + ).filter( + Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in, BookingStatus.checked_out]) + ) + + if start_date: + query = query.filter(Booking.check_in_date >= start_date) + if end_date: + query = query.filter(Booking.check_in_date <= end_date) + + bookings = query.all() + + if not bookings: + return { + 'adr': 0.0, + 'period': { + 'start': start_date.isoformat() if start_date else None, + 'end': end_date.isoformat() if end_date else None + } + } + + total_adr = 0.0 + count = 0 + + for booking in bookings: + try: + if booking.check_in_date and booking.check_out_date and booking.total_price: + nights = (booking.check_out_date - booking.check_in_date).days + 1 + if nights > 0: + daily_rate = float(booking.total_price) / nights + total_adr += daily_rate + count += 1 + except Exception as e: + logger.warning(f"Error processing booking {booking.id} for ADR: {str(e)}") + continue + + adr = total_adr / count if count > 0 else 0.0 + + return { + 'adr': round(adr, 2), + 'period': { + 'start': start_date.isoformat() if start_date else None, + 'end': end_date.isoformat() if end_date else None + } + } + except Exception as e: + logger.error(f"Error calculating ADR: {str(e)}") + raise + + @staticmethod + def get_occupancy_rate(db: Session, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None) -> Dict[str, Any]: + """Calculate Occupancy Rate""" + try: + total_rooms = db.query(Room).count() + + if not start_date or not end_date: + end_date = datetime.utcnow() + start_date = end_date - timedelta(days=30) + + if start_date > end_date: + # Swap if dates are reversed + start_date, end_date = end_date, start_date + + days = (end_date - start_date).days + 1 + if days <= 0: + days = 1 + + available_room_nights = total_rooms * days if total_rooms > 0 else 0 + + # Calculate occupied room nights by fetching bookings and calculating in Python + # Use load_only to exclude non-existent columns (rate_plan_id, group_booking_id) + bookings_query = db.query(Booking).options( + load_only(Booking.id, Booking.check_in_date, Booking.check_out_date, Booking.status) + ).filter( + Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in, BookingStatus.checked_out]) + ) + + # Filter bookings that overlap with the date range + bookings = bookings_query.filter( + and_(Booking.check_in_date <= end_date, Booking.check_out_date >= start_date) + ).all() + + occupied_room_nights = 0 + + for booking in bookings: + try: + if booking.check_in_date and booking.check_out_date: + # Calculate overlap with date range + overlap_start = max(booking.check_in_date, start_date) + overlap_end = min(booking.check_out_date, end_date) + + if overlap_start <= overlap_end: + nights = (overlap_end - overlap_start).days + 1 + if nights > 0: + occupied_room_nights += nights + except Exception as e: + logger.warning(f"Error processing booking {booking.id} for occupancy: {str(e)}") + continue + + occupancy_rate = (occupied_room_nights / available_room_nights * 100) if available_room_nights > 0 else 0.0 + + return { + 'occupancy_rate': round(occupancy_rate, 2), + 'occupied_room_nights': int(occupied_room_nights), + 'available_room_nights': available_room_nights, + 'period_days': days + } + except Exception as e: + logger.error(f"Error calculating occupancy rate: {str(e)}") + raise + + @staticmethod + def get_revenue_forecast(db: Session, forecast_days: int = 30) -> Dict[str, Any]: + """Revenue forecasting based on historical data""" + end_date = datetime.utcnow() + start_date = end_date - timedelta(days=90) # Use last 90 days for forecast + + # Get daily revenue for last 90 days + daily_revenue = db.query( + func.date(Payment.payment_date).label('date'), + func.sum(Payment.amount).label('revenue') + ).filter( + Payment.payment_status == PaymentStatus.completed, + Payment.payment_date >= start_date + ).group_by(func.date(Payment.payment_date)).order_by(func.date(Payment.payment_date)).all() + + if not daily_revenue: + return {'forecast': [], 'average_daily_revenue': 0, 'forecast_period': forecast_days} + + # Calculate average daily revenue + total_revenue = sum(float(rev) for _, rev in daily_revenue) + avg_daily_revenue = total_revenue / len(daily_revenue) if daily_revenue else 0 + + # Simple forecast: use average daily revenue + forecast = [] + for i in range(1, forecast_days + 1): + forecast_date = (end_date + timedelta(days=i)).date() + forecast.append({ + 'date': forecast_date.isoformat(), + 'forecasted_revenue': round(avg_daily_revenue, 2), + 'confidence': 'medium' + }) + + return { + 'forecast': forecast, + 'average_daily_revenue': round(avg_daily_revenue, 2), + 'forecast_period': forecast_days, + 'based_on_days': len(daily_revenue) + } + + @staticmethod + def get_market_penetration(db: Session, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None) -> Dict[str, Any]: + """Market penetration analysis""" + if not start_date or not end_date: + end_date = datetime.utcnow() + start_date = end_date - timedelta(days=30) + + # Get bookings by source/channel (if available) + # For now, we'll analyze by booking creation method + # Use func.count() to avoid loading all columns (including non-existent rate_plan_id) + total_bookings = db.query(Booking).filter( + Booking.created_at >= start_date, + Booking.created_at <= end_date + ).with_entities(func.count(Booking.id)).scalar() or 0 + + # Analyze by room type popularity + room_type_bookings = db.query( + RoomType.name, + func.count(Booking.id).label('bookings'), + func.sum(Payment.amount).label('revenue') + ).join(Room, RoomType.id == Room.room_type_id).join( + Booking, Room.id == Booking.room_id + ).join( + Payment, Booking.id == Payment.booking_id + ).filter( + Payment.payment_status == PaymentStatus.completed, + Booking.created_at >= start_date, + Booking.created_at <= end_date + ).group_by(RoomType.name).all() + + penetration_by_type = [ + { + 'room_type': name, + 'bookings': int(count), + 'revenue': float(revenue or 0), + 'market_share': round((count / total_bookings * 100) if total_bookings > 0 else 0, 2) + } + for name, count, revenue in room_type_bookings + ] + + return { + 'total_bookings': total_bookings, + 'penetration_by_room_type': penetration_by_type, + 'period': { + 'start': start_date.isoformat(), + 'end': end_date.isoformat() + } + } + + # ==================== OPERATIONAL ANALYTICS ==================== + + @staticmethod + def get_staff_performance(db: Session, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None) -> Dict[str, Any]: + """Staff performance metrics""" + if not start_date or not end_date: + end_date = datetime.utcnow() + start_date = end_date - timedelta(days=30) + + # Get staff users + from ..models.role import Role + staff_users = db.query(User).join(Role, User.role_id == Role.id).filter( + or_(Role.name == 'staff', Role.name == 'admin') + ).all() + + performance_metrics = [] + for staff in staff_users: + # Count bookings handled (if we track this) + # For now, we'll use check-ins/check-outs as proxy + checkins = db.query(func.count(Booking.id)).filter( + Booking.status == BookingStatus.checked_in, + Booking.created_at >= start_date, + Booking.created_at <= end_date + ).scalar() or 0 + + performance_metrics.append({ + 'staff_id': staff.id, + 'staff_name': staff.full_name, + 'email': staff.email, + 'check_ins_handled': checkins, + 'performance_score': checkins # Simple metric + }) + + return { + 'staff_performance': performance_metrics, + 'period': { + 'start': start_date.isoformat(), + 'end': end_date.isoformat() + } + } + + @staticmethod + def get_service_usage_analytics(db: Session, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None) -> Dict[str, Any]: + """Service usage analytics""" + query = db.query( + Service.id, + Service.name, + Service.category, + func.count(ServiceUsage.id).label('usage_count'), + func.sum(ServiceUsage.total_price).label('total_revenue'), + func.avg(ServiceUsage.unit_price).label('avg_price') + ).join(ServiceUsage, Service.id == ServiceUsage.service_id) + + if start_date: + query = query.filter(ServiceUsage.usage_date >= start_date) + if end_date: + query = query.filter(ServiceUsage.usage_date <= end_date) + + service_data = query.group_by(Service.id, Service.name, Service.category).all() + + services = [ + { + 'service_id': sid, + 'service_name': name, + 'category': category, + 'usage_count': int(count), + 'total_revenue': float(revenue or 0), + 'average_price': float(avg_price or 0) + } + for sid, name, category, count, revenue, avg_price in service_data + ] + + return { + 'services': services, + 'total_services': len(services), + 'total_usage': sum(s['usage_count'] for s in services), + 'total_revenue': sum(s['total_revenue'] for s in services) + } + + @staticmethod + def get_operational_efficiency(db: Session, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None) -> Dict[str, Any]: + """Operational efficiency metrics""" + if not start_date or not end_date: + end_date = datetime.utcnow() + start_date = end_date - timedelta(days=30) + + # Booking conversion rate + # Use func.count() to avoid loading all columns (including non-existent rate_plan_id) + total_bookings = db.query(Booking).filter( + Booking.created_at >= start_date, + Booking.created_at <= end_date + ).with_entities(func.count(Booking.id)).scalar() or 0 + + confirmed_bookings = db.query(Booking).filter( + Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in, BookingStatus.checked_out]), + Booking.created_at >= start_date, + Booking.created_at <= end_date + ).with_entities(func.count(Booking.id)).scalar() or 0 + + conversion_rate = (confirmed_bookings / total_bookings * 100) if total_bookings > 0 else 0 + + # Average booking value + avg_booking_value = db.query(func.avg(Booking.total_price)).filter( + Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in, BookingStatus.checked_out]), + Booking.created_at >= start_date, + Booking.created_at <= end_date + ).scalar() or 0.0 + + # Cancellation rate + cancelled_bookings = db.query(Booking).filter( + Booking.status == BookingStatus.cancelled, + Booking.created_at >= start_date, + Booking.created_at <= end_date + ).with_entities(func.count(Booking.id)).scalar() or 0 + + cancellation_rate = (cancelled_bookings / total_bookings * 100) if total_bookings > 0 else 0 + + return { + 'conversion_rate': round(conversion_rate, 2), + 'average_booking_value': round(float(avg_booking_value), 2), + 'cancellation_rate': round(cancellation_rate, 2), + 'total_bookings': total_bookings, + 'confirmed_bookings': confirmed_bookings, + 'cancelled_bookings': cancelled_bookings + } + + # ==================== GUEST ANALYTICS ==================== + + @staticmethod + def get_guest_lifetime_value(db: Session, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None) -> Dict[str, Any]: + """Guest Lifetime Value (LTV) analysis""" + query = db.query( + User.id, + User.full_name, + User.email, + func.count(distinct(Booking.id)).label('total_bookings'), + func.sum(Payment.amount).label('total_spent') + ).join(Booking, User.id == Booking.user_id).join( + Payment, Booking.id == Payment.booking_id + ).filter( + Payment.payment_status == PaymentStatus.completed + ) + + if start_date: + query = query.filter(Payment.payment_date >= start_date) + if end_date: + query = query.filter(Payment.payment_date <= end_date) + + guest_data = query.group_by(User.id, User.full_name, User.email).order_by( + func.sum(Payment.amount).desc() + ).limit(100).all() + + guests = [ + { + 'user_id': uid, + 'name': name, + 'email': email, + 'total_bookings': int(bookings), + 'lifetime_value': float(spent or 0), + 'average_booking_value': round(float(spent or 0) / int(bookings) if bookings > 0 else 0, 2) + } + for uid, name, email, bookings, spent in guest_data + ] + + avg_ltv = sum(g['lifetime_value'] for g in guests) / len(guests) if guests else 0 + + return { + 'guests': guests, + 'average_ltv': round(avg_ltv, 2), + 'total_guests_analyzed': len(guests) + } + + @staticmethod + def get_customer_acquisition_cost(db: Session, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None) -> Dict[str, Any]: + """Customer Acquisition Cost (CAC) analysis""" + if not start_date or not end_date: + end_date = datetime.utcnow() + start_date = end_date - timedelta(days=30) + + # Get new customers (first booking in period) + new_customers = db.query(distinct(Booking.user_id)).filter( + Booking.created_at >= start_date, + Booking.created_at <= end_date + ).subquery() + + # Get first booking date for each user + first_booking_dates = db.query( + Booking.user_id, + func.min(Booking.created_at).label('first_booking') + ).group_by(Booking.user_id).subquery() + + new_customers_in_period = db.query(Booking.user_id).join( + first_booking_dates, + Booking.user_id == first_booking_dates.c.user_id + ).filter( + first_booking_dates.c.first_booking >= start_date, + first_booking_dates.c.first_booking <= end_date + ).distinct().count() + + # For CAC calculation, we'd need marketing spend data + # For now, we'll return the number of new customers + # In a real system, CAC = Marketing Spend / New Customers + + return { + 'new_customers': new_customers_in_period, + 'period': { + 'start': start_date.isoformat(), + 'end': end_date.isoformat() + }, + 'note': 'CAC calculation requires marketing spend data' + } + + @staticmethod + def get_repeat_guest_rate(db: Session, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None) -> Dict[str, Any]: + """Repeat guest rate analysis""" + if not start_date or not end_date: + end_date = datetime.utcnow() + start_date = end_date - timedelta(days=90) + + # Total unique guests + total_guests = db.query(distinct(Booking.user_id)).filter( + Booking.created_at >= start_date, + Booking.created_at <= end_date + ).count() + + # Guests with multiple bookings + repeat_guests = db.query(Booking.user_id).filter( + Booking.created_at >= start_date, + Booking.created_at <= end_date + ).group_by(Booking.user_id).having( + func.count(Booking.id) > 1 + ).count() + + repeat_rate = (repeat_guests / total_guests * 100) if total_guests > 0 else 0 + + return { + 'repeat_guest_rate': round(repeat_rate, 2), + 'total_guests': total_guests, + 'repeat_guests': repeat_guests, + 'one_time_guests': total_guests - repeat_guests + } + + @staticmethod + def get_guest_satisfaction_trends(db: Session, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None) -> Dict[str, Any]: + """Guest satisfaction trends from reviews""" + query = db.query( + func.date(Review.created_at).label('date'), + func.avg(Review.rating).label('avg_rating'), + func.count(Review.id).label('review_count') + ).filter(Review.status == ReviewStatus.approved) + + if start_date: + query = query.filter(Review.created_at >= start_date) + if end_date: + query = query.filter(Review.created_at <= end_date) + + trends = query.group_by(func.date(Review.created_at)).order_by( + func.date(Review.created_at) + ).all() + + satisfaction_data = [ + { + 'date': str(date), + 'average_rating': round(float(avg_rating or 0), 2), + 'review_count': int(count) + } + for date, avg_rating, count in trends + ] + + overall_avg = sum(d['average_rating'] for d in satisfaction_data) / len(satisfaction_data) if satisfaction_data else 0 + + return { + 'trends': satisfaction_data, + 'overall_average_rating': round(overall_avg, 2), + 'total_reviews': sum(d['review_count'] for d in satisfaction_data) + } + + # ==================== FINANCIAL ANALYTICS ==================== + + @staticmethod + def get_profit_loss(db: Session, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None) -> Dict[str, Any]: + """Profit & Loss report""" + if not start_date or not end_date: + end_date = datetime.utcnow() + start_date = end_date - timedelta(days=30) + + # Revenue + total_revenue = db.query(func.sum(Payment.amount)).filter( + Payment.payment_status == PaymentStatus.completed, + Payment.payment_date >= start_date, + Payment.payment_date <= end_date + ).scalar() or 0.0 + + # Refunds + refunds = db.query(func.sum(Payment.amount)).filter( + Payment.payment_status == PaymentStatus.refunded, + Payment.payment_date >= start_date, + Payment.payment_date <= end_date + ).scalar() or 0.0 + + # Service costs (if we track service costs) + # For now, we'll use service revenue as proxy for costs + service_revenue = db.query(func.sum(ServiceUsage.total_price)).filter( + ServiceUsage.usage_date >= start_date, + ServiceUsage.usage_date <= end_date + ).scalar() or 0.0 + + # Net revenue + net_revenue = float(total_revenue) - float(refunds) + + # Gross profit (simplified - would need actual cost data) + gross_profit = net_revenue # Assuming no direct costs tracked + + return { + 'total_revenue': round(float(total_revenue), 2), + 'refunds': round(float(refunds), 2), + 'net_revenue': round(net_revenue, 2), + 'gross_profit': round(gross_profit, 2), + 'period': { + 'start': start_date.isoformat(), + 'end': end_date.isoformat() + } + } + + @staticmethod + def get_payment_method_analytics(db: Session, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None) -> Dict[str, Any]: + """Payment method analytics""" + query = db.query( + Payment.payment_method, + func.count(Payment.id).label('transaction_count'), + func.sum(Payment.amount).label('total_amount'), + func.avg(Payment.amount).label('avg_amount') + ).filter(Payment.payment_status == PaymentStatus.completed) + + if start_date: + query = query.filter(Payment.payment_date >= start_date) + if end_date: + query = query.filter(Payment.payment_date <= end_date) + + method_data = query.group_by(Payment.payment_method).all() + + methods = [ + { + 'payment_method': method.value if hasattr(method, 'value') else str(method), + 'transaction_count': int(count), + 'total_amount': float(total or 0), + 'average_amount': round(float(avg or 0), 2), + 'percentage': 0 # Will calculate below + } + for method, count, total, avg in method_data + ] + + total_amount = sum(m['total_amount'] for m in methods) + for method in methods: + method['percentage'] = round((method['total_amount'] / total_amount * 100) if total_amount > 0 else 0, 2) + + return { + 'payment_methods': methods, + 'total_transactions': sum(m['transaction_count'] for m in methods), + 'total_amount': round(total_amount, 2) + } + + @staticmethod + def get_refund_analysis(db: Session, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None) -> Dict[str, Any]: + """Refund analysis""" + query = db.query( + func.date(Payment.payment_date).label('date'), + func.count(Payment.id).label('refund_count'), + func.sum(Payment.amount).label('refund_amount') + ).filter( + Payment.payment_status == PaymentStatus.refunded + ) + + if start_date: + query = query.filter(Payment.payment_date >= start_date) + if end_date: + query = query.filter(Payment.payment_date <= end_date) + + refund_data = query.group_by(func.date(Payment.payment_date)).order_by( + func.date(Payment.payment_date) + ).all() + + refunds = [ + { + 'date': str(date), + 'refund_count': int(count), + 'refund_amount': float(amount or 0) + } + for date, count, amount in refund_data + ] + + total_refunds = sum(r['refund_amount'] for r in refunds) + total_count = sum(r['refund_count'] for r in refunds) + avg_refund = total_refunds / total_count if total_count > 0 else 0 + + return { + 'refunds': refunds, + 'total_refund_amount': round(total_refunds, 2), + 'total_refund_count': total_count, + 'average_refund_amount': round(avg_refund, 2) + } + + # ==================== COMPREHENSIVE ANALYTICS ==================== + + @staticmethod + def get_comprehensive_analytics( + db: Session, + start_date: Optional[str] = None, + end_date: Optional[str] = None, + include_revenue: bool = True, + include_operational: bool = True, + include_guest: bool = True, + include_financial: bool = True + ) -> Dict[str, Any]: + """Get comprehensive analytics across all categories""" + start, end = AnalyticsService.parse_date_range(start_date, end_date) + + result = { + 'period': { + 'start': start.isoformat() if start else None, + 'end': end.isoformat() if end else None + } + } + + if include_revenue: + result['revenue'] = { + 'revpar': AnalyticsService.get_revpar(db, start, end), + 'adr': AnalyticsService.get_adr(db, start, end), + 'occupancy_rate': AnalyticsService.get_occupancy_rate(db, start, end), + 'revenue_forecast': AnalyticsService.get_revenue_forecast(db), + 'market_penetration': AnalyticsService.get_market_penetration(db, start, end) + } + + if include_operational: + result['operational'] = { + 'staff_performance': AnalyticsService.get_staff_performance(db, start, end), + 'service_usage': AnalyticsService.get_service_usage_analytics(db, start, end), + 'operational_efficiency': AnalyticsService.get_operational_efficiency(db, start, end) + } + + if include_guest: + result['guest'] = { + 'lifetime_value': AnalyticsService.get_guest_lifetime_value(db, start, end), + 'customer_acquisition_cost': AnalyticsService.get_customer_acquisition_cost(db, start, end), + 'repeat_guest_rate': AnalyticsService.get_repeat_guest_rate(db, start, end), + 'satisfaction_trends': AnalyticsService.get_guest_satisfaction_trends(db, start, end) + } + + if include_financial: + result['financial'] = { + 'profit_loss': AnalyticsService.get_profit_loss(db, start, end), + 'payment_methods': AnalyticsService.get_payment_method_analytics(db, start, end), + 'refund_analysis': AnalyticsService.get_refund_analysis(db, start, end) + } + + return result + diff --git a/Backend/src/services/borica_service.py b/Backend/src/services/borica_service.py new file mode 100644 index 00000000..3e732dd8 --- /dev/null +++ b/Backend/src/services/borica_service.py @@ -0,0 +1,388 @@ +import logging +import hashlib +import hmac +import base64 +import urllib.parse +from typing import Optional, Dict, Any +from datetime import datetime +from ..config.settings import settings +from ..models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus +from ..models.booking import Booking, BookingStatus +from ..models.system_settings import SystemSettings +from sqlalchemy.orm import Session +import os + +logger = logging.getLogger(__name__) + +def get_borica_terminal_id(db: Session) -> Optional[str]: + try: + setting = db.query(SystemSettings).filter(SystemSettings.key == 'borica_terminal_id').first() + if setting and setting.value: + return setting.value + except Exception: + pass + return settings.BORICA_TERMINAL_ID if settings.BORICA_TERMINAL_ID else None + +def get_borica_merchant_id(db: Session) -> Optional[str]: + try: + setting = db.query(SystemSettings).filter(SystemSettings.key == 'borica_merchant_id').first() + if setting and setting.value: + return setting.value + except Exception: + pass + return settings.BORICA_MERCHANT_ID if settings.BORICA_MERCHANT_ID else None + +def get_borica_private_key_path(db: Session) -> Optional[str]: + try: + setting = db.query(SystemSettings).filter(SystemSettings.key == 'borica_private_key_path').first() + if setting and setting.value: + return setting.value + except Exception: + pass + return settings.BORICA_PRIVATE_KEY_PATH if settings.BORICA_PRIVATE_KEY_PATH else None + +def get_borica_certificate_path(db: Session) -> Optional[str]: + try: + setting = db.query(SystemSettings).filter(SystemSettings.key == 'borica_certificate_path').first() + if setting and setting.value: + return setting.value + except Exception: + pass + return settings.BORICA_CERTIFICATE_PATH if settings.BORICA_CERTIFICATE_PATH else None + +def get_borica_gateway_url(db: Session) -> str: + try: + setting = db.query(SystemSettings).filter(SystemSettings.key == 'borica_gateway_url').first() + if setting and setting.value: + return setting.value + except Exception: + pass + mode = get_borica_mode(db) + if mode == 'production': + return settings.BORICA_GATEWAY_URL.replace('dev', 'gate') if 'dev' in settings.BORICA_GATEWAY_URL else settings.BORICA_GATEWAY_URL + return settings.BORICA_GATEWAY_URL + +def get_borica_mode(db: Session) -> str: + try: + setting = db.query(SystemSettings).filter(SystemSettings.key == 'borica_mode').first() + if setting and setting.value: + return setting.value + except Exception: + pass + return settings.BORICA_MODE if settings.BORICA_MODE else 'test' + +class BoricaService: + """ + Borica payment gateway service for processing online payments. + Borica is the Bulgarian payment gateway system. + """ + + @staticmethod + def generate_transaction_id(booking_id: int) -> str: + """Generate a unique transaction ID for Borica""" + timestamp = datetime.utcnow().strftime('%Y%m%d%H%M%S') + return f"BOOK{booking_id:06d}{timestamp}" + + @staticmethod + def create_payment_request( + amount: float, + currency: str, + order_id: str, + description: str, + return_url: str, + db: Optional[Session] = None + ) -> Dict[str, Any]: + """ + Create a Borica payment request. + Returns the payment form data that needs to be submitted to Borica gateway. + """ + terminal_id = None + merchant_id = None + gateway_url = None + + if db: + terminal_id = get_borica_terminal_id(db) + merchant_id = get_borica_merchant_id(db) + gateway_url = get_borica_gateway_url(db) + + if not terminal_id: + terminal_id = settings.BORICA_TERMINAL_ID + if not merchant_id: + merchant_id = settings.BORICA_MERCHANT_ID + if not gateway_url: + gateway_url = get_borica_gateway_url(db) if db else settings.BORICA_GATEWAY_URL + + if not terminal_id or not merchant_id: + raise ValueError('Borica Terminal ID and Merchant ID are required') + + # Convert amount to minor units (cents/stotinki) + amount_minor = int(round(amount * 100)) + + # Format amount as string with leading zeros (12 digits) + amount_str = f"{amount_minor:012d}" + + # Create transaction timestamp (YYYYMMDDHHMMSS) + transaction_timestamp = datetime.utcnow().strftime('%Y%m%d%H%M%S') + + # Create order description (max 125 chars) + order_description = description[:125] if description else f"Booking Payment {order_id}" + + # Create signature data string + # Format: TERMINAL=TERMINAL_ID,TRTYPE=1,ORDER=ORDER_ID,AMOUNT=AMOUNT,TIMESTAMP=TIMESTAMP + signature_data = f"TERMINAL={terminal_id},TRTYPE=1,ORDER={order_id},AMOUNT={amount_str},TIMESTAMP={transaction_timestamp}" + + # For test mode, we'll use a simple HMAC signature + # In production, you would use the private key to sign + private_key_path = get_borica_private_key_path(db) if db else settings.BORICA_PRIVATE_KEY_PATH + + # Generate signature (simplified for testing - in production use proper certificate signing) + signature = BoricaService._generate_signature(signature_data, private_key_path) + + return { + 'terminal_id': terminal_id, + 'merchant_id': merchant_id, + 'order_id': order_id, + 'amount': amount_str, + 'currency': currency.upper(), + 'description': order_description, + 'timestamp': transaction_timestamp, + 'signature': signature, + 'gateway_url': gateway_url, + 'return_url': return_url, + 'trtype': '1' # Sale transaction + } + + @staticmethod + def _generate_signature(data: str, private_key_path: Optional[str] = None) -> str: + """ + Generate signature for Borica request. + In production, this should use the actual private key certificate. + For testing, we'll use a simple hash. + """ + if private_key_path and os.path.exists(private_key_path): + try: + # In production, you would load the private key and sign the data + # This is a simplified version for testing + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import padding + from cryptography.hazmat.backends import default_backend + + with open(private_key_path, 'rb') as key_file: + private_key = serialization.load_pem_private_key( + key_file.read(), + password=None, + backend=default_backend() + ) + + signature = private_key.sign( + data.encode('utf-8'), + padding.PKCS1v15(), + hashes.SHA1() + ) + return base64.b64encode(signature).decode('utf-8') + except Exception as e: + logger.warning(f'Failed to sign with private key, using test signature: {e}') + + # Fallback: simple hash for testing + return hashlib.sha256(data.encode('utf-8')).hexdigest()[:40] + + @staticmethod + def verify_response_signature(response_data: Dict[str, Any], db: Optional[Session] = None) -> bool: + """ + Verify the signature of a Borica response. + """ + try: + # Extract signature from response + signature = response_data.get('P_SIGN', '') + if not signature: + return False + + # Reconstruct signature data from response + terminal_id = response_data.get('TERMINAL', '') + trtype = response_data.get('TRTYPE', '') + order_id = response_data.get('ORDER', '') + amount = response_data.get('AMOUNT', '') + timestamp = response_data.get('TIMESTAMP', '') + nonce = response_data.get('NONCE', '') + rcode = response_data.get('RC', '') + + # Build signature string + signature_data = f"TERMINAL={terminal_id},TRTYPE={trtype},ORDER={order_id},AMOUNT={amount},TIMESTAMP={timestamp},NONCE={nonce},RC={rcode}" + + # Verify signature using certificate + certificate_path = get_borica_certificate_path(db) if db else settings.BORICA_CERTIFICATE_PATH + + if certificate_path and os.path.exists(certificate_path): + try: + from cryptography import x509 + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import padding + + with open(certificate_path, 'rb') as cert_file: + cert = x509.load_pem_x509_certificate( + cert_file.read(), + default_backend() + ) + + public_key = cert.public_key() + signature_bytes = base64.b64decode(signature) + + public_key.verify( + signature_bytes, + signature_data.encode('utf-8'), + padding.PKCS1v15(), + hashes.SHA1() + ) + return True + except Exception as e: + logger.error(f'Signature verification failed: {e}') + return False + + # For testing, accept if signature exists + return bool(signature) + except Exception as e: + logger.error(f'Error verifying signature: {e}') + return False + + @staticmethod + async def confirm_payment( + response_data: Dict[str, Any], + db: Session, + booking_id: Optional[int] = None + ) -> Dict[str, Any]: + """ + Confirm a Borica payment from the response data. + """ + try: + # Verify signature + if not BoricaService.verify_response_signature(response_data, db): + raise ValueError('Invalid payment response signature') + + # Extract response data + order_id = response_data.get('ORDER', '') + amount_str = response_data.get('AMOUNT', '') + rcode = response_data.get('RC', '') + rcode_msg = response_data.get('RCTEXT', '') + + # Convert amount from minor units + amount = float(amount_str) / 100 if amount_str else 0.0 + + # Get booking ID from order_id if not provided + if not booking_id and order_id: + # Extract booking ID from order_id (format: BOOK{booking_id}{timestamp}) + try: + if order_id.startswith('BOOK'): + booking_id = int(order_id[4:10]) + except (ValueError, IndexError): + pass + + if not booking_id: + raise ValueError('Booking ID is required') + + booking = db.query(Booking).filter(Booking.id == booking_id).first() + if not booking: + raise ValueError('Booking not found') + + # Check response code (00 = success) + if rcode != '00': + # Payment failed + payment = db.query(Payment).filter( + Payment.booking_id == booking_id, + Payment.transaction_id == order_id, + Payment.payment_method == PaymentMethod.borica + ).first() + + if payment: + payment.payment_status = PaymentStatus.failed + payment.notes = f'Borica payment failed: {rcode} - {rcode_msg}' + db.commit() + db.refresh(payment) + + raise ValueError(f'Payment failed: {rcode} - {rcode_msg}') + + # Payment successful + payment = db.query(Payment).filter( + Payment.booking_id == booking_id, + Payment.transaction_id == order_id, + Payment.payment_method == PaymentMethod.borica + ).first() + + if not payment: + payment = db.query(Payment).filter( + Payment.booking_id == booking_id, + Payment.payment_method == PaymentMethod.borica, + Payment.payment_status == PaymentStatus.pending + ).order_by(Payment.created_at.desc()).first() + + if payment: + payment.payment_status = PaymentStatus.completed + payment.payment_date = datetime.utcnow() + payment.amount = amount + payment.transaction_id = order_id + payment.notes = f'Borica payment completed: {rcode_msg}' + else: + payment_type = PaymentType.full + if booking.requires_deposit and not booking.deposit_paid: + payment_type = PaymentType.deposit + + payment = Payment( + booking_id=booking_id, + amount=amount, + payment_method=PaymentMethod.borica, + payment_type=payment_type, + payment_status=PaymentStatus.completed, + transaction_id=order_id, + payment_date=datetime.utcnow(), + notes=f'Borica payment completed: {rcode_msg}' + ) + db.add(payment) + + db.commit() + db.refresh(payment) + + # Update booking status if payment completed + if payment.payment_status == PaymentStatus.completed: + db.refresh(booking) + total_paid = sum( + float(p.amount) for p in booking.payments + if p.payment_status == PaymentStatus.completed + ) + + if payment.payment_type == PaymentType.deposit: + booking.deposit_paid = True + if booking.status in [BookingStatus.pending, BookingStatus.cancelled]: + booking.status = BookingStatus.confirmed + elif payment.payment_type == PaymentType.full: + if total_paid >= float(booking.total_price): + if booking.status in [BookingStatus.pending, BookingStatus.cancelled]: + booking.status = BookingStatus.confirmed + + db.commit() + db.refresh(booking) + + def get_enum_value(enum_obj): + if enum_obj is None: + return None + if isinstance(enum_obj, (PaymentMethod, PaymentType, PaymentStatus)): + return enum_obj.value + return enum_obj + + return { + 'id': payment.id, + 'booking_id': payment.booking_id, + 'amount': float(payment.amount) if payment.amount else 0.0, + 'payment_method': get_enum_value(payment.payment_method), + 'payment_type': get_enum_value(payment.payment_type), + 'payment_status': get_enum_value(payment.payment_status), + 'transaction_id': payment.transaction_id, + 'payment_date': payment.payment_date.isoformat() if payment.payment_date else None + } + except ValueError as e: + db.rollback() + raise + except Exception as e: + logger.error(f'Error confirming Borica payment: {e}', exc_info=True) + db.rollback() + raise ValueError(f'Error confirming payment: {str(e)}') + diff --git a/Backend/src/services/email_campaign_service.py b/Backend/src/services/email_campaign_service.py new file mode 100644 index 00000000..0a9d138e --- /dev/null +++ b/Backend/src/services/email_campaign_service.py @@ -0,0 +1,517 @@ +from sqlalchemy.orm import Session +from typing import List, Dict, Any, Optional +from datetime import datetime, timedelta +from ..models.email_campaign import ( + Campaign, CampaignStatus, CampaignType, EmailStatus, + CampaignSegment, EmailTemplate, CampaignEmail, EmailClick, + DripSequence, DripSequenceStep, DripSequenceEnrollment, Unsubscribe +) +from ..models.user import User +from ..models.booking import Booking +from ..config.logging_config import get_logger +from ..utils.mailer import send_email + +logger = get_logger(__name__) + +class EmailCampaignService: + """Service for managing email campaigns""" + + @staticmethod + def create_campaign( + db: Session, + name: str, + subject: str, + html_content: str, + text_content: Optional[str] = None, + campaign_type: CampaignType = CampaignType.newsletter, + segment_id: Optional[int] = None, + scheduled_at: Optional[datetime] = None, + created_by: Optional[int] = None, + **kwargs + ) -> Campaign: + """Create a new email campaign""" + campaign = Campaign( + name=name, + subject=subject, + html_content=html_content, + text_content=text_content, + campaign_type=campaign_type, + segment_id=segment_id, + scheduled_at=scheduled_at, + created_by=created_by, + **kwargs + ) + db.add(campaign) + db.commit() + db.refresh(campaign) + return campaign + + @staticmethod + def get_campaign_recipients( + db: Session, + campaign: Campaign + ) -> List[User]: + """Get list of recipients for a campaign based on segment""" + if campaign.segment_id: + segment = db.query(CampaignSegment).filter( + CampaignSegment.id == campaign.segment_id + ).first() + if segment: + return EmailCampaignService._apply_segment_criteria(db, segment.criteria) + + # If no segment, return all active users (or based on campaign type) + if campaign.campaign_type == CampaignType.newsletter: + return db.query(User).filter(User.is_active == True).all() + + return [] + + @staticmethod + def _apply_segment_criteria(db: Session, criteria: Dict[str, Any]) -> List[User]: + """Apply segment criteria to get matching users""" + query = db.query(User).filter(User.is_active == True) + + # Role filter + if 'role' in criteria: + from ..models.role import Role + role = db.query(Role).filter(Role.name == criteria['role']).first() + if role: + query = query.filter(User.role_id == role.id) + + # Last booking days + if 'last_booking_days' in criteria: + cutoff_date = datetime.utcnow() - timedelta(days=criteria['last_booking_days']) + query = query.join(Booking).filter(Booking.created_at >= cutoff_date) + + # VIP status + if 'is_vip' in criteria: + query = query.filter(User.is_vip == criteria['is_vip']) + + # Has bookings + if 'has_bookings' in criteria: + if criteria['has_bookings']: + query = query.join(Booking).distinct() + else: + query = query.outerjoin(Booking).filter(Booking.id.is_(None)) + + return query.distinct().all() + + @staticmethod + def send_campaign(db: Session, campaign_id: int) -> Dict[str, Any]: + """Send an email campaign""" + campaign = db.query(Campaign).filter(Campaign.id == campaign_id).first() + if not campaign: + raise ValueError("Campaign not found") + + if campaign.status not in [CampaignStatus.draft, CampaignStatus.scheduled]: + raise ValueError(f"Cannot send campaign with status: {campaign.status}") + + # Get recipients + recipients = EmailCampaignService.get_campaign_recipients(db, campaign) + campaign.total_recipients = len(recipients) + + # Update status + campaign.status = CampaignStatus.sending + db.commit() + + sent_count = 0 + failed_count = 0 + + for user in recipients: + # Check if user unsubscribed + if EmailCampaignService._is_unsubscribed(db, user.email, campaign): + continue + + try: + # Create campaign email record + campaign_email = CampaignEmail( + campaign_id=campaign.id, + user_id=user.id, + email=user.email, + status=EmailStatus.pending + ) + db.add(campaign_email) + db.flush() + + # Replace template variables + html_content = EmailCampaignService._replace_variables( + campaign.html_content or '', + user + ) + subject = EmailCampaignService._replace_variables( + campaign.subject, + user + ) + + # Send email (async function) + import asyncio + try: + loop = asyncio.get_event_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + loop.run_until_complete(send_email( + to=user.email, + subject=subject, + html=html_content, + text=campaign.text_content + )) + + campaign_email.status = EmailStatus.sent + campaign_email.sent_at = datetime.utcnow() + sent_count += 1 + + except Exception as e: + logger.error(f"Failed to send email to {user.email}: {str(e)}") + if 'campaign_email' in locals(): + campaign_email.status = EmailStatus.failed + campaign_email.error_message = str(e) + failed_count += 1 + + # Update campaign stats + campaign.total_sent = sent_count + campaign.status = CampaignStatus.sent + campaign.sent_at = datetime.utcnow() + db.commit() + + return { + "sent": sent_count, + "failed": failed_count, + "total": len(recipients) + } + + @staticmethod + def _replace_variables(content: str, user: User) -> str: + """Replace template variables with user data""" + replacements = { + '{{name}}': user.full_name or 'Guest', + '{{email}}': user.email, + '{{first_name}}': user.full_name.split()[0] if user.full_name else 'Guest', + } + + for key, value in replacements.items(): + content = content.replace(key, str(value)) + + return content + + @staticmethod + def _is_unsubscribed(db: Session, email: str, campaign: Optional[Campaign] = None) -> bool: + """Check if email is unsubscribed""" + query = db.query(Unsubscribe).filter(Unsubscribe.email == email) + + # Check for global unsubscribe + global_unsubscribe = query.filter(Unsubscribe.unsubscribe_all == True).first() + if global_unsubscribe: + return True + + # Check for campaign-specific unsubscribe + if campaign: + campaign_unsubscribe = query.filter( + Unsubscribe.campaign_id == campaign.id + ).first() + if campaign_unsubscribe: + return True + + # Check for type-specific unsubscribe + type_unsubscribe = query.filter( + Unsubscribe.unsubscribe_type == campaign.campaign_type.value + ).first() + if type_unsubscribe: + return True + + return False + + @staticmethod + def track_email_open(db: Session, campaign_email_id: int): + """Track email open""" + campaign_email = db.query(CampaignEmail).filter( + CampaignEmail.id == campaign_email_id + ).first() + + if campaign_email and campaign_email.status == EmailStatus.sent: + campaign_email.status = EmailStatus.opened + campaign_email.opened_at = datetime.utcnow() + campaign_email.open_count += 1 + campaign_email.last_opened_at = datetime.utcnow() + + # Update campaign stats + campaign = campaign_email.campaign + campaign.total_opened += 1 + if campaign.total_delivered > 0: + campaign.open_rate = (campaign.total_opened / campaign.total_delivered) * 100 + + db.commit() + + @staticmethod + def track_email_click(db: Session, campaign_email_id: int, url: str, ip_address: Optional[str] = None, user_agent: Optional[str] = None): + """Track email click""" + campaign_email = db.query(CampaignEmail).filter( + CampaignEmail.id == campaign_email_id + ).first() + + if campaign_email: + # Create click record + click = EmailClick( + campaign_email_id=campaign_email_id, + url=url, + ip_address=ip_address, + user_agent=user_agent + ) + db.add(click) + + # Update email status + if campaign_email.status == EmailStatus.opened: + campaign_email.status = EmailStatus.clicked + campaign_email.clicked_at = datetime.utcnow() + + campaign_email.click_count += 1 + campaign_email.last_clicked_at = datetime.utcnow() + + # Update campaign stats + campaign = campaign_email.campaign + campaign.total_clicked += 1 + if campaign.total_opened > 0: + campaign.click_rate = (campaign.total_clicked / campaign.total_opened) * 100 + + db.commit() + + @staticmethod + def create_segment( + db: Session, + name: str, + criteria: Dict[str, Any], + description: Optional[str] = None, + created_by: Optional[int] = None + ) -> CampaignSegment: + """Create a new campaign segment""" + segment = CampaignSegment( + name=name, + description=description, + criteria=criteria, + created_by=created_by + ) + db.add(segment) + db.commit() + db.refresh(segment) + + # Calculate estimated count + EmailCampaignService._calculate_segment_count(db, segment) + + return segment + + @staticmethod + def _calculate_segment_count(db: Session, segment: CampaignSegment): + """Calculate and update segment estimated count""" + users = EmailCampaignService._apply_segment_criteria(db, segment.criteria) + segment.estimated_count = len(users) + segment.last_calculated_at = datetime.utcnow() + db.commit() + + @staticmethod + def create_drip_sequence( + db: Session, + name: str, + trigger_event: Optional[str] = None, + description: Optional[str] = None, + created_by: Optional[int] = None + ) -> DripSequence: + """Create a new drip sequence""" + sequence = DripSequence( + name=name, + description=description, + trigger_event=trigger_event, + created_by=created_by + ) + db.add(sequence) + db.commit() + db.refresh(sequence) + return sequence + + @staticmethod + def add_drip_step( + db: Session, + sequence_id: int, + subject: str, + html_content: str, + delay_days: int = 0, + delay_hours: int = 0, + step_order: Optional[int] = None, + text_content: Optional[str] = None, + template_id: Optional[int] = None + ) -> DripSequenceStep: + """Add a step to a drip sequence""" + if step_order is None: + # Get next step order + last_step = db.query(DripSequenceStep).filter( + DripSequenceStep.sequence_id == sequence_id + ).order_by(DripSequenceStep.step_order.desc()).first() + step_order = (last_step.step_order + 1) if last_step else 1 + + step = DripSequenceStep( + sequence_id=sequence_id, + step_order=step_order, + subject=subject, + html_content=html_content, + text_content=text_content, + template_id=template_id, + delay_days=delay_days, + delay_hours=delay_hours + ) + db.add(step) + db.commit() + db.refresh(step) + return step + + @staticmethod + def enroll_user_in_drip( + db: Session, + sequence_id: int, + user_id: int, + trigger_data: Optional[Dict[str, Any]] = None + ) -> DripSequenceEnrollment: + """Enroll a user in a drip sequence""" + # Check if already enrolled + existing = db.query(DripSequenceEnrollment).filter( + DripSequenceEnrollment.sequence_id == sequence_id, + DripSequenceEnrollment.user_id == user_id, + DripSequenceEnrollment.completed == False + ).first() + + if existing: + return existing + + sequence = db.query(DripSequence).filter(DripSequence.id == sequence_id).first() + if not sequence: + raise ValueError("Drip sequence not found") + + # Get first step + first_step = db.query(DripSequenceStep).filter( + DripSequenceStep.sequence_id == sequence_id, + DripSequenceStep.step_order == 1 + ).first() + + if not first_step: + raise ValueError("Drip sequence has no steps") + + # Calculate next send time + next_send_at = datetime.utcnow() + timedelta(days=first_step.delay_days, hours=first_step.delay_hours) + + enrollment = DripSequenceEnrollment( + sequence_id=sequence_id, + user_id=user_id, + current_step=0, + next_send_at=next_send_at, + trigger_data=trigger_data + ) + db.add(enrollment) + db.commit() + db.refresh(enrollment) + return enrollment + + @staticmethod + def process_drip_sequences(db: Session): + """Process drip sequences and send pending emails""" + # Get enrollments ready to send + enrollments = db.query(DripSequenceEnrollment).filter( + DripSequenceEnrollment.completed == False, + DripSequenceEnrollment.next_send_at <= datetime.utcnow() + ).all() + + for enrollment in enrollments: + try: + sequence = enrollment.sequence + if not sequence.is_active: + continue + + # Get next step + next_step_order = enrollment.current_step + 1 + step = db.query(DripSequenceStep).filter( + DripSequenceStep.sequence_id == sequence.id, + DripSequenceStep.step_order == next_step_order, + DripSequenceStep.is_active == True + ).first() + + if not step: + # Sequence completed + enrollment.completed = True + enrollment.completed_at = datetime.utcnow() + db.commit() + continue + + # Get user + user = db.query(User).filter(User.id == enrollment.user_id).first() + if not user or not user.is_active: + continue + + # Check unsubscribe + if EmailCampaignService._is_unsubscribed(db, user.email): + enrollment.completed = True + db.commit() + continue + + # Send email + html_content = EmailCampaignService._replace_variables(step.html_content, user) + subject = EmailCampaignService._replace_variables(step.subject, user) + + # Send email (async function) + import asyncio + try: + loop = asyncio.get_event_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + loop.run_until_complete(send_email( + to=user.email, + subject=subject, + html=html_content, + text=step.text_content + )) + + # Update enrollment + enrollment.current_step = next_step_order + + # Calculate next send time + next_step = db.query(DripSequenceStep).filter( + DripSequenceStep.sequence_id == sequence.id, + DripSequenceStep.step_order == next_step_order + 1 + ).first() + + if next_step: + enrollment.next_send_at = datetime.utcnow() + timedelta( + days=next_step.delay_days, + hours=next_step.delay_hours + ) + else: + enrollment.completed = True + enrollment.completed_at = datetime.utcnow() + + db.commit() + + except Exception as e: + logger.error(f"Error processing drip enrollment {enrollment.id}: {str(e)}") + db.rollback() + + @staticmethod + def handle_abandoned_booking(db: Session, booking_id: int): + """Handle abandoned booking recovery email""" + booking = db.query(Booking).filter(Booking.id == booking_id).first() + if not booking: + return + + # Find abandoned booking recovery sequence + sequence = db.query(DripSequence).filter( + DripSequence.trigger_event == 'checkout_abandoned', + DripSequence.is_active == True + ).first() + + if sequence and booking.user_id: + EmailCampaignService.enroll_user_in_drip( + db=db, + sequence_id=sequence.id, + user_id=booking.user_id, + trigger_data={"booking_id": booking_id} + ) + +email_campaign_service = EmailCampaignService() + diff --git a/Backend/src/services/encryption_service.py b/Backend/src/services/encryption_service.py new file mode 100644 index 00000000..2d254b7f --- /dev/null +++ b/Backend/src/services/encryption_service.py @@ -0,0 +1,89 @@ +from cryptography.fernet import Fernet +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.backends import default_backend +import base64 +import os +from typing import Optional +import logging +from ..config.settings import settings + +logger = logging.getLogger(__name__) + +class EncryptionService: + """Service for data encryption at rest""" + + def __init__(self, encryption_key: Optional[str] = None): + """ + Initialize encryption service + + Args: + encryption_key: Base64-encoded encryption key. If not provided, will use ENCRYPTION_KEY from settings or env var + """ + if encryption_key: + self.key = encryption_key.encode() + else: + # Try to get key from settings first (loads from .env), then fall back to os.getenv + key_str = getattr(settings, 'ENCRYPTION_KEY', None) or os.getenv('ENCRYPTION_KEY') + if not key_str or key_str.strip() == '': + # Generate a key if not provided (for development only) + logger.warning("ENCRYPTION_KEY not set. Generating temporary key. This should be set in production!") + key = Fernet.generate_key() + self.key = key + else: + self.key = key_str.encode() + + try: + self.cipher = Fernet(self.key) + except Exception as e: + logger.error(f"Failed to initialize encryption: {str(e)}") + raise + + def encrypt(self, data: str) -> str: + """Encrypt a string""" + try: + if not data: + return data + encrypted = self.cipher.encrypt(data.encode()) + return base64.urlsafe_b64encode(encrypted).decode() + except Exception as e: + logger.error(f"Encryption failed: {str(e)}") + raise + + def decrypt(self, encrypted_data: str) -> str: + """Decrypt a string""" + try: + if not encrypted_data: + return encrypted_data + decoded = base64.urlsafe_b64decode(encrypted_data.encode()) + decrypted = self.cipher.decrypt(decoded) + return decrypted.decode() + except Exception as e: + logger.error(f"Decryption failed: {str(e)}") + raise + + def encrypt_dict(self, data: dict) -> dict: + """Encrypt sensitive fields in a dictionary""" + encrypted = {} + sensitive_fields = ['password', 'token', 'secret', 'key', 'api_key', 'access_token', 'refresh_token'] + + for key, value in data.items(): + if any(sensitive in key.lower() for sensitive in sensitive_fields): + if isinstance(value, str): + encrypted[key] = self.encrypt(value) + else: + encrypted[key] = value + else: + encrypted[key] = value + + return encrypted + + @staticmethod + def generate_key() -> str: + """Generate a new encryption key""" + key = Fernet.generate_key() + return key.decode() + +# Global instance +encryption_service = EncryptionService() + diff --git a/Backend/src/services/gdpr_service.py b/Backend/src/services/gdpr_service.py new file mode 100644 index 00000000..fec41425 --- /dev/null +++ b/Backend/src/services/gdpr_service.py @@ -0,0 +1,215 @@ +from sqlalchemy.orm import Session +from typing import Optional, Dict, Any, List +from datetime import datetime +import secrets +import logging + +from ..models.gdpr_compliance import ( + DataSubjectRequest, + DataSubjectRequestType, + DataSubjectRequestStatus, + DataRetentionPolicy, + ConsentRecord +) +from ..models.user import User +from ..models.booking import Booking +from ..models.payment import Payment +from ..models.review import Review +from ..config.logging_config import get_logger + +logger = get_logger(__name__) + +class GDPRService: + """Service for GDPR compliance operations""" + + @staticmethod + def create_data_subject_request( + db: Session, + email: str, + request_type: DataSubjectRequestType, + description: Optional[str] = None, + ip_address: Optional[str] = None, + user_agent: Optional[str] = None + ) -> DataSubjectRequest: + """Create a new data subject request""" + # Find user by email + user = db.query(User).filter(User.email == email.lower()).first() + + # Generate verification token + verification_token = secrets.token_urlsafe(32) + + request = DataSubjectRequest( + user_id=user.id if user else None, + email=email.lower(), + request_type=request_type, + status=DataSubjectRequestStatus.pending, + description=description, + verification_token=verification_token, + ip_address=ip_address, + user_agent=user_agent + ) + + db.add(request) + db.commit() + db.refresh(request) + + logger.info(f"Data subject request created: {request_type.value} for {email}") + return request + + @staticmethod + def verify_request(db: Session, verification_token: str) -> bool: + """Verify a data subject request""" + request = db.query(DataSubjectRequest).filter( + DataSubjectRequest.verification_token == verification_token + ).first() + + if not request: + return False + + request.verified = True + request.verified_at = datetime.utcnow() + db.commit() + + return True + + @staticmethod + def get_user_data(db: Session, user_id: int) -> Dict[str, Any]: + """Get all data for a user (for access request)""" + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise ValueError("User not found") + + # Collect all user data + data = { + "user": { + "id": user.id, + "email": user.email, + "full_name": user.full_name, + "phone": user.phone, + "created_at": user.created_at.isoformat() if user.created_at else None, + }, + "bookings": [], + "payments": [], + "reviews": [], + } + + # Get bookings + bookings = db.query(Booking).filter(Booking.user_id == user_id).all() + for booking in bookings: + data["bookings"].append({ + "id": booking.id, + "booking_number": booking.booking_number, + "check_in_date": booking.check_in_date.isoformat() if booking.check_in_date else None, + "check_out_date": booking.check_out_date.isoformat() if booking.check_out_date else None, + "total_price": float(booking.total_price) if booking.total_price else None, + "status": booking.status.value if hasattr(booking.status, 'value') else booking.status, + }) + + # Get payments + payments = db.query(Payment).filter(Payment.booking.has(user_id=user_id)).all() + for payment in payments: + data["payments"].append({ + "id": payment.id, + "amount": float(payment.amount) if payment.amount else None, + "payment_method": payment.payment_method, + "payment_status": payment.payment_status, + "payment_date": payment.payment_date.isoformat() if payment.payment_date else None, + }) + + # Get reviews + reviews = db.query(Review).filter(Review.user_id == user_id).all() + for review in reviews: + data["reviews"].append({ + "id": review.id, + "rating": review.rating, + "comment": review.comment, + "created_at": review.created_at.isoformat() if review.created_at else None, + }) + + return data + + @staticmethod + def delete_user_data(db: Session, user_id: int) -> bool: + """Delete all user data (for erasure request)""" + try: + user = db.query(User).filter(User.id == user_id).first() + if not user: + return False + + # Anonymize user data instead of deleting (for audit trail) + user.email = f"deleted_{user.id}@deleted.local" + user.full_name = "Deleted User" + user.phone = None + user.password = "deleted" # Invalidate password + + # Delete related data + # Note: In production, you might want to soft-delete or anonymize instead + db.query(Booking).filter(Booking.user_id == user_id).delete() + db.query(Review).filter(Review.user_id == user_id).delete() + + db.commit() + logger.info(f"User data deleted/anonymized for user {user_id}") + return True + except Exception as e: + logger.error(f"Error deleting user data: {str(e)}") + db.rollback() + return False + + @staticmethod + def export_user_data(db: Session, user_id: int) -> Dict[str, Any]: + """Export user data in portable format (for portability request)""" + return GDPRService.get_user_data(db, user_id) + + @staticmethod + def record_consent( + db: Session, + user_id: int, + consent_type: str, + granted: bool, + ip_address: Optional[str] = None, + user_agent: Optional[str] = None, + version: Optional[str] = None + ) -> ConsentRecord: + """Record user consent""" + # Revoke previous consent if granting new one + if granted: + previous = db.query(ConsentRecord).filter( + ConsentRecord.user_id == user_id, + ConsentRecord.consent_type == consent_type, + ConsentRecord.revoked_at.is_(None) + ).first() + + if previous: + previous.revoked_at = datetime.utcnow() + + consent = ConsentRecord( + user_id=user_id, + consent_type=consent_type, + granted=granted, + granted_at=datetime.utcnow() if granted else None, + revoked_at=datetime.utcnow() if not granted else None, + ip_address=ip_address, + user_agent=user_agent, + version=version + ) + + db.add(consent) + db.commit() + db.refresh(consent) + + return consent + + @staticmethod + def check_consent(db: Session, user_id: int, consent_type: str) -> bool: + """Check if user has granted consent""" + consent = db.query(ConsentRecord).filter( + ConsentRecord.user_id == user_id, + ConsentRecord.consent_type == consent_type, + ConsentRecord.granted == True, + ConsentRecord.revoked_at.is_(None) + ).order_by(ConsentRecord.granted_at.desc()).first() + + return consent is not None + +gdpr_service = GDPRService() + diff --git a/Backend/src/services/group_booking_service.py b/Backend/src/services/group_booking_service.py new file mode 100644 index 00000000..a736a7b9 --- /dev/null +++ b/Backend/src/services/group_booking_service.py @@ -0,0 +1,574 @@ +from sqlalchemy.orm import Session +from datetime import datetime, timedelta +from typing import Optional, List, Dict, Any +import random +import string +from decimal import Decimal +from ..models.group_booking import ( + GroupBooking, GroupBookingMember, GroupRoomBlock, GroupPayment, + GroupBookingStatus, PaymentOption +) +from ..models.booking import Booking, BookingStatus +from ..models.room import Room, RoomStatus +from ..models.room_type import RoomType +from ..models.user import User +from ..models.payment import Payment, PaymentStatus, PaymentMethod +import logging + +logger = logging.getLogger(__name__) + +class GroupBookingService: + + @staticmethod + def generate_group_booking_number(db: Session) -> str: + """Generate unique group booking number""" + max_attempts = 10 + for _ in range(max_attempts): + timestamp = datetime.utcnow().strftime('%Y%m%d') + random_suffix = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) + booking_number = f"GRP-{timestamp}-{random_suffix}" + + existing = db.query(GroupBooking).filter( + GroupBooking.group_booking_number == booking_number + ).first() + + if not existing: + return booking_number + + # Fallback + return f"GRP-{int(datetime.utcnow().timestamp())}" + + @staticmethod + def calculate_group_discount( + total_rooms: int, + base_rate: Decimal, + discount_percentage: Optional[float] = None + ) -> Dict[str, Any]: + """Calculate group discount based on number of rooms""" + original_total = base_rate * total_rooms + + # Default discount tiers + if discount_percentage is None: + if total_rooms >= 20: + discount_percentage = 15.0 + elif total_rooms >= 10: + discount_percentage = 10.0 + elif total_rooms >= 5: + discount_percentage = 5.0 + else: + discount_percentage = 0.0 + + discount_amount = original_total * Decimal(str(discount_percentage)) / Decimal('100') + total_price = original_total - discount_amount + + return { + 'original_total': float(original_total), + 'discount_percentage': discount_percentage, + 'discount_amount': float(discount_amount), + 'total_price': float(total_price) + } + + @staticmethod + def check_room_availability( + db: Session, + room_type_id: int, + check_in: datetime, + check_out: datetime, + num_rooms: int + ) -> Dict[str, Any]: + """Check if enough rooms are available for blocking""" + # Get all rooms of this type + rooms = db.query(Room).filter(Room.room_type_id == room_type_id).all() + + if len(rooms) < num_rooms: + return { + 'available': False, + 'available_count': len(rooms), + 'required_count': num_rooms, + 'message': f'Only {len(rooms)} rooms available, {num_rooms} required' + } + + # Check for conflicting bookings + available_rooms = [] + for room in rooms: + # Check if room has any bookings during the period + conflicting_bookings = db.query(Booking).filter( + Booking.room_id == room.id, + Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in]), + Booking.check_in_date < check_out, + Booking.check_out_date > check_in + ).count() + + # Check for other group blocks + conflicting_blocks = db.query(GroupRoomBlock).join(GroupBooking).filter( + GroupRoomBlock.room_type_id == room_type_id, + GroupBooking.status.in_([ + GroupBookingStatus.confirmed, + GroupBookingStatus.partially_confirmed, + GroupBookingStatus.checked_in + ]), + GroupBooking.check_in_date < check_out, + GroupBooking.check_out_date > check_in, + GroupRoomBlock.is_active == True + ).count() + + if conflicting_bookings == 0 and conflicting_blocks == 0: + available_rooms.append(room) + + if len(available_rooms) < num_rooms: + return { + 'available': False, + 'available_count': len(available_rooms), + 'required_count': num_rooms, + 'message': f'Only {len(available_rooms)} rooms available for the selected dates' + } + + return { + 'available': True, + 'available_count': len(available_rooms), + 'required_count': num_rooms + } + + @staticmethod + def create_group_booking( + db: Session, + coordinator_id: int, + coordinator_name: str, + coordinator_email: str, + coordinator_phone: Optional[str], + check_in_date: datetime, + check_out_date: datetime, + room_blocks: List[Dict[str, Any]], + group_name: Optional[str] = None, + group_type: Optional[str] = None, + payment_option: PaymentOption = PaymentOption.coordinator_pays_all, + deposit_required: bool = False, + deposit_percentage: Optional[int] = None, + special_requests: Optional[str] = None, + notes: Optional[str] = None, + cancellation_policy: Optional[str] = None, + cancellation_deadline: Optional[datetime] = None, + cancellation_penalty_percentage: Optional[float] = None, + group_discount_percentage: Optional[float] = None + ) -> GroupBooking: + """Create a new group booking with room blocks""" + + # Validate dates + if check_out_date <= check_in_date: + raise ValueError("Check-out date must be after check-in date") + + # Calculate total rooms and base pricing + total_rooms = sum(block.get('num_rooms', 0) for block in room_blocks) + if total_rooms == 0: + raise ValueError("At least one room must be blocked") + + # Calculate pricing for each room block + total_original_price = Decimal('0') + room_block_objects = [] + + for block_data in room_blocks: + room_type_id = block_data.get('room_type_id') + num_rooms = block_data.get('num_rooms', 0) + rate_per_room = Decimal(str(block_data.get('rate_per_room', 0))) + + if not room_type_id or num_rooms <= 0: + continue + + # Check availability + availability = GroupBookingService.check_room_availability( + db, room_type_id, check_in_date, check_out_date, num_rooms + ) + + if not availability['available']: + raise ValueError(availability.get('message', 'Rooms not available')) + + # Get room type + room_type = db.query(RoomType).filter(RoomType.id == room_type_id).first() + if not room_type: + raise ValueError(f"Room type {room_type_id} not found") + + block_total = rate_per_room * num_rooms + total_original_price += block_total + + # Create room block object (will be saved later) + room_block = GroupRoomBlock( + room_type_id=room_type_id, + rooms_blocked=num_rooms, + rooms_confirmed=0, + rooms_available=num_rooms, + rate_per_room=rate_per_room, + total_block_price=block_total, + is_active=True + ) + room_block_objects.append(room_block) + + # Calculate group discount + base_rate = total_original_price / total_rooms if total_rooms > 0 else Decimal('0') + pricing = GroupBookingService.calculate_group_discount( + total_rooms, base_rate, group_discount_percentage + ) + + # Calculate deposit + deposit_amount = None + if deposit_required: + if deposit_percentage: + deposit_amount = Decimal(str(pricing['total_price'])) * Decimal(str(deposit_percentage)) / Decimal('100') + else: + deposit_amount = Decimal(str(pricing['total_price'])) * Decimal('0.2') # Default 20% + + # Create group booking + group_booking_number = GroupBookingService.generate_group_booking_number(db) + + group_booking = GroupBooking( + group_booking_number=group_booking_number, + coordinator_id=coordinator_id, + coordinator_name=coordinator_name, + coordinator_email=coordinator_email, + coordinator_phone=coordinator_phone, + group_name=group_name, + group_type=group_type, + total_rooms=total_rooms, + total_guests=0, # Will be updated when members are added + check_in_date=check_in_date, + check_out_date=check_out_date, + base_rate_per_room=base_rate, + group_discount_percentage=Decimal(str(pricing['discount_percentage'])), + group_discount_amount=Decimal(str(pricing['discount_amount'])), + original_total_price=Decimal(str(pricing['original_total'])), + discount_amount=Decimal(str(pricing['discount_amount'])), + total_price=Decimal(str(pricing['total_price'])), + payment_option=payment_option, + deposit_required=deposit_required, + deposit_percentage=deposit_percentage, + deposit_amount=deposit_amount, + amount_paid=Decimal('0'), + balance_due=Decimal(str(pricing['total_price'])), + status=GroupBookingStatus.draft, + cancellation_policy=cancellation_policy, + cancellation_deadline=cancellation_deadline, + cancellation_penalty_percentage=Decimal(str(cancellation_penalty_percentage)) if cancellation_penalty_percentage else None, + special_requests=special_requests, + notes=notes + ) + + db.add(group_booking) + db.flush() + + # Add room blocks + for room_block in room_block_objects: + room_block.group_booking_id = group_booking.id + db.add(room_block) + + db.commit() + db.refresh(group_booking) + + return group_booking + + @staticmethod + def add_member_to_group( + db: Session, + group_booking_id: int, + full_name: str, + email: Optional[str] = None, + phone: Optional[str] = None, + user_id: Optional[int] = None, + room_block_id: Optional[int] = None, + special_requests: Optional[str] = None, + preferences: Optional[Dict[str, Any]] = None + ) -> GroupBookingMember: + """Add a member to a group booking""" + + group_booking = db.query(GroupBooking).filter( + GroupBooking.id == group_booking_id + ).first() + + if not group_booking: + raise ValueError("Group booking not found") + + # Calculate individual amount if individual payment + individual_amount = None + if group_booking.payment_option == PaymentOption.individual_payments: + # Distribute cost evenly among members + current_member_count = db.query(GroupBookingMember).filter( + GroupBookingMember.group_booking_id == group_booking_id + ).count() + individual_amount = group_booking.total_price / (current_member_count + 1) + + member = GroupBookingMember( + group_booking_id=group_booking_id, + full_name=full_name, + email=email, + phone=phone, + user_id=user_id, + room_block_id=room_block_id, + special_requests=special_requests, + preferences=preferences, + individual_amount=individual_amount, + individual_paid=Decimal('0'), + individual_balance=individual_amount if individual_amount else Decimal('0') + ) + + db.add(member) + + # Update total guests + group_booking.total_guests += 1 + + db.commit() + db.refresh(member) + + return member + + @staticmethod + def confirm_group_booking( + db: Session, + group_booking_id: int + ) -> GroupBooking: + """Confirm a group booking and activate room blocks""" + + group_booking = db.query(GroupBooking).filter( + GroupBooking.id == group_booking_id + ).first() + + if not group_booking: + raise ValueError("Group booking not found") + + if group_booking.status not in [GroupBookingStatus.draft, GroupBookingStatus.pending]: + raise ValueError(f"Cannot confirm booking with status {group_booking.status}") + + # Re-check availability + room_blocks = db.query(GroupRoomBlock).filter( + GroupRoomBlock.group_booking_id == group_booking_id + ).all() + + for room_block in room_blocks: + availability = GroupBookingService.check_room_availability( + db, + room_block.room_type_id, + group_booking.check_in_date, + group_booking.check_out_date, + room_block.rooms_blocked + ) + + if not availability['available']: + raise ValueError(f"Rooms no longer available: {availability.get('message')}") + + # Update status + group_booking.status = GroupBookingStatus.confirmed + group_booking.confirmed_at = datetime.utcnow() + + db.commit() + db.refresh(group_booking) + + return group_booking + + @staticmethod + def create_individual_booking_from_member( + db: Session, + member_id: int, + room_id: int + ) -> Booking: + """Create an individual booking for a group member""" + + member = db.query(GroupBookingMember).filter( + GroupBookingMember.id == member_id + ).first() + + if not member: + raise ValueError("Group member not found") + + group_booking = member.group_booking + + if not group_booking: + raise ValueError("Group booking not found") + + # Check if room is available + room = db.query(Room).filter(Room.id == room_id).first() + if not room: + raise ValueError("Room not found") + + # Verify room type matches + if member.room_block_id: + room_block = db.query(GroupRoomBlock).filter( + GroupRoomBlock.id == member.room_block_id + ).first() + if room_block and room.room_type_id != room_block.room_type_id: + raise ValueError("Room type does not match the assigned room block") + + # Calculate price for this booking + nights = (group_booking.check_out_date - group_booking.check_in_date).days + if nights <= 0: + nights = 1 + + if member.individual_amount: + booking_price = member.individual_amount + else: + # Use proportional share + booking_price = group_booking.total_price / group_booking.total_rooms + + # Generate booking number + import random + prefix = 'BK' + ts = int(datetime.utcnow().timestamp() * 1000) + rand = random.randint(1000, 9999) + booking_number = f'{prefix}-{ts}-{rand}' + + # Ensure uniqueness + existing = db.query(Booking).filter(Booking.booking_number == booking_number).first() + if existing: + booking_number = f'{prefix}-{ts}-{rand + 1}' + + # Create booking + booking = Booking( + booking_number=booking_number, + user_id=member.user_id if member.user_id else group_booking.coordinator_id, + room_id=room_id, + check_in_date=group_booking.check_in_date, + check_out_date=group_booking.check_out_date, + num_guests=1, + total_price=booking_price, + original_price=booking_price, + discount_amount=Decimal('0'), + status=BookingStatus.confirmed, + deposit_paid=False, + requires_deposit=False, + special_requests=member.special_requests, + group_booking_id=group_booking_id + ) + + db.add(booking) + + # Update member + member.assigned_room_id = room_id + member.individual_booking_id = booking.id + + # Update room block + if member.room_block_id: + room_block = db.query(GroupRoomBlock).filter( + GroupRoomBlock.id == member.room_block_id + ).first() + if room_block: + room_block.rooms_confirmed += 1 + room_block.rooms_available -= 1 + + # Update group booking status + confirmed_count = db.query(GroupBookingMember).filter( + GroupBookingMember.group_booking_id == group_booking_id, + GroupBookingMember.individual_booking_id.isnot(None) + ).count() + + if confirmed_count == group_booking.total_rooms: + group_booking.status = GroupBookingStatus.confirmed + elif confirmed_count > 0: + group_booking.status = GroupBookingStatus.partially_confirmed + + db.commit() + db.refresh(booking) + + return booking + + @staticmethod + def add_group_payment( + db: Session, + group_booking_id: int, + amount: Decimal, + payment_method: str, + payment_type: str = 'deposit', + transaction_id: Optional[str] = None, + paid_by_member_id: Optional[int] = None, + paid_by_user_id: Optional[int] = None, + notes: Optional[str] = None + ) -> GroupPayment: + """Add a payment to a group booking""" + + group_booking = db.query(GroupBooking).filter( + GroupBooking.id == group_booking_id + ).first() + + if not group_booking: + raise ValueError("Group booking not found") + + payment = GroupPayment( + group_booking_id=group_booking_id, + amount=amount, + payment_method=payment_method, + payment_type=payment_type, + payment_status='completed', + transaction_id=transaction_id, + payment_date=datetime.utcnow(), + paid_by_member_id=paid_by_member_id, + paid_by_user_id=paid_by_user_id, + notes=notes + ) + + db.add(payment) + + # Update group booking payment totals + group_booking.amount_paid += amount + group_booking.balance_due = group_booking.total_price - group_booking.amount_paid + + # Update member payment if individual payment + if paid_by_member_id: + member = db.query(GroupBookingMember).filter( + GroupBookingMember.id == paid_by_member_id + ).first() + if member: + member.individual_paid += amount + member.individual_balance = (member.individual_amount or Decimal('0')) - member.individual_paid + + db.commit() + db.refresh(payment) + + return payment + + @staticmethod + def cancel_group_booking( + db: Session, + group_booking_id: int, + cancellation_reason: Optional[str] = None + ) -> GroupBooking: + """Cancel a group booking""" + + group_booking = db.query(GroupBooking).filter( + GroupBooking.id == group_booking_id + ).first() + + if not group_booking: + raise ValueError("Group booking not found") + + if group_booking.status in [GroupBookingStatus.checked_out, GroupBookingStatus.cancelled]: + raise ValueError(f"Cannot cancel booking with status {group_booking.status}") + + # Calculate cancellation penalty + penalty_amount = Decimal('0') + if group_booking.cancellation_penalty_percentage: + penalty_amount = group_booking.total_price * ( + Decimal(str(group_booking.cancellation_penalty_percentage)) / Decimal('100') + ) + + # Update status + group_booking.status = GroupBookingStatus.cancelled + group_booking.cancelled_at = datetime.utcnow() + + # Release room blocks + room_blocks = db.query(GroupRoomBlock).filter( + GroupRoomBlock.group_booking_id == group_booking_id + ).all() + + for room_block in room_blocks: + room_block.is_active = False + room_block.block_released_at = datetime.utcnow() + + # Cancel individual bookings if any + individual_bookings = db.query(Booking).filter( + Booking.group_booking_id == group_booking_id + ).all() + + for booking in individual_bookings: + if booking.status not in [BookingStatus.checked_out, BookingStatus.cancelled]: + booking.status = BookingStatus.cancelled + + db.commit() + db.refresh(group_booking) + + return group_booking + diff --git a/Backend/src/services/guest_profile_service.py b/Backend/src/services/guest_profile_service.py index 01679be3..5ba067f4 100644 --- a/Backend/src/services/guest_profile_service.py +++ b/Backend/src/services/guest_profile_service.py @@ -1,4 +1,4 @@ -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, load_only from sqlalchemy import func, and_, or_, desc from typing import List, Dict, Optional from datetime import datetime, timedelta @@ -50,7 +50,17 @@ class GuestProfileService: @staticmethod def get_booking_history(user_id: int, db: Session, limit: Optional[int] = None) -> List[Booking]: """Get complete booking history for a guest""" - query = db.query(Booking).filter(Booking.user_id == user_id).order_by(desc(Booking.created_at)) + # Use load_only to exclude non-existent columns (rate_plan_id, group_booking_id) + query = db.query(Booking).options( + load_only( + Booking.id, Booking.booking_number, Booking.user_id, Booking.room_id, + Booking.check_in_date, Booking.check_out_date, Booking.num_guests, + Booking.total_price, Booking.original_price, Booking.discount_amount, + Booking.promotion_code, Booking.status, Booking.deposit_paid, + Booking.requires_deposit, Booking.special_requests, + Booking.created_at, Booking.updated_at + ) + ).filter(Booking.user_id == user_id).order_by(desc(Booking.created_at)) if limit: query = query.limit(limit) return query.all() @@ -58,18 +68,30 @@ class GuestProfileService: @staticmethod def get_booking_statistics(user_id: int, db: Session) -> Dict: """Get booking statistics for a guest""" - total_bookings = db.query(Booking).filter(Booking.user_id == user_id).count() - completed_bookings = db.query(Booking).filter( + # Use func.count with load_only to exclude non-existent columns (rate_plan_id, group_booking_id) + # This avoids SQLAlchemy generating subqueries with all columns + total_bookings = db.query(func.count(Booking.id)).filter(Booking.user_id == user_id).scalar() or 0 + completed_bookings = db.query(func.count(Booking.id)).filter( Booking.user_id == user_id, Booking.status == BookingStatus.checked_out - ).count() - cancelled_bookings = db.query(Booking).filter( + ).scalar() or 0 + cancelled_bookings = db.query(func.count(Booking.id)).filter( Booking.user_id == user_id, Booking.status == BookingStatus.cancelled - ).count() + ).scalar() or 0 # Get last visit date - last_booking = db.query(Booking).filter( + # Use load_only to exclude non-existent columns (rate_plan_id, group_booking_id) + last_booking = db.query(Booking).options( + load_only( + Booking.id, Booking.booking_number, Booking.user_id, Booking.room_id, + Booking.check_in_date, Booking.check_out_date, Booking.num_guests, + Booking.total_price, Booking.original_price, Booking.discount_amount, + Booking.promotion_code, Booking.status, Booking.deposit_paid, + Booking.requires_deposit, Booking.special_requests, + Booking.created_at, Booking.updated_at + ) + ).filter( Booking.user_id == user_id, Booking.status == BookingStatus.checked_out ).order_by(desc(Booking.check_in_date)).first() @@ -77,6 +99,7 @@ class GuestProfileService: last_visit_date = last_booking.check_in_date if last_booking else None # Get total nights stayed + # Aggregate queries don't need load_only as they don't load full objects total_nights = db.query( func.sum(func.extract('day', Booking.check_out_date - Booking.check_in_date)) ).filter( @@ -177,7 +200,17 @@ class GuestProfileService: satisfaction_score = GuestProfileService.calculate_satisfaction_score(user_id, db) # Get preferred room types - bookings = db.query(Booking).filter(Booking.user_id == user_id).all() + # Use load_only to exclude non-existent columns (rate_plan_id, group_booking_id) + bookings = db.query(Booking).options( + load_only( + Booking.id, Booking.booking_number, Booking.user_id, Booking.room_id, + Booking.check_in_date, Booking.check_out_date, Booking.num_guests, + Booking.total_price, Booking.original_price, Booking.discount_amount, + Booking.promotion_code, Booking.status, Booking.deposit_paid, + Booking.requires_deposit, Booking.special_requests, + Booking.created_at, Booking.updated_at + ) + ).filter(Booking.user_id == user_id).all() room_type_counts = {} for booking in bookings: if booking.room and booking.room.room_type: diff --git a/Backend/src/services/notification_service.py b/Backend/src/services/notification_service.py new file mode 100644 index 00000000..7cc9d665 --- /dev/null +++ b/Backend/src/services/notification_service.py @@ -0,0 +1,373 @@ +from sqlalchemy.orm import Session +from sqlalchemy import and_, or_, func, desc +from typing import Optional, Dict, List, Any +from datetime import datetime, timedelta +from ..models.notification import ( + Notification, NotificationTemplate, NotificationPreference, NotificationDeliveryLog, + NotificationChannel, NotificationStatus, NotificationType +) +from ..models.user import User +from ..models.booking import Booking +from ..models.payment import Payment +import logging +import re + +logger = logging.getLogger(__name__) + +class NotificationService: + """Multi-channel Notification Service""" + + @staticmethod + def get_user_preferences(db: Session, user_id: int) -> NotificationPreference: + """Get or create user notification preferences""" + preferences = db.query(NotificationPreference).filter( + NotificationPreference.user_id == user_id + ).first() + + if not preferences: + preferences = NotificationPreference(user_id=user_id) + db.add(preferences) + db.commit() + db.refresh(preferences) + + return preferences + + @staticmethod + def update_user_preferences( + db: Session, + user_id: int, + preferences_data: Dict[str, Any] + ) -> NotificationPreference: + """Update user notification preferences""" + preferences = NotificationService.get_user_preferences(db, user_id) + + for key, value in preferences_data.items(): + if hasattr(preferences, key): + setattr(preferences, key, value) + + preferences.updated_at = datetime.utcnow() + db.commit() + db.refresh(preferences) + return preferences + + @staticmethod + def should_send_notification( + db: Session, + user_id: int, + notification_type: NotificationType, + channel: NotificationChannel + ) -> bool: + """Check if notification should be sent based on user preferences""" + preferences = NotificationService.get_user_preferences(db, user_id) + + # Check global channel preference + channel_attr = f'{channel.value}_enabled' + if not getattr(preferences, channel_attr, False): + return False + + # Check type-specific preference + type_attr = f'{notification_type.value}_{channel.value}' + return getattr(preferences, type_attr, True) + + @staticmethod + def create_template( + db: Session, + name: str, + notification_type: NotificationType, + channel: NotificationChannel, + content: str, + created_by: int, + subject: Optional[str] = None, + variables: Optional[List[str]] = None + ) -> NotificationTemplate: + """Create notification template""" + template = NotificationTemplate( + name=name, + notification_type=notification_type, + channel=channel, + subject=subject, + content=content, + variables=variables or [], + created_by=created_by, + is_active=True + ) + db.add(template) + db.commit() + db.refresh(template) + return template + + @staticmethod + def get_template( + db: Session, + notification_type: NotificationType, + channel: NotificationChannel + ) -> Optional[NotificationTemplate]: + """Get active template for notification type and channel""" + return db.query(NotificationTemplate).filter( + and_( + NotificationTemplate.notification_type == notification_type, + NotificationTemplate.channel == channel, + NotificationTemplate.is_active == True + ) + ).first() + + @staticmethod + def render_template(template: NotificationTemplate, variables: Dict[str, Any]) -> Dict[str, str]: + """Render template with variables""" + content = template.content + subject = template.subject or '' + + # Replace variables in format {{variable_name}} + for key, value in variables.items(): + placeholder = f'{{{{{key}}}}}' + content = content.replace(placeholder, str(value)) + subject = subject.replace(placeholder, str(value)) + + return {'subject': subject, 'content': content} + + @staticmethod + def send_notification( + db: Session, + user_id: Optional[int], + notification_type: NotificationType, + channel: NotificationChannel, + content: str, + subject: Optional[str] = None, + template_id: Optional[int] = None, + priority: str = 'normal', + scheduled_at: Optional[datetime] = None, + booking_id: Optional[int] = None, + payment_id: Optional[int] = None, + meta_data: Optional[Dict[str, Any]] = None + ) -> Notification: + """Create and send notification""" + # Check user preferences if user_id is provided + if user_id and not NotificationService.should_send_notification(db, user_id, notification_type, channel): + logger.info(f"Notification skipped due to user preferences: user_id={user_id}, type={notification_type}, channel={channel}") + # Still create notification but mark as skipped + notification = Notification( + user_id=user_id, + notification_type=notification_type, + channel=channel, + subject=subject, + content=content, + template_id=template_id, + status=NotificationStatus.failed, + priority=priority, + scheduled_at=scheduled_at, + booking_id=booking_id, + payment_id=payment_id, + meta_data={**(meta_data or {}), 'skipped': True, 'reason': 'user_preference'} + ) + db.add(notification) + db.commit() + return notification + + notification = Notification( + user_id=user_id, + notification_type=notification_type, + channel=channel, + subject=subject, + content=content, + template_id=template_id, + status=NotificationStatus.pending, + priority=priority, + scheduled_at=scheduled_at, + booking_id=booking_id, + payment_id=payment_id, + meta_data=meta_data or {} + ) + db.add(notification) + db.commit() + db.refresh(notification) + + # Send notification if not scheduled + if not scheduled_at or scheduled_at <= datetime.utcnow(): + NotificationService._deliver_notification(db, notification) + + return notification + + @staticmethod + def _deliver_notification(db: Session, notification: Notification): + """Deliver notification through appropriate channel""" + try: + if notification.channel == NotificationChannel.email: + NotificationService._send_email(db, notification) + elif notification.channel == NotificationChannel.sms: + NotificationService._send_sms(db, notification) + elif notification.channel == NotificationChannel.push: + NotificationService._send_push(db, notification) + elif notification.channel == NotificationChannel.whatsapp: + NotificationService._send_whatsapp(db, notification) + elif notification.channel == NotificationChannel.in_app: + NotificationService._send_in_app(db, notification) + except Exception as e: + logger.error(f"Failed to deliver notification {notification.id}: {str(e)}") + notification.status = NotificationStatus.failed + notification.error_message = str(e) + db.commit() + + @staticmethod + def _send_email(db: Session, notification: Notification): + """Send email notification""" + # TODO: Integrate with email service (SendGrid, AWS SES, etc.) + # For now, just mark as sent + notification.status = NotificationStatus.sent + notification.sent_at = datetime.utcnow() + db.commit() + logger.info(f"Email notification {notification.id} sent (mock)") + + @staticmethod + def _send_sms(db: Session, notification: Notification): + """Send SMS notification""" + # TODO: Integrate with SMS service (Twilio, AWS SNS, etc.) + # For now, just mark as sent + notification.status = NotificationStatus.sent + notification.sent_at = datetime.utcnow() + db.commit() + logger.info(f"SMS notification {notification.id} sent (mock)") + + @staticmethod + def _send_push(db: Session, notification: Notification): + """Send push notification""" + # TODO: Integrate with push notification service (FCM, APNS, etc.) + # For now, just mark as sent + notification.status = NotificationStatus.sent + notification.sent_at = datetime.utcnow() + db.commit() + logger.info(f"Push notification {notification.id} sent (mock)") + + @staticmethod + def _send_whatsapp(db: Session, notification: Notification): + """Send WhatsApp notification""" + # TODO: Integrate with WhatsApp Business API + # For now, just mark as sent + notification.status = NotificationStatus.sent + notification.sent_at = datetime.utcnow() + db.commit() + logger.info(f"WhatsApp notification {notification.id} sent (mock)") + + @staticmethod + def _send_in_app(db: Session, notification: Notification): + """Send in-app notification""" + # In-app notifications are always "delivered" immediately + notification.status = NotificationStatus.delivered + notification.sent_at = datetime.utcnow() + notification.delivered_at = datetime.utcnow() + db.commit() + logger.info(f"In-app notification {notification.id} delivered") + + @staticmethod + def send_booking_confirmation(db: Session, booking: Booking): + """Send booking confirmation notifications""" + user = booking.user + if not user: + return + + # Get template or use default + template = NotificationService.get_template(db, NotificationType.booking_confirmation, NotificationChannel.email) + + if template: + variables = { + 'booking_number': booking.booking_number, + 'guest_name': user.full_name, + 'check_in': booking.check_in_date.strftime('%Y-%m-%d') if booking.check_in_date else '', + 'check_out': booking.check_out_date.strftime('%Y-%m-%d') if booking.check_out_date else '', + 'total_price': str(booking.total_price), + } + rendered = NotificationService.render_template(template, variables) + NotificationService.send_notification( + db=db, + user_id=user.id, + notification_type=NotificationType.booking_confirmation, + channel=NotificationChannel.email, + content=rendered['content'], + subject=rendered['subject'], + template_id=template.id, + booking_id=booking.id + ) + else: + # Fallback to default message + NotificationService.send_notification( + db=db, + user_id=user.id, + notification_type=NotificationType.booking_confirmation, + channel=NotificationChannel.email, + subject=f'Booking Confirmation - {booking.booking_number}', + content=f'Your booking {booking.booking_number} has been confirmed.', + booking_id=booking.id + ) + + @staticmethod + def send_payment_receipt(db: Session, payment: Payment): + """Send payment receipt notifications""" + booking = payment.booking + if not booking or not booking.user: + return + + user = booking.user + template = NotificationService.get_template(db, NotificationType.payment_receipt, NotificationChannel.email) + + if template: + variables = { + 'payment_amount': str(payment.amount), + 'payment_method': payment.payment_method.value if hasattr(payment.payment_method, 'value') else str(payment.payment_method), + 'transaction_id': payment.transaction_id or '', + 'booking_number': booking.booking_number, + 'guest_name': user.full_name, + } + rendered = NotificationService.render_template(template, variables) + NotificationService.send_notification( + db=db, + user_id=user.id, + notification_type=NotificationType.payment_receipt, + channel=NotificationChannel.email, + content=rendered['content'], + subject=rendered['subject'], + template_id=template.id, + payment_id=payment.id, + booking_id=booking.id + ) + + @staticmethod + def get_notifications( + db: Session, + user_id: Optional[int] = None, + notification_type: Optional[NotificationType] = None, + channel: Optional[NotificationChannel] = None, + status: Optional[NotificationStatus] = None, + skip: int = 0, + limit: int = 100 + ) -> List[Notification]: + """Get notifications with filters""" + query = db.query(Notification) + + if user_id: + query = query.filter(Notification.user_id == user_id) + if notification_type: + query = query.filter(Notification.notification_type == notification_type) + if channel: + query = query.filter(Notification.channel == channel) + if status: + query = query.filter(Notification.status == status) + + return query.order_by(desc(Notification.created_at)).offset(skip).limit(limit).all() + + @staticmethod + def mark_as_read(db: Session, notification_id: int, user_id: int) -> Optional[Notification]: + """Mark notification as read""" + notification = db.query(Notification).filter( + and_( + Notification.id == notification_id, + Notification.user_id == user_id + ) + ).first() + + if notification: + notification.status = NotificationStatus.read + notification.read_at = datetime.utcnow() + db.commit() + db.refresh(notification) + + return notification + diff --git a/Backend/src/services/oauth_service.py b/Backend/src/services/oauth_service.py new file mode 100644 index 00000000..71a09fa1 --- /dev/null +++ b/Backend/src/services/oauth_service.py @@ -0,0 +1,209 @@ +from typing import Optional, Dict, Any +from sqlalchemy.orm import Session +from datetime import datetime, timedelta +import httpx +import secrets +from urllib.parse import urlencode +import logging + +from ..models.security_event import OAuthProvider, OAuthToken +from ..models.user import User +from ..config.logging_config import get_logger + +logger = get_logger(__name__) + +class OAuthService: + """Service for handling OAuth 2.0 / OpenID Connect authentication""" + + @staticmethod + def get_authorization_url(db: Session, provider_name: str, redirect_uri: str, state: Optional[str] = None) -> str: + """Generate OAuth authorization URL""" + provider = db.query(OAuthProvider).filter( + OAuthProvider.name == provider_name, + OAuthProvider.is_active == True + ).first() + + if not provider: + raise ValueError(f"OAuth provider '{provider_name}' not found or inactive") + + if not state: + state = secrets.token_urlsafe(32) + + params = { + 'client_id': provider.client_id, + 'redirect_uri': redirect_uri, + 'response_type': 'code', + 'scope': provider.scopes or 'openid profile email', + 'state': state, + } + + return f"{provider.authorization_url}?{urlencode(params)}" + + @staticmethod + async def exchange_code_for_token( + db: Session, + provider_name: str, + code: str, + redirect_uri: str + ) -> Dict[str, Any]: + """Exchange authorization code for access token""" + provider = db.query(OAuthProvider).filter( + OAuthProvider.name == provider_name, + OAuthProvider.is_active == True + ).first() + + if not provider: + raise ValueError(f"OAuth provider '{provider_name}' not found or inactive") + + async with httpx.AsyncClient() as client: + response = await client.post( + provider.token_url, + data={ + 'grant_type': 'authorization_code', + 'code': code, + 'redirect_uri': redirect_uri, + 'client_id': provider.client_id, + 'client_secret': provider.client_secret, + }, + headers={'Accept': 'application/json'} + ) + + if response.status_code != 200: + logger.error(f"OAuth token exchange failed: {response.text}") + raise ValueError("Failed to exchange authorization code for token") + + token_data = response.json() + return token_data + + @staticmethod + async def get_user_info( + db: Session, + provider_name: str, + access_token: str + ) -> Dict[str, Any]: + """Get user information from OAuth provider""" + provider = db.query(OAuthProvider).filter( + OAuthProvider.name == provider_name, + OAuthProvider.is_active == True + ).first() + + if not provider: + raise ValueError(f"OAuth provider '{provider_name}' not found or inactive") + + async with httpx.AsyncClient() as client: + response = await client.get( + provider.userinfo_url, + headers={ + 'Authorization': f'Bearer {access_token}', + 'Accept': 'application/json' + } + ) + + if response.status_code != 200: + logger.error(f"Failed to get user info: {response.text}") + raise ValueError("Failed to get user information from OAuth provider") + + return response.json() + + @staticmethod + def save_oauth_token( + db: Session, + user_id: int, + provider_id: int, + provider_user_id: str, + access_token: str, + refresh_token: Optional[str] = None, + expires_in: Optional[int] = None, + scopes: Optional[str] = None + ) -> OAuthToken: + """Save or update OAuth token for user""" + expires_at = None + if expires_in: + expires_at = datetime.utcnow() + timedelta(seconds=expires_in) + + # Check if token already exists + existing_token = db.query(OAuthToken).filter( + OAuthToken.user_id == user_id, + OAuthToken.provider_id == provider_id + ).first() + + if existing_token: + existing_token.access_token = access_token + existing_token.refresh_token = refresh_token + existing_token.expires_at = expires_at + existing_token.scopes = scopes + existing_token.updated_at = datetime.utcnow() + db.commit() + db.refresh(existing_token) + return existing_token + else: + new_token = OAuthToken( + user_id=user_id, + provider_id=provider_id, + provider_user_id=provider_user_id, + access_token=access_token, + refresh_token=refresh_token, + expires_at=expires_at, + scopes=scopes + ) + db.add(new_token) + db.commit() + db.refresh(new_token) + return new_token + + @staticmethod + def find_or_create_user_from_oauth( + db: Session, + provider_name: str, + user_info: Dict[str, Any] + ) -> User: + """Find existing user or create new user from OAuth user info""" + provider = db.query(OAuthProvider).filter( + OAuthProvider.name == provider_name + ).first() + + if not provider: + raise ValueError(f"OAuth provider '{provider_name}' not found") + + # Try to find user by OAuth token + provider_user_id = user_info.get('sub') or user_info.get('id') + oauth_token = db.query(OAuthToken).filter( + OAuthToken.provider_id == provider.id, + OAuthToken.provider_user_id == str(provider_user_id) + ).first() + + if oauth_token: + return oauth_token.user + + # Try to find user by email + email = user_info.get('email') + if email: + user = db.query(User).filter(User.email == email.lower()).first() + if user: + return user + + # Create new user + from ..models.role import Role + customer_role = db.query(Role).filter(Role.name == 'customer').first() + if not customer_role: + raise ValueError("Customer role not found") + + name = user_info.get('name') or user_info.get('given_name', '') + ' ' + user_info.get('family_name', '') + if not name.strip(): + name = email.split('@')[0] if email else 'User' + + new_user = User( + email=email.lower() if email else f"{provider_user_id}@{provider_name}.oauth", + full_name=name.strip(), + role_id=customer_role.id, + is_active=True, + email_verified=True # OAuth providers verify emails + ) + db.add(new_user) + db.commit() + db.refresh(new_user) + + return new_user + +oauth_service = OAuthService() + diff --git a/Backend/src/services/room_assignment_service.py b/Backend/src/services/room_assignment_service.py new file mode 100644 index 00000000..5ab650e8 --- /dev/null +++ b/Backend/src/services/room_assignment_service.py @@ -0,0 +1,241 @@ +""" +Room Assignment Optimization Service +Provides intelligent room assignment based on guest preferences, room attributes, and availability +""" +from sqlalchemy.orm import Session +from sqlalchemy import and_, or_, func +from datetime import datetime +from typing import List, Optional, Dict +from ..models.room import Room, RoomStatus +from ..models.booking import Booking, BookingStatus +from ..models.room_attribute import RoomAttribute +from ..models.user import User + + +class RoomAssignmentService: + """Service for optimizing room assignments""" + + @staticmethod + def find_best_room( + db: Session, + room_type_id: int, + check_in: datetime, + check_out: datetime, + num_guests: int, + guest_preferences: Optional[Dict] = None, + exclude_room_ids: Optional[List[int]] = None + ) -> Optional[Room]: + """ + Find the best available room for a booking based on multiple criteria + + Args: + db: Database session + room_type_id: Required room type ID + check_in: Check-in date + check_out: Check-out date + num_guests: Number of guests + guest_preferences: Optional dict with preferences like {'view': 'ocean', 'floor': 'high', 'quiet': True} + exclude_room_ids: List of room IDs to exclude from consideration + + Returns: + Best matching Room or None if no room available + """ + # Base query: available rooms of the correct type + query = db.query(Room).filter( + Room.room_type_id == room_type_id, + Room.status == RoomStatus.available + ) + + # Exclude specific rooms if provided + if exclude_room_ids: + query = query.filter(~Room.id.in_(exclude_room_ids)) + + # Exclude rooms with overlapping bookings + overlapping_rooms = db.query(Booking.room_id).filter( + and_( + Booking.status != BookingStatus.cancelled, + Booking.check_in_date < check_out, + Booking.check_out_date > check_in + ) + ).subquery() + query = query.filter(~Room.id.in_(db.query(overlapping_rooms.c.room_id))) + + # Exclude rooms blocked by maintenance + from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus + blocked_rooms = db.query(RoomMaintenance.room_id).filter( + and_( + RoomMaintenance.blocks_room == True, + RoomMaintenance.status.in_([MaintenanceStatus.scheduled, MaintenanceStatus.in_progress]), + or_( + and_( + RoomMaintenance.block_start.isnot(None), + RoomMaintenance.block_end.isnot(None), + RoomMaintenance.block_start < check_out, + RoomMaintenance.block_end > check_in + ), + and_( + RoomMaintenance.scheduled_start < check_out, + RoomMaintenance.scheduled_end.isnot(None), + RoomMaintenance.scheduled_end > check_in + ) + ) + ) + ).subquery() + query = query.filter(~Room.id.in_(db.query(blocked_rooms.c.room_id))) + + available_rooms = query.all() + + if not available_rooms: + return None + + # Score rooms based on preferences + if guest_preferences: + scored_rooms = [] + for room in available_rooms: + score = RoomAssignmentService._calculate_room_score(room, guest_preferences, db) + scored_rooms.append((score, room)) + + # Sort by score (highest first) and return best match + scored_rooms.sort(key=lambda x: x[0], reverse=True) + return scored_rooms[0][1] if scored_rooms else None + + # If no preferences, return first available room + return available_rooms[0] if available_rooms else None + + @staticmethod + def _calculate_room_score(room: Room, preferences: Dict, db: Session) -> float: + """ + Calculate a score for a room based on guest preferences + Higher score = better match + """ + score = 0.0 + + # View preference + if 'view' in preferences and room.view: + if preferences['view'].lower() in room.view.lower(): + score += 10.0 + + # Floor preference + if 'floor' in preferences: + preferred_floor = preferences['floor'] + if preferred_floor == 'high' and room.floor >= 3: + score += 5.0 + elif preferred_floor == 'low' and room.floor <= 2: + score += 5.0 + elif isinstance(preferred_floor, int) and room.floor == preferred_floor: + score += 10.0 + + # Quiet preference + if preferences.get('quiet'): + # Check room attributes for noise level + quiet_attr = db.query(RoomAttribute).filter( + and_( + RoomAttribute.room_id == room.id, + RoomAttribute.attribute_name == 'noise_level', + RoomAttribute.is_active == True + ) + ).first() + if quiet_attr and 'quiet' in quiet_attr.attribute_value.lower(): + score += 8.0 + + # Accessibility preference + if preferences.get('accessible'): + accessible_attr = db.query(RoomAttribute).filter( + and_( + RoomAttribute.room_id == room.id, + RoomAttribute.attribute_name == 'accessibility', + RoomAttribute.is_active == True + ) + ).first() + if accessible_attr and 'accessible' in accessible_attr.attribute_value.lower(): + score += 15.0 # High priority for accessibility + + # Featured rooms get a small boost + if room.featured: + score += 2.0 + + # Rooms with higher ratings get a boost + from ..models.review import Review, ReviewStatus + avg_rating = db.query(func.avg(Review.rating)).filter( + and_( + Review.room_id == room.id, + Review.status == ReviewStatus.approved + ) + ).scalar() + if avg_rating: + score += float(avg_rating) * 2.0 + + return score + + @staticmethod + def get_room_availability_calendar( + db: Session, + room_id: int, + start_date: datetime, + end_date: datetime + ) -> Dict: + """ + Get detailed availability information for a room over a date range + """ + room = db.query(Room).filter(Room.id == room_id).first() + if not room: + return None + + # Get bookings + bookings = db.query(Booking).filter( + and_( + Booking.room_id == room_id, + Booking.status != BookingStatus.cancelled, + Booking.check_in_date < end_date, + Booking.check_out_date > start_date + ) + ).all() + + # Get maintenance blocks + from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus + maintenance_blocks = db.query(RoomMaintenance).filter( + and_( + RoomMaintenance.room_id == room_id, + RoomMaintenance.blocks_room == True, + RoomMaintenance.status.in_([MaintenanceStatus.scheduled, MaintenanceStatus.in_progress]), + or_( + and_( + RoomMaintenance.block_start.isnot(None), + RoomMaintenance.block_end.isnot(None), + RoomMaintenance.block_start < end_date, + RoomMaintenance.block_end > start_date + ), + and_( + RoomMaintenance.scheduled_start < end_date, + RoomMaintenance.scheduled_end.isnot(None), + RoomMaintenance.scheduled_end > start_date + ) + ) + ) + ).all() + + return { + 'room_id': room_id, + 'room_number': room.room_number, + 'status': room.status.value, + 'bookings': [ + { + 'id': b.id, + 'check_in': b.check_in_date.isoformat(), + 'check_out': b.check_out_date.isoformat(), + 'status': b.status.value + } + for b in bookings + ], + 'maintenance_blocks': [ + { + 'id': m.id, + 'title': m.title, + 'start': (m.block_start or m.scheduled_start).isoformat(), + 'end': (m.block_end or m.scheduled_end).isoformat() if (m.block_end or m.scheduled_end) else None, + 'type': m.maintenance_type.value + } + for m in maintenance_blocks + ] + } + diff --git a/Backend/src/services/security_monitoring_service.py b/Backend/src/services/security_monitoring_service.py new file mode 100644 index 00000000..4b9a4c7a --- /dev/null +++ b/Backend/src/services/security_monitoring_service.py @@ -0,0 +1,189 @@ +from sqlalchemy.orm import Session +from typing import Optional, List, Dict, Any +from datetime import datetime, timedelta +from ..models.security_event import SecurityEvent, SecurityEventType, SecurityEventSeverity +from ..models.user import User +from ..config.logging_config import get_logger + +logger = get_logger(__name__) + +class SecurityMonitoringService: + """Service for monitoring and analyzing security events""" + + @staticmethod + def log_security_event( + db: Session, + event_type: SecurityEventType, + severity: SecurityEventSeverity, + user_id: Optional[int] = None, + ip_address: Optional[str] = None, + user_agent: Optional[str] = None, + request_path: Optional[str] = None, + request_method: Optional[str] = None, + request_id: Optional[str] = None, + description: Optional[str] = None, + details: Optional[Dict[str, Any]] = None + ) -> SecurityEvent: + """Log a security event""" + try: + event = SecurityEvent( + user_id=user_id, + event_type=event_type, + severity=severity, + ip_address=ip_address, + user_agent=user_agent, + request_path=request_path, + request_method=request_method, + request_id=request_id, + description=description, + details=details + ) + db.add(event) + db.commit() + db.refresh(event) + + # Check for suspicious patterns + SecurityMonitoringService._check_suspicious_patterns(db, event) + + return event + except Exception as e: + logger.error(f"Error logging security event: {str(e)}") + db.rollback() + raise + + @staticmethod + def _check_suspicious_patterns(db: Session, event: SecurityEvent): + """Check for suspicious activity patterns""" + # Multiple failed login attempts from same IP + if event.event_type == SecurityEventType.login_failure: + recent_failures = db.query(SecurityEvent).filter( + SecurityEvent.event_type == SecurityEventType.login_failure, + SecurityEvent.ip_address == event.ip_address, + SecurityEvent.created_at >= datetime.utcnow() - timedelta(minutes=15) + ).count() + + if recent_failures >= 5: + # Log suspicious activity + SecurityMonitoringService.log_security_event( + db, + SecurityEventType.suspicious_activity, + SecurityEventSeverity.high, + ip_address=event.ip_address, + description=f"Multiple failed login attempts ({recent_failures}) from IP {event.ip_address}", + details={"failure_count": recent_failures} + ) + + # Multiple permission denied from same user + if event.event_type == SecurityEventType.permission_denied and event.user_id: + recent_denials = db.query(SecurityEvent).filter( + SecurityEvent.event_type == SecurityEventType.permission_denied, + SecurityEvent.user_id == event.user_id, + SecurityEvent.created_at >= datetime.utcnow() - timedelta(hours=1) + ).count() + + if recent_denials >= 10: + SecurityMonitoringService.log_security_event( + db, + SecurityEventType.suspicious_activity, + SecurityEventSeverity.medium, + user_id=event.user_id, + description=f"User {event.user_id} has {recent_denials} permission denials in the last hour", + details={"denial_count": recent_denials} + ) + + @staticmethod + def get_security_events( + db: Session, + user_id: Optional[int] = None, + event_type: Optional[SecurityEventType] = None, + severity: Optional[SecurityEventSeverity] = None, + ip_address: Optional[str] = None, + resolved: Optional[bool] = None, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + limit: int = 100, + offset: int = 0 + ) -> List[SecurityEvent]: + """Get security events with filters""" + query = db.query(SecurityEvent) + + if user_id: + query = query.filter(SecurityEvent.user_id == user_id) + if event_type: + query = query.filter(SecurityEvent.event_type == event_type) + if severity: + query = query.filter(SecurityEvent.severity == severity) + if ip_address: + query = query.filter(SecurityEvent.ip_address == ip_address) + if resolved is not None: + query = query.filter(SecurityEvent.resolved == resolved) + if start_date: + query = query.filter(SecurityEvent.created_at >= start_date) + if end_date: + query = query.filter(SecurityEvent.created_at <= end_date) + + return query.order_by(SecurityEvent.created_at.desc()).offset(offset).limit(limit).all() + + @staticmethod + def get_security_stats( + db: Session, + days: int = 7 + ) -> Dict[str, Any]: + """Get security statistics for the last N days""" + start_date = datetime.utcnow() - timedelta(days=days) + + total_events = db.query(SecurityEvent).filter( + SecurityEvent.created_at >= start_date + ).count() + + by_type = {} + by_severity = {} + + events = db.query(SecurityEvent).filter( + SecurityEvent.created_at >= start_date + ).all() + + for event in events: + event_type = event.event_type.value + severity = event.severity.value + + by_type[event_type] = by_type.get(event_type, 0) + 1 + by_severity[severity] = by_severity.get(severity, 0) + 1 + + unresolved_critical = db.query(SecurityEvent).filter( + SecurityEvent.severity == SecurityEventSeverity.critical, + SecurityEvent.resolved == False, + SecurityEvent.created_at >= start_date + ).count() + + return { + "total_events": total_events, + "by_type": by_type, + "by_severity": by_severity, + "unresolved_critical": unresolved_critical, + "period_days": days + } + + @staticmethod + def resolve_event( + db: Session, + event_id: int, + resolved_by: int, + resolution_notes: Optional[str] = None + ) -> SecurityEvent: + """Mark a security event as resolved""" + event = db.query(SecurityEvent).filter(SecurityEvent.id == event_id).first() + if not event: + raise ValueError("Security event not found") + + event.resolved = True + event.resolved_at = datetime.utcnow() + event.resolved_by = resolved_by + event.resolution_notes = resolution_notes + + db.commit() + db.refresh(event) + return event + +security_monitoring_service = SecurityMonitoringService() + diff --git a/Backend/src/services/security_scan_service.py b/Backend/src/services/security_scan_service.py new file mode 100644 index 00000000..d2797fa9 --- /dev/null +++ b/Backend/src/services/security_scan_service.py @@ -0,0 +1,314 @@ +from sqlalchemy.orm import Session +from typing import List, Dict, Any, Optional +from datetime import datetime, timedelta +import logging +from ..models.security_event import SecurityEvent, SecurityEventType, SecurityEventSeverity +from ..models.user import User +from ..models.booking import Booking +from ..models.payment import Payment +from ..config.logging_config import get_logger + +logger = get_logger(__name__) + +class SecurityScanService: + """Service for automated security scanning""" + + @staticmethod + def run_full_scan(db: Session) -> Dict[str, Any]: + """Run a full security scan""" + results = { + "scan_id": f"scan_{datetime.utcnow().isoformat()}", + "started_at": datetime.utcnow().isoformat(), + "checks": [], + "total_issues": 0, + "critical_issues": 0, + "high_issues": 0, + "medium_issues": 0, + "low_issues": 0 + } + + # Run all security checks + checks = [ + SecurityScanService._check_weak_passwords(db), + SecurityScanService._check_inactive_users(db), + SecurityScanService._check_failed_login_attempts(db), + SecurityScanService._check_suspicious_activity(db), + SecurityScanService._check_unresolved_security_events(db), + SecurityScanService._check_expired_tokens(db), + SecurityScanService._check_unusual_payment_patterns(db), + SecurityScanService._check_data_retention_compliance(db), + ] + + for check in checks: + if check: + results["checks"].append(check) + results["total_issues"] += check.get("issue_count", 0) + severity = check.get("severity", "low") + if severity == "critical": + results["critical_issues"] += check.get("issue_count", 0) + elif severity == "high": + results["high_issues"] += check.get("issue_count", 0) + elif severity == "medium": + results["medium_issues"] += check.get("issue_count", 0) + else: + results["low_issues"] += check.get("issue_count", 0) + + completed_at = datetime.utcnow() + results["completed_at"] = completed_at.isoformat() + # Parse start time - handle ISO format with/without microseconds + start_str = results["started_at"] + try: + # Remove timezone info and microseconds for simpler parsing + if '.' in start_str: + start_str = start_str.split('.')[0] + if 'Z' in start_str: + start_str = start_str.replace('Z', '') + if '+' in start_str: + start_str = start_str.split('+')[0] + started_at = datetime.fromisoformat(start_str) + except Exception: + # Fallback: use current time if parsing fails + started_at = completed_at + results["duration_seconds"] = (completed_at - started_at).total_seconds() + + # Log critical and high issues as security events + for check in results["checks"]: + if check.get("severity") in ["critical", "high"] and check.get("issue_count", 0) > 0: + SecurityScanService._log_scan_finding(db, check) + + return results + + @staticmethod + def _check_weak_passwords(db: Session) -> Optional[Dict[str, Any]]: + """Check for users with weak passwords""" + # This is a placeholder - in production, you'd check password strength + # For now, we'll check for users without password changes in a long time + cutoff_date = datetime.utcnow() - timedelta(days=365) + users = db.query(User).filter( + User.created_at < cutoff_date, + User.is_active == True + ).all() + + if len(users) > 10: # Threshold + return { + "check_name": "Weak Passwords", + "check_type": "password_security", + "severity": "medium", + "status": "failed", + "issue_count": len(users), + "description": f"{len(users)} users have not changed passwords in over a year", + "recommendation": "Enforce password rotation policy", + "affected_items": [{"user_id": u.id, "email": u.email} for u in users[:10]] + } + return None + + @staticmethod + def _check_inactive_users(db: Session) -> Optional[Dict[str, Any]]: + """Check for inactive users that should be deactivated""" + cutoff_date = datetime.utcnow() - timedelta(days=180) + # Users who haven't logged in for 6 months + inactive_users = db.query(User).filter( + User.is_active == True + ).all() + + # This is simplified - in production, track last login + if len(inactive_users) > 50: + return { + "check_name": "Inactive Users", + "check_type": "user_management", + "severity": "low", + "status": "warning", + "issue_count": len(inactive_users), + "description": f"Found {len(inactive_users)} potentially inactive users", + "recommendation": "Review and deactivate inactive accounts", + "affected_items": [] + } + return None + + @staticmethod + def _check_failed_login_attempts(db: Session) -> Optional[Dict[str, Any]]: + """Check for excessive failed login attempts""" + from ..models.security_event import SecurityEvent, SecurityEventType + + recent_failures = db.query(SecurityEvent).filter( + SecurityEvent.event_type == SecurityEventType.login_failure, + SecurityEvent.created_at >= datetime.utcnow() - timedelta(hours=24) + ).count() + + if recent_failures > 50: + return { + "check_name": "Excessive Failed Logins", + "check_type": "authentication", + "severity": "high", + "status": "failed", + "issue_count": recent_failures, + "description": f"{recent_failures} failed login attempts in the last 24 hours", + "recommendation": "Review failed login attempts and consider IP blocking", + "affected_items": [] + } + return None + + @staticmethod + def _check_suspicious_activity(db: Session) -> Optional[Dict[str, Any]]: + """Check for suspicious activity patterns""" + from ..models.security_event import SecurityEvent, SecurityEventType, SecurityEventSeverity + + suspicious_events = db.query(SecurityEvent).filter( + SecurityEvent.event_type == SecurityEventType.suspicious_activity, + SecurityEvent.resolved == False, + SecurityEvent.created_at >= datetime.utcnow() - timedelta(days=7) + ).count() + + if suspicious_events > 0: + return { + "check_name": "Unresolved Suspicious Activity", + "check_type": "threat_detection", + "severity": "critical" if suspicious_events > 5 else "high", + "status": "failed", + "issue_count": suspicious_events, + "description": f"{suspicious_events} unresolved suspicious activity events in the last 7 days", + "recommendation": "Review and resolve suspicious activity events immediately", + "affected_items": [] + } + return None + + @staticmethod + def _check_unresolved_security_events(db: Session) -> Optional[Dict[str, Any]]: + """Check for unresolved critical security events""" + from ..models.security_event import SecurityEvent, SecurityEventSeverity + + unresolved_critical = db.query(SecurityEvent).filter( + SecurityEvent.severity == SecurityEventSeverity.critical, + SecurityEvent.resolved == False, + SecurityEvent.created_at >= datetime.utcnow() - timedelta(days=7) + ).count() + + if unresolved_critical > 0: + return { + "check_name": "Unresolved Critical Events", + "check_type": "incident_management", + "severity": "critical", + "status": "failed", + "issue_count": unresolved_critical, + "description": f"{unresolved_critical} unresolved critical security events", + "recommendation": "Resolve critical security events immediately", + "affected_items": [] + } + return None + + @staticmethod + def _check_expired_tokens(db: Session) -> Optional[Dict[str, Any]]: + """Check for expired tokens that should be cleaned up""" + from ..models.refresh_token import RefreshToken + + expired_tokens = db.query(RefreshToken).filter( + RefreshToken.expires_at < datetime.utcnow() + ).count() + + if expired_tokens > 1000: + return { + "check_name": "Expired Tokens", + "check_type": "token_management", + "severity": "low", + "status": "warning", + "issue_count": expired_tokens, + "description": f"{expired_tokens} expired tokens found in database", + "recommendation": "Clean up expired tokens to improve database performance", + "affected_items": [] + } + return None + + @staticmethod + def _check_unusual_payment_patterns(db: Session) -> Optional[Dict[str, Any]]: + """Check for unusual payment patterns that might indicate fraud""" + from ..models.payment import PaymentStatus + + # Check for multiple failed payments from same IP + recent_payments = db.query(Payment).filter( + Payment.payment_date >= datetime.utcnow() - timedelta(hours=24) + ).all() + + # Simplified check - in production, use more sophisticated fraud detection + failed_payments = [p for p in recent_payments if p.payment_status == PaymentStatus.failed] + + if len(failed_payments) > 20: + return { + "check_name": "Unusual Payment Patterns", + "check_type": "fraud_detection", + "severity": "medium", + "status": "warning", + "issue_count": len(failed_payments), + "description": f"{len(failed_payments)} failed payments in the last 24 hours", + "recommendation": "Review failed payment patterns for potential fraud", + "affected_items": [] + } + return None + + @staticmethod + def _check_data_retention_compliance(db: Session) -> Optional[Dict[str, Any]]: + """Check data retention policy compliance""" + from ..models.gdpr_compliance import DataRetentionPolicy + + policies = db.query(DataRetentionPolicy).filter( + DataRetentionPolicy.is_active == True, + DataRetentionPolicy.auto_delete == True + ).all() + + # Check if there's data that should have been deleted + issues = [] + for policy in policies: + # This is simplified - in production, check actual data age + if policy.retention_days < 30: # Very short retention + issues.append({ + "policy": policy.data_type, + "retention_days": policy.retention_days + }) + + if issues: + return { + "check_name": "Data Retention Compliance", + "check_type": "gdpr_compliance", + "severity": "high", + "status": "warning", + "issue_count": len(issues), + "description": f"Found {len(issues)} data retention policies that may need review", + "recommendation": "Review data retention policies for GDPR compliance", + "affected_items": issues + } + return None + + @staticmethod + def _log_scan_finding(db: Session, check: Dict[str, Any]): + """Log scan findings as security events""" + try: + event = SecurityEvent( + event_type=SecurityEventType.suspicious_activity, + severity=SecurityEventSeverity(check["severity"]), + description=f"Security Scan: {check['check_name']} - {check['description']}", + details={ + "check_type": check.get("check_type"), + "issue_count": check.get("issue_count"), + "recommendation": check.get("recommendation"), + "affected_items": check.get("affected_items", []) + } + ) + db.add(event) + db.commit() + except Exception as e: + logger.error(f"Error logging scan finding: {str(e)}") + db.rollback() + + @staticmethod + def schedule_scan(db: Session, interval_hours: int = 24) -> Dict[str, Any]: + """Schedule automatic security scans""" + # In production, use a task scheduler like Celery or APScheduler + # For now, this is a placeholder that returns scan configuration + return { + "scheduled": True, + "interval_hours": interval_hours, + "next_scan": (datetime.utcnow() + timedelta(hours=interval_hours)).isoformat(), + "message": "Scan scheduled. In production, use a task scheduler to run scans automatically." + } + +security_scan_service = SecurityScanService() + diff --git a/Backend/src/services/task_service.py b/Backend/src/services/task_service.py new file mode 100644 index 00000000..47676ccc --- /dev/null +++ b/Backend/src/services/task_service.py @@ -0,0 +1,338 @@ +from sqlalchemy.orm import Session, joinedload, selectinload +from sqlalchemy import and_, or_, func, desc, case +from typing import Optional, Dict, List, Any +from datetime import datetime, timedelta +from ..models.workflow import Task, TaskComment, TaskStatus, TaskPriority +from ..models.user import User +import logging + +logger = logging.getLogger(__name__) + +class TaskService: + """Task Management Service""" + + @staticmethod + def create_task( + db: Session, + title: str, + created_by: int, + task_type: str = 'general', + description: Optional[str] = None, + priority: TaskPriority = TaskPriority.medium, + workflow_instance_id: Optional[int] = None, + booking_id: Optional[int] = None, + room_id: Optional[int] = None, + assigned_to: Optional[int] = None, + due_date: Optional[datetime] = None, + estimated_duration_minutes: Optional[int] = None, + metadata: Optional[Dict[str, Any]] = None + ) -> Task: + """Create a new task""" + task = Task( + title=title, + description=description, + task_type=task_type, + priority=priority, + workflow_instance_id=workflow_instance_id, + booking_id=booking_id, + room_id=room_id, + assigned_to=assigned_to, + created_by=created_by, + due_date=due_date, + estimated_duration_minutes=estimated_duration_minutes, + meta_data=metadata or {}, + status=TaskStatus.pending if not assigned_to else TaskStatus.assigned + ) + db.add(task) + db.commit() + db.refresh(task) + return task + + @staticmethod + def get_tasks( + db: Session, + assigned_to: Optional[int] = None, + created_by: Optional[int] = None, + status: Optional[TaskStatus] = None, + priority: Optional[TaskPriority] = None, + task_type: Optional[str] = None, + booking_id: Optional[int] = None, + room_id: Optional[int] = None, + workflow_instance_id: Optional[int] = None, + overdue_only: bool = False, + skip: int = 0, + limit: int = 100 + ) -> List[Task]: + """Get tasks with optional filters""" + query = db.query(Task).options( + joinedload(Task.assignee).selectinload(User.role), + joinedload(Task.creator_user).selectinload(User.role) + ) + + if assigned_to: + query = query.filter(Task.assigned_to == assigned_to) + if created_by: + query = query.filter(Task.created_by == created_by) + if status: + query = query.filter(Task.status == status) + if priority: + query = query.filter(Task.priority == priority) + if task_type: + query = query.filter(Task.task_type == task_type) + if booking_id: + query = query.filter(Task.booking_id == booking_id) + if room_id: + query = query.filter(Task.room_id == room_id) + if workflow_instance_id: + query = query.filter(Task.workflow_instance_id == workflow_instance_id) + if overdue_only: + now = datetime.utcnow() + query = query.filter( + and_( + Task.due_date < now, + Task.status != TaskStatus.completed, + Task.status != TaskStatus.cancelled + ) + ) + + # MySQL doesn't support NULLS LAST, so we use CASE to handle NULLs + return query.order_by( + desc(Task.priority == TaskPriority.urgent), + desc(Task.priority == TaskPriority.high), + case((Task.due_date.is_(None), 1), else_=0), # NULLs last + Task.due_date.asc(), + Task.created_at.desc() + ).offset(skip).limit(limit).all() + + @staticmethod + def get_task_by_id(db: Session, task_id: int) -> Optional[Task]: + """Get task by ID""" + return db.query(Task).options( + joinedload(Task.assignee).selectinload(User.role), + joinedload(Task.creator_user).selectinload(User.role) + ).filter(Task.id == task_id).first() + + @staticmethod + def update_task( + db: Session, + task_id: int, + title: Optional[str] = None, + description: Optional[str] = None, + status: Optional[TaskStatus] = None, + priority: Optional[TaskPriority] = None, + assigned_to: Optional[int] = None, + due_date: Optional[datetime] = None, + notes: Optional[str] = None, + actual_duration_minutes: Optional[int] = None + ) -> Optional[Task]: + """Update task""" + task = db.query(Task).filter(Task.id == task_id).first() + if not task: + return None + + if title is not None: + task.title = title + if description is not None: + task.description = description + if status is not None: + task.status = status + if status == TaskStatus.completed: + task.completed_at = datetime.utcnow() + if not task.actual_duration_minutes and task.created_at: + duration = (datetime.utcnow() - task.created_at).total_seconds() / 60 + task.actual_duration_minutes = int(duration) + if priority is not None: + task.priority = priority + if assigned_to is not None: + task.assigned_to = assigned_to + if assigned_to and task.status == TaskStatus.pending: + task.status = TaskStatus.assigned + if due_date is not None: + task.due_date = due_date + if notes is not None: + task.notes = notes + if actual_duration_minutes is not None: + task.actual_duration_minutes = actual_duration_minutes + + # Update status to overdue if past due date + if task.due_date and task.due_date < datetime.utcnow(): + if task.status not in [TaskStatus.completed, TaskStatus.cancelled]: + task.status = TaskStatus.overdue + + task.updated_at = datetime.utcnow() + db.commit() + db.refresh(task) + return task + + @staticmethod + def assign_task(db: Session, task_id: int, user_id: int) -> Optional[Task]: + """Assign task to a user""" + task = db.query(Task).filter(Task.id == task_id).first() + if not task: + return None + + task.assigned_to = user_id + if task.status == TaskStatus.pending: + task.status = TaskStatus.assigned + task.updated_at = datetime.utcnow() + db.commit() + db.refresh(task) + return task + + @staticmethod + def start_task(db: Session, task_id: int) -> Optional[Task]: + """Mark task as in progress""" + task = db.query(Task).filter(Task.id == task_id).first() + if not task: + return None + + task.status = TaskStatus.in_progress + task.updated_at = datetime.utcnow() + db.commit() + db.refresh(task) + return task + + @staticmethod + def complete_task(db: Session, task_id: int, notes: Optional[str] = None) -> Optional[Task]: + """Mark task as completed""" + task = db.query(Task).filter(Task.id == task_id).first() + if not task: + return None + + task.status = TaskStatus.completed + task.completed_at = datetime.utcnow() + if notes: + task.notes = (task.notes or '') + f'\n\nCompleted: {notes}' + + # Calculate actual duration + if task.created_at: + duration = (datetime.utcnow() - task.created_at).total_seconds() / 60 + task.actual_duration_minutes = int(duration) + + task.updated_at = datetime.utcnow() + db.commit() + db.refresh(task) + + # Update workflow instance status if this task belongs to one + if task.workflow_instance_id: + from ..services.workflow_service import WorkflowService + WorkflowService.complete_workflow_instance(db, task.workflow_instance_id) + + return task + + @staticmethod + def cancel_task(db: Session, task_id: int, reason: Optional[str] = None) -> Optional[Task]: + """Cancel task""" + task = db.query(Task).filter(Task.id == task_id).first() + if not task: + return None + + task.status = TaskStatus.cancelled + if reason: + task.notes = (task.notes or '') + f'\n\nCancelled: {reason}' + task.updated_at = datetime.utcnow() + db.commit() + db.refresh(task) + return task + + @staticmethod + def add_task_comment( + db: Session, + task_id: int, + user_id: int, + comment: str + ) -> TaskComment: + """Add comment to task""" + task_comment = TaskComment( + task_id=task_id, + user_id=user_id, + comment=comment + ) + db.add(task_comment) + db.commit() + db.refresh(task_comment) + return task_comment + + @staticmethod + def get_task_comments(db: Session, task_id: int) -> List[TaskComment]: + """Get all comments for a task""" + return db.query(TaskComment).options( + joinedload(TaskComment.user) + ).filter( + TaskComment.task_id == task_id + ).order_by(TaskComment.created_at.asc()).all() + + @staticmethod + def get_task_statistics( + db: Session, + assigned_to: Optional[int] = None, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None + ) -> Dict[str, Any]: + """Get task statistics""" + query = db.query(Task) + + if assigned_to: + query = query.filter(Task.assigned_to == assigned_to) + if start_date: + query = query.filter(Task.created_at >= start_date) + if end_date: + query = query.filter(Task.created_at <= end_date) + + total = query.count() + pending = query.filter(Task.status == TaskStatus.pending).count() + assigned = query.filter(Task.status == TaskStatus.assigned).count() + in_progress = query.filter(Task.status == TaskStatus.in_progress).count() + completed = query.filter(Task.status == TaskStatus.completed).count() + overdue = query.filter(Task.status == TaskStatus.overdue).count() + cancelled = query.filter(Task.status == TaskStatus.cancelled).count() + + # Calculate average completion time + completed_tasks = query.filter(Task.status == TaskStatus.completed).all() + avg_completion_time = None + if completed_tasks: + total_duration = sum( + t.actual_duration_minutes or 0 + for t in completed_tasks + if t.actual_duration_minutes + ) + count_with_duration = sum(1 for t in completed_tasks if t.actual_duration_minutes) + if count_with_duration > 0: + avg_completion_time = total_duration / count_with_duration + + return { + 'total': total, + 'pending': pending, + 'assigned': assigned, + 'in_progress': in_progress, + 'completed': completed, + 'overdue': overdue, + 'cancelled': cancelled, + 'completion_rate': (completed / total * 100) if total > 0 else 0, + 'average_completion_time_minutes': avg_completion_time + } + + @staticmethod + def get_my_tasks(db: Session, user_id: int, status: Optional[TaskStatus] = None) -> List[Task]: + """Get tasks assigned to a user""" + query = db.query(Task).options( + joinedload(Task.assignee).selectinload(User.role), + joinedload(Task.creator_user).selectinload(User.role) + ).filter(Task.assigned_to == user_id) + + if status: + query = query.filter(Task.status == status) + else: + # Exclude completed and cancelled by default + query = query.filter( + Task.status.notin_([TaskStatus.completed, TaskStatus.cancelled]) + ) + + # MySQL doesn't support NULLS LAST, so we use CASE to handle NULLs + return query.order_by( + desc(Task.priority == TaskPriority.urgent), + desc(Task.priority == TaskPriority.high), + case((Task.due_date.is_(None), 1), else_=0), # NULLs last + Task.due_date.asc() + ).all() + diff --git a/Backend/src/services/workflow_service.py b/Backend/src/services/workflow_service.py new file mode 100644 index 00000000..e92b68e0 --- /dev/null +++ b/Backend/src/services/workflow_service.py @@ -0,0 +1,314 @@ +from sqlalchemy.orm import Session +from sqlalchemy import and_, or_, func +from typing import Optional, Dict, List, Any +from datetime import datetime, timedelta +from ..models.workflow import ( + Workflow, WorkflowInstance, Task, TaskComment, + WorkflowType, WorkflowStatus, WorkflowTrigger, TaskStatus, TaskPriority +) +from ..models.booking import Booking, BookingStatus +from ..models.room import Room +from ..models.user import User +import logging + +logger = logging.getLogger(__name__) + +class WorkflowService: + """Workflow Automation & Task Management Service""" + + @staticmethod + def create_workflow( + db: Session, + name: str, + workflow_type: WorkflowType, + trigger: WorkflowTrigger, + steps: List[Dict[str, Any]], + created_by: int, + description: Optional[str] = None, + trigger_config: Optional[Dict[str, Any]] = None, + sla_hours: Optional[int] = None + ) -> Workflow: + """Create a new workflow""" + workflow = Workflow( + name=name, + description=description, + workflow_type=workflow_type, + trigger=trigger, + trigger_config=trigger_config or {}, + steps=steps, + sla_hours=sla_hours, + created_by=created_by, + status=WorkflowStatus.active, + is_active=True + ) + db.add(workflow) + db.commit() + db.refresh(workflow) + return workflow + + @staticmethod + def get_workflows( + db: Session, + workflow_type: Optional[WorkflowType] = None, + status: Optional[WorkflowStatus] = None, + skip: int = 0, + limit: int = 100 + ) -> List[Workflow]: + """Get workflows with optional filters""" + query = db.query(Workflow) + + if workflow_type: + query = query.filter(Workflow.workflow_type == workflow_type) + if status: + query = query.filter(Workflow.status == status) + + return query.filter(Workflow.is_active == True).offset(skip).limit(limit).all() + + @staticmethod + def get_workflow_by_id(db: Session, workflow_id: int) -> Optional[Workflow]: + """Get workflow by ID""" + return db.query(Workflow).filter(Workflow.id == workflow_id).first() + + @staticmethod + def update_workflow( + db: Session, + workflow_id: int, + name: Optional[str] = None, + description: Optional[str] = None, + steps: Optional[List[Dict[str, Any]]] = None, + status: Optional[WorkflowStatus] = None, + trigger_config: Optional[Dict[str, Any]] = None, + sla_hours: Optional[int] = None + ) -> Optional[Workflow]: + """Update workflow""" + workflow = db.query(Workflow).filter(Workflow.id == workflow_id).first() + if not workflow: + return None + + if name is not None: + workflow.name = name + if description is not None: + workflow.description = description + if steps is not None: + workflow.steps = steps + if status is not None: + workflow.status = status + if trigger_config is not None: + workflow.trigger_config = trigger_config + if sla_hours is not None: + workflow.sla_hours = sla_hours + + workflow.updated_at = datetime.utcnow() + db.commit() + db.refresh(workflow) + return workflow + + @staticmethod + def delete_workflow(db: Session, workflow_id: int) -> bool: + """Soft delete workflow""" + workflow = db.query(Workflow).filter(Workflow.id == workflow_id).first() + if not workflow: + return False + + workflow.is_active = False + workflow.status = WorkflowStatus.archived + workflow.updated_at = datetime.utcnow() + db.commit() + return True + + @staticmethod + def trigger_workflow( + db: Session, + workflow_id: int, + booking_id: Optional[int] = None, + room_id: Optional[int] = None, + user_id: Optional[int] = None, + metadata: Optional[Dict[str, Any]] = None + ) -> Optional[WorkflowInstance]: + """Trigger a workflow and create an instance""" + workflow = db.query(Workflow).filter( + and_(Workflow.id == workflow_id, Workflow.is_active == True) + ).first() + + if not workflow or workflow.status != WorkflowStatus.active: + return None + + # Calculate due date based on SLA + due_date = None + if workflow.sla_hours: + due_date = datetime.utcnow() + timedelta(hours=workflow.sla_hours) + + # Create workflow instance + instance = WorkflowInstance( + workflow_id=workflow_id, + booking_id=booking_id, + room_id=room_id, + user_id=user_id, + status='pending', + due_date=due_date, + meta_data=metadata or {} + ) + db.add(instance) + db.commit() + db.refresh(instance) + + # Create tasks from workflow steps + WorkflowService._create_tasks_from_workflow(db, instance, workflow, created_by=workflow.created_by) + + return instance + + @staticmethod + def _create_tasks_from_workflow( + db: Session, + instance: WorkflowInstance, + workflow: Workflow, + created_by: int + ): + """Create tasks from workflow steps""" + for step in workflow.steps: + task = Task( + title=step.get('title', 'Untitled Task'), + description=step.get('description'), + task_type=step.get('task_type', 'general'), + priority=TaskPriority(step.get('priority', 'medium')), + workflow_instance_id=instance.id, + booking_id=instance.booking_id, + room_id=instance.room_id, + assigned_to=step.get('assigned_to'), + created_by=created_by, + due_date=WorkflowService._calculate_task_due_date(step, instance.due_date), + estimated_duration_minutes=step.get('estimated_duration_minutes'), + meta_data=step.get('metadata', {}) + ) + db.add(task) + + db.commit() + + @staticmethod + def _calculate_task_due_date(step: Dict[str, Any], workflow_due_date: Optional[datetime]) -> Optional[datetime]: + """Calculate task due date based on step configuration""" + if not workflow_due_date: + return None + + offset_hours = step.get('due_date_offset_hours', 0) + return workflow_due_date - timedelta(hours=offset_hours) + + @staticmethod + def get_workflow_instances( + db: Session, + workflow_id: Optional[int] = None, + booking_id: Optional[int] = None, + status: Optional[str] = None, + skip: int = 0, + limit: int = 100 + ) -> List[WorkflowInstance]: + """Get workflow instances with optional filters""" + query = db.query(WorkflowInstance) + + if workflow_id: + query = query.filter(WorkflowInstance.workflow_id == workflow_id) + if booking_id: + query = query.filter(WorkflowInstance.booking_id == booking_id) + if status: + query = query.filter(WorkflowInstance.status == status) + + return query.order_by(WorkflowInstance.created_at.desc()).offset(skip).limit(limit).all() + + @staticmethod + def complete_workflow_instance(db: Session, instance_id: int) -> Optional[WorkflowInstance]: + """Mark workflow instance as completed""" + instance = db.query(WorkflowInstance).filter(WorkflowInstance.id == instance_id).first() + if not instance: + return None + + # Check if all tasks are completed + incomplete_tasks = db.query(Task).filter( + and_( + Task.workflow_instance_id == instance_id, + Task.status != TaskStatus.completed, + Task.status != TaskStatus.cancelled + ) + ).count() + + if incomplete_tasks > 0: + instance.status = 'in_progress' + else: + instance.status = 'completed' + instance.completed_at = datetime.utcnow() + + instance.updated_at = datetime.utcnow() + db.commit() + db.refresh(instance) + return instance + + @staticmethod + def get_pre_arrival_workflows(db: Session) -> List[Workflow]: + """Get pre-arrival workflows""" + return db.query(Workflow).filter( + and_( + Workflow.workflow_type == WorkflowType.pre_arrival, + Workflow.status == WorkflowStatus.active, + Workflow.is_active == True + ) + ).all() + + @staticmethod + def get_room_preparation_workflows(db: Session) -> List[Workflow]: + """Get room preparation workflows""" + return db.query(Workflow).filter( + and_( + Workflow.workflow_type == WorkflowType.room_preparation, + Workflow.status == WorkflowStatus.active, + Workflow.is_active == True + ) + ).all() + + @staticmethod + def auto_trigger_workflows_for_booking(db: Session, booking: Booking): + """Automatically trigger workflows for a booking based on trigger conditions""" + # Get workflows that should be triggered for this booking + workflows = db.query(Workflow).filter( + and_( + Workflow.is_active == True, + Workflow.status == WorkflowStatus.active, + or_( + Workflow.trigger == WorkflowTrigger.booking_created, + Workflow.trigger == WorkflowTrigger.booking_confirmed + ) + ) + ).all() + + for workflow in workflows: + # Check trigger conditions + if WorkflowService._should_trigger_workflow(workflow, booking): + WorkflowService.trigger_workflow( + db=db, + workflow_id=workflow.id, + booking_id=booking.id, + room_id=booking.room_id, + user_id=booking.user_id, + meta_data={'booking_status': booking.status.value if hasattr(booking.status, 'value') else str(booking.status)} + ) + + @staticmethod + def _should_trigger_workflow(workflow: Workflow, booking: Booking) -> bool: + """Check if workflow should be triggered for a booking""" + trigger_config = workflow.trigger_config or {} + + # Check booking status filter + if 'booking_status' in trigger_config: + required_status = trigger_config['booking_status'] + booking_status = booking.status.value if hasattr(booking.status, 'value') else str(booking.status) + if booking_status != required_status: + return False + + # Check time-based triggers (e.g., X hours before check-in) + if 'hours_before_checkin' in trigger_config: + hours_before = trigger_config['hours_before_checkin'] + if booking.check_in_date: + trigger_time = booking.check_in_date - timedelta(hours=hours_before) + if datetime.utcnow() < trigger_time: + return False + + return True + diff --git a/Backend/src/tasks/security_scan_task.py b/Backend/src/tasks/security_scan_task.py new file mode 100644 index 00000000..e8abaeac --- /dev/null +++ b/Backend/src/tasks/security_scan_task.py @@ -0,0 +1,66 @@ +""" +Automated Security Scan Task +This module can be integrated with task schedulers like Celery or APScheduler +to run security scans automatically at scheduled intervals. +""" + +from datetime import datetime +import logging +from ..services.security_scan_service import security_scan_service +from ..config.database import get_db +from ..config.logging_config import get_logger + +logger = get_logger(__name__) + +def run_scheduled_security_scan(): + """ + Task function to run scheduled security scans. + This can be called by: + - Celery periodic tasks + - APScheduler + - Cron jobs + - Systemd timers + """ + try: + logger.info("Starting scheduled security scan") + db_gen = get_db() + db = next(db_gen) + + try: + results = security_scan_service.run_full_scan(db=db) + logger.info( + f"Security scan completed: {results['total_issues']} issues found " + f"({results['critical_issues']} critical, {results['high_issues']} high)" + ) + return results + finally: + db.close() + except Exception as e: + logger.error(f"Error running scheduled security scan: {str(e)}", exc_info=True) + raise + +# Example Celery task (uncomment if using Celery): +# from celery import shared_task +# +# @shared_task +# def scheduled_security_scan_task(): +# """Celery task for scheduled security scans""" +# return run_scheduled_security_scan() + +# Example APScheduler job (uncomment if using APScheduler): +# from apscheduler.schedulers.background import BackgroundScheduler +# +# def setup_scheduled_scans(): +# """Setup scheduled security scans using APScheduler""" +# scheduler = BackgroundScheduler() +# scheduler.add_job( +# run_scheduled_security_scan, +# 'interval', +# hours=24, # Run every 24 hours +# id='security_scan', +# name='Automated Security Scan', +# replace_existing=True +# ) +# scheduler.start() +# return scheduler + diff --git a/Backend/src/utils/__pycache__/currency_helpers.cpython-312.pyc b/Backend/src/utils/__pycache__/currency_helpers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca704c5cab81760ce80020b8745214903428f882 GIT binary patch literal 796 zcmZ8ezi-n(6h8Z$HpFcbC?G*WvDyxJC}xFLNX-ut3WO3jpk%Rv@0zAf?dWVHVs$`> z|Dg**5kgRffe|JoR+gd;RJVYDKOj|L$;5MhRqE%{_ujpC{@%NHUneIc2q0absr?WT z`Wc_&6RKh! zPZbM=Og>dI^wPcL{kuBHRqFDP*Jx0t3Tz6LouGH6o6*+oEvi+mr&^jio37o^a<)tB z^GVy@gvZd56?2nXM01#_dGLUaHk^X7PV1oOTx~S@6zChHM88FL5h|PuN?ihahW^OY z-{r(NIk6jl8+{c$P_BKrav(44OA8&yqr58dal;_CX&CH01UPnF))U!7Y#IXW0#G-+ zxMdTsPH(aVY=c`k4}jWO5QHO96r^?-O~el6_@NBkUwwZR0cWs|F2~M_`zYR?5rxIw P*){_6ZuW0rdL{n@OyARd literal 0 HcmV?d00001 diff --git a/Backend/src/utils/__pycache__/response_helpers.cpython-312.pyc b/Backend/src/utils/__pycache__/response_helpers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be9648ee1b79a518e81d50948d880bb653f3882e GIT binary patch literal 1291 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 Q2-G7-1Ao9dBO*wD0|qyHF#rGn literal 0 HcmV?d00001 diff --git a/Backend/src/utils/currency_helpers.py b/Backend/src/utils/currency_helpers.py new file mode 100644 index 00000000..8aa3e2c7 --- /dev/null +++ b/Backend/src/utils/currency_helpers.py @@ -0,0 +1,24 @@ +""" +Utility functions for currency handling +""" +CURRENCY_SYMBOLS = { + 'USD': '$', + 'EUR': '€', + 'GBP': '£', + 'JPY': '¥', + 'CNY': '¥', + 'KRW': '₩', + 'SGD': 'S$', + 'THB': '฿', + 'AUD': 'A$', + 'CAD': 'C$', + 'VND': '₫', + 'INR': '₹', + 'CHF': 'CHF', + 'NZD': 'NZ$' +} + +def get_currency_symbol(currency: str) -> str: + """Get currency symbol for a given currency code""" + return CURRENCY_SYMBOLS.get(currency.upper(), currency) + diff --git a/Backend/src/utils/response_helpers.py b/Backend/src/utils/response_helpers.py new file mode 100644 index 00000000..b7729c1f --- /dev/null +++ b/Backend/src/utils/response_helpers.py @@ -0,0 +1,51 @@ +""" +Utility functions for standardizing API responses +""" +from typing import Any, Dict, Optional + +def success_response( + data: Any = None, + message: Optional[str] = None, + **kwargs +) -> Dict[str, Any]: + """ + Create a standardized success response. + Returns both 'success' (boolean) and 'status' (string) for backward compatibility. + """ + response: Dict[str, Any] = { + 'success': True, + 'status': 'success' + } + + if data is not None: + response['data'] = data + + if message: + response['message'] = message + + # Add any additional fields + response.update(kwargs) + + return response + +def error_response( + message: str, + errors: Optional[list] = None, + **kwargs +) -> Dict[str, Any]: + """ + Create a standardized error response. + """ + response: Dict[str, Any] = { + 'success': False, + 'status': 'error', + 'message': message + } + + if errors: + response['errors'] = errors + + response.update(kwargs) + + return response + diff --git a/Backend/src/utils/role_helpers.py b/Backend/src/utils/role_helpers.py new file mode 100644 index 00000000..ab8a8677 --- /dev/null +++ b/Backend/src/utils/role_helpers.py @@ -0,0 +1,47 @@ +""" +Utility functions for role-based access control +""" +from sqlalchemy.orm import Session +from ..models.user import User +from ..models.role import Role + +def get_user_role_name(user: User, db: Session) -> str: + """Get the role name for a user""" + role = db.query(Role).filter(Role.id == user.role_id).first() + return role.name if role else 'customer' + +def is_admin(user: User, db: Session) -> bool: + """Check if user is admin""" + return get_user_role_name(user, db) == 'admin' + +def is_staff(user: User, db: Session) -> bool: + """Check if user is staff""" + return get_user_role_name(user, db) == 'staff' + +def is_accountant(user: User, db: Session) -> bool: + """Check if user is accountant""" + return get_user_role_name(user, db) == 'accountant' + +def is_customer(user: User, db: Session) -> bool: + """Check if user is customer""" + return get_user_role_name(user, db) == 'customer' + +def can_access_all_payments(user: User, db: Session) -> bool: + """Check if user can see all payments (admin or accountant)""" + role_name = get_user_role_name(user, db) + return role_name in ['admin', 'accountant'] + +def can_access_all_invoices(user: User, db: Session) -> bool: + """Check if user can see all invoices (admin or accountant)""" + role_name = get_user_role_name(user, db) + return role_name in ['admin', 'accountant'] + +def can_create_invoices(user: User, db: Session) -> bool: + """Check if user can create invoices (admin, staff, or accountant)""" + role_name = get_user_role_name(user, db) + return role_name in ['admin', 'staff', 'accountant'] + +def can_manage_users(user: User, db: Session) -> bool: + """Check if user can manage users (admin only)""" + return is_admin(user, db) + diff --git a/Backend/venv/bin/coverage b/Backend/venv/bin/coverage new file mode 100755 index 00000000..413244ed --- /dev/null +++ b/Backend/venv/bin/coverage @@ -0,0 +1,7 @@ +#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python +import sys +from coverage.cmdline import main +if __name__ == '__main__': + if sys.argv[0].endswith('.exe'): + sys.argv[0] = sys.argv[0][:-4] + sys.exit(main()) diff --git a/Backend/venv/bin/coverage-3.12 b/Backend/venv/bin/coverage-3.12 new file mode 100755 index 00000000..413244ed --- /dev/null +++ b/Backend/venv/bin/coverage-3.12 @@ -0,0 +1,7 @@ +#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python +import sys +from coverage.cmdline import main +if __name__ == '__main__': + if sys.argv[0].endswith('.exe'): + sys.argv[0] = sys.argv[0][:-4] + sys.exit(main()) diff --git a/Backend/venv/bin/coverage3 b/Backend/venv/bin/coverage3 new file mode 100755 index 00000000..413244ed --- /dev/null +++ b/Backend/venv/bin/coverage3 @@ -0,0 +1,7 @@ +#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python +import sys +from coverage.cmdline import main +if __name__ == '__main__': + if sys.argv[0].endswith('.exe'): + sys.argv[0] = sys.argv[0][:-4] + sys.exit(main()) diff --git a/Backend/venv/bin/httpx b/Backend/venv/bin/httpx new file mode 100755 index 00000000..06a9c9ab --- /dev/null +++ b/Backend/venv/bin/httpx @@ -0,0 +1,7 @@ +#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python +import sys +from httpx import main +if __name__ == '__main__': + if sys.argv[0].endswith('.exe'): + sys.argv[0] = sys.argv[0][:-4] + sys.exit(main()) diff --git a/Backend/venv/bin/pip b/Backend/venv/bin/pip index 97cb4193..b3de0772 100755 --- a/Backend/venv/bin/pip +++ b/Backend/venv/bin/pip @@ -1,8 +1,7 @@ -#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3 -# -*- coding: utf-8 -*- -import re +#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python import sys from pip._internal.cli.main import main if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + if sys.argv[0].endswith('.exe'): + sys.argv[0] = sys.argv[0][:-4] sys.exit(main()) diff --git a/Backend/venv/bin/pip3 b/Backend/venv/bin/pip3 index 97cb4193..b3de0772 100755 --- a/Backend/venv/bin/pip3 +++ b/Backend/venv/bin/pip3 @@ -1,8 +1,7 @@ -#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3 -# -*- coding: utf-8 -*- -import re +#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python import sys from pip._internal.cli.main import main if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + if sys.argv[0].endswith('.exe'): + sys.argv[0] = sys.argv[0][:-4] sys.exit(main()) diff --git a/Backend/venv/bin/pip3.12 b/Backend/venv/bin/pip3.12 index 97cb4193..b3de0772 100755 --- a/Backend/venv/bin/pip3.12 +++ b/Backend/venv/bin/pip3.12 @@ -1,8 +1,7 @@ -#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3 -# -*- coding: utf-8 -*- -import re +#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python import sys from pip._internal.cli.main import main if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + if sys.argv[0].endswith('.exe'): + sys.argv[0] = sys.argv[0][:-4] sys.exit(main()) diff --git a/Backend/venv/bin/py.test b/Backend/venv/bin/py.test new file mode 100755 index 00000000..765d415d --- /dev/null +++ b/Backend/venv/bin/py.test @@ -0,0 +1,7 @@ +#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python +import sys +from pytest import console_main +if __name__ == '__main__': + if sys.argv[0].endswith('.exe'): + sys.argv[0] = sys.argv[0][:-4] + sys.exit(console_main()) diff --git a/Backend/venv/bin/pygmentize b/Backend/venv/bin/pygmentize new file mode 100755 index 00000000..2c00d8d3 --- /dev/null +++ b/Backend/venv/bin/pygmentize @@ -0,0 +1,7 @@ +#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python +import sys +from pygments.cmdline import main +if __name__ == '__main__': + if sys.argv[0].endswith('.exe'): + sys.argv[0] = sys.argv[0][:-4] + sys.exit(main()) diff --git a/Backend/venv/bin/pytest b/Backend/venv/bin/pytest new file mode 100755 index 00000000..765d415d --- /dev/null +++ b/Backend/venv/bin/pytest @@ -0,0 +1,7 @@ +#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python +import sys +from pytest import console_main +if __name__ == '__main__': + if sys.argv[0].endswith('.exe'): + sys.argv[0] = sys.argv[0][:-4] + sys.exit(console_main()) diff --git a/Backend/venv/lib/python3.12/site-packages/__pycache__/py.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/__pycache__/py.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae542517fe9cfb0f3ac29dc36596a12e32c1c2c5 GIT binary patch literal 501 zcmZ`$Jx{|h5IrYNQ&NF|0fZ1LA5#ZvK30STLWnZJp2Z4{X+x7ZvfYYwDie%s{04pt zOW9BskSc`OkPr(Kc0U9QH~8MWclS=VA0{UqQ1QC48dMQLUz2f|Lx4WJ3fzGYJ_;d1 zEUKD(__=$?3+7^Q2QS>r&Ztb^L4|WNR6`n zu1Cu#D|&Ivq$dLwi<3?vNh_3d#yhq`i6^kbC%4b@)ET4Hi*ck1z276-~(! ze8eOT*Y_D~25}wld$lHwecYyT8;8LeR?8bK-mYwJVG&5Wo~Q<|PK6RGN%~#b8Lryp z`r*{afY?*VhlvpSDgavRgYyO}Jy>~$QV&WWd01TTn%Bjv;?40R%s(wGjac;&=3a`k Ow}(Av@yg~?%H;>^ID*dr literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/__pycache__/typing_extensions.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/__pycache__/typing_extensions.cpython-312.pyc index c808b902175ba203c59784228321a171cb98050b..3a31168a02d3d2896e182c2a6f083f4d07287db4 100644 GIT binary patch delta 28 icmZ4YpL5-RPVUpZyj%=GplGPn$lc1#xRsk}aXkQm#|PX1 delta 28 icmZ4YpL5-RPVUpZyj%=G@H9f9k-L?faVs~|;(7p$c?iV- diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__init__.py b/Backend/venv/lib/python3.12/site-packages/_pytest/__init__.py new file mode 100644 index 00000000..8eb8ec96 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/__init__.py @@ -0,0 +1,13 @@ +from __future__ import annotations + + +__all__ = ["__version__", "version_tuple"] + +try: + from ._version import version as __version__ + from ._version import version_tuple +except ImportError: # pragma: no cover + # broken installation, we don't even try + # unknown only works because we do poor mans version compare + __version__ = "unknown" + version_tuple = (0, 0, "unknown") diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e37f5a7037df26cdb382e4c1627904510fe02227 GIT binary patch literal 489 zcmXw!zfaph6vyAij?eieRfPqq6D1&5f`hUl)FGl$QAO1iOV!D6N3dewS?6;?HpJ3} zopxvG-_oUHWC@mvfh{Tv%GBqkdBfB9ectb%olXHFPyC+R9|Jr}ZFKeb$Rk2T}<(lxoP5_=II+d#=^)*3Mg&$5w7;SP=DP&Nyad zZW-sXiY@0`*?8vfr&<(=d}m(Zw(8G(58c!ugxtaE)PwfQH}}e(BZa$H{W{>S)?PFw5K6e%)Sy?~Sf=OS;Zq^gx88 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/_argcomplete.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/_argcomplete.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7337f601502d3acce882800a7eb6cfe42d94c703 GIT binary patch literal 4839 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/cacheprovider.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/cacheprovider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4a1bf7df72ab4c91df00021b4fe8a869af801a6 GIT binary patch literal 31700 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 DuoTS< literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/capture.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/capture.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94c580331c744db0dbdfcb58b645ec5db3d31519 GIT binary patch literal 55739 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 J@G-U7{|lq#^>+XO literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/compat.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/compat.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6b40de4e2b9f709ccaa48753b549d5fae64ce816 GIT binary patch literal 13019 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 Ai~s-t literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/debugging.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/debugging.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..05ce468ea3497f9c3e913897175d876e0d49526b GIT binary patch literal 18297 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 J)blc;e*sHc;pG4T literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/doctest.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/doctest.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3ed92326dc5349677ae6133f56221e01d5f5896 GIT binary patch literal 32661 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/fixtures.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/fixtures.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5fb01e7aa2ad08a2947839e7092480df584212d5 GIT binary patch literal 82899 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≦!GxcA_aP7}g5P$>_LqNnCcLdLQWL% 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 QUOH$o6@Sc;%NG9s0|lLilK=n! literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/legacypath.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/legacypath.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..deaa00d05261951a58612772a4fd3754dccf508a GIT binary patch literal 24174 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 cKF)F*XIOHkkL7wr_6TBo<1;g$K(%bf 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 z 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 P`l^2Y2LfK$F7p2aSq?ES literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/main.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64e96750b9826188292193af18b7c34aa1eb555d GIT binary patch literal 47350 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/monkeypatch.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/monkeypatch.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..afbd2e80aca330e2a74a3304693899c39bd34879 GIT binary patch literal 17223 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/nodes.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/nodes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4bdc87b7f52c89944fe71fd36bcce7986571ed68 GIT binary patch literal 30852 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/outcomes.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/outcomes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4fa11505ad7c1e3c98bcb254a9bd73032b7c4799 GIT binary patch literal 11730 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 KGe!hFnErqEE%)&N literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/pastebin.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/pastebin.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c077406a57a5c6c8f88a79ac1dbdfb1a0801ed30 GIT binary patch literal 5897 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/pathlib.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/pathlib.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ac6da062811615460b5d0a86ec9c25d7c04013b GIT binary patch literal 41817 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/pytester.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/pytester.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e04dacc6dcbb012ff16f5a67a3a66816e548e3cf GIT binary patch literal 85019 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/pytester_assertions.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/pytester_assertions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6ab96081da06b50ec53e67abfca1cfef4fa353a GIT binary patch literal 2498 zcmb7F&2Jk;6rc6S?s{#9%RymK#KsT9Alx$N?fDrBv3&JBhbh@0!_h z>y(WY5E2|yacDu6C?|sY5A?_#AtmI1Srw@g^uWzk;L;QC&Dv=awTf7;-~Qg4H*db) zZ{E+@tVZB@a{hJu7nP7dQRzSN2uOS10=PpgvPdk+iaAmvwiuHT$DMd1v6v7z;UpXK zqAYL{u(GH~Bqe@UD*7IjmS)LcBp20#HpEu?sO81Fm9bRlpITFl?5&wu>NF|Sn@(%R zcJ*Zs;2Nhsr|ccwuX`=W(p``1OH_9})1sE{xw_4L-Mi^d|BcHuq%du|;A3*zbA3?N z59uc@>eeXW?7N=h&>H`MHa*6{pe;b~NUJ5HZ4xpG61Gj+y9EHYOj=I_rwQ?uITDZuz0jXaYs84pFBtJq zZkoPNtx)k-?Pe2zylmPIwO|2phq{Iqs$PrNyax3d96n(w6qpcUzFOtXtkEU2wpzC> zTCIi}qo&h~m`2=>DjK555O9f`whLpawB3X)qn3{W_5J5ry;6q}D=Tih@*ee9x!0`B zdYn4vu6W+6?XFa=z?jsvDmSQmqvF_06*$Yf=e{+4;bO(NIX%||2Xlq`m1=a#=pYWe zitA^lIZ%Rh52-KhmpRlgBl@Y(FG5<77F3AM5g>pDcfyyK zMK0*PZgLP*_aPnaWgE%gOM2Y|R+vn`Y4f^pVHBOXeS>P4uWqZcM47LjKCd^Z>H2VG z=$37{lU(;|wH7mUxNI;qr)@B`c(cXzW#%<>6HGqWS<9VXh^U1}dw^duWz1uKsI>bW zZTGk-AE6?96?>jQF#=*fRBxKh1)lh!W>KHwy%bS;g>oaqGU$1j%|}A861t3{$fC%j zD1h*>?>xTRhh(ln%BX)((3f(w=4&LB0Yhj&zEpPT>T;0E(r4(;!g?x0TUOdGfp9n_z41 zp|o3m`zv*`w4;uJv5%DewldzCFn$>Oer(HmczJi?5;TqNsIPS<&i2thtII%WHVg|i z;!GUgzA!Yg$+uQ`tLf0I{W@{=c}=)BrSDr0l@2}7|F_pQahhS=c6pRQufe3mEzJ;x zGNZi3+%PfkxztdPO!kO6KW_|0vmdbR8o%muHU(MCqd`Q#7Y!0+kQi8GqCl9AQ4dV0 zXBvco_2Dd{e~>r3kWoV&cnigDz9^mP#u7QX8%LBO*}-lW(Eu69cXNma(I)%~MAr6-Xgj|j z0fJyCct^{f@riEnl%#hC#=3F%KF;L2N%#UkAee$k#b7B!8lp6!47O$wMS~(5B)O4p V9#MhhM@3X9Te9}gFxbSx{{xMQRt^9F literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/python.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/python.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22dc154041329181de92399f23423b5b422d427d GIT binary patch literal 75220 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�Z2Btk`?pH(Ta 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 at^RNJKi2r`anw`ns+z6&k%ot?xc?8s*a^V^ literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/python_api.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/python_api.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..35261a2cb8731ece1cb996576556512a75135a7f GIT binary patch literal 35400 zcmdUYdvsgJdFQ=&5ib(pLwrf3K#GzGi3COJMbUb{thZ%TvSr!|2?8Jqi3I3-L5ZY5 zS9aVk)wng)q!HEFYkK27me$%6`83%m+n%kQCXI8p+X4a|(g?Fh**a&N-R>URR1-&= zKlb;{eFB7JrOE!W*Vgc2?#$eIeDghi^Ud7E6(97%+Ai*}W0>rVbOkH{ssgVP^Mc+*>*3svirP# zTZav0So`e*jt++$=j?DIF0VhoBOiZl{RIQA4i|f7$GyA5&2u5QV8Js-f8jt;M-k82 z9`awXI_P}G)KP5U9_NDjuW-SFcMVdDI!c1BSGbNP?1>vs3PUCD8s%rDEVc--#k$xs z7F&YYCA!#h7F&wgGF@y1i!DcNMW{k8w=%dq=m}Q7V(wTPtO+hf&6WjggUj%@D!3w8 zg}>D!dEUC8Q#HJN+To8xqA`Cg9F2(hY2Op=?+*p&Cw}D}^$!k)BfYq_wufFA3Pl1T z+?(6OqoE)@-x~^q2mJlV=zdRhV6Zg584)Ae$@9n|i)Bj`|Cj1ru07{6u;=ErR;mbFM1YiF^n#!BoP=F5gx*O<6~Y0QrD z9HXe|m^1Ed$D4RH_B`h;I3^IudyQ%H!ALMPoVGsE9rK4HpHVr!#zDAfv6Kn*drm-51Kxg`RG2sHO--jxW{WsoBG3IENux##E=k6 z8wdQuX=8sV;>}MxdWQP@JEPraQ17&JAS8Y+B0~e+A)zzc)2UP{ov*w- z))@|p$lxFDM9n%WTKa{`D8J;w*t>0%Qp8je)8??$ZBez`LUOmI^VlP#aUiPSACabN zKDy1LzwUQ7^+g9lO}&xfroAEYY%Dt1bRZfF^*8R0M$cjnH|_QZ&W0kvrgNdlxu*Vb zchlfVtS=f_?`v*p62q}j;~)~CrD9Vjo`l4hoS@S`81@a0q+OlTD1{I)C`5+^&>FV{ z|HKxIed^@*G|4$nJU{bZkwpNu-*w*6&NS?7tmI9C|)+@O0wz7ZTRaS^J8qBU9TG))wth063=%0F#bU#@%;0NBhM%HoW5iI!aXBO{lu16Y`J)N-px775~k8$ z-LoN5Brf>f>c%}K_5kkGOJ;QZLN}tWa3QXP4{{xbAm3pO8ahltV~07IA1nx(z~U`I zSI`WWo)>fnE%<8<76$Y1*A}!w@Ys>wffyTNoWY`?9e?wK4m>Fc7Bivc8ZmfF()L|! zKzIAa&}iRrA$l$x40*&pzks4^%S0Y8zFM>WZH3Q0#mt z6tNRwN;5_DAeShM4*cj*;(f<{9z^>BDwD+z;st0uztDvnC<6SLInKwqPW+OtxDmfJ z0b#%|%EPX>3BTmN1Ec58*pw&Q-*@F(o*Uu3hGQqZW}yyIX(Q%q+K6eIwnY4q&gj{+ zqZ6DbaJDlxG8pn&(k3y~-^0=hK4cIYDQKdsg`J%-!5;{9WAgN&-#RgK)CvosL19sarO4fs+KS*JH&;-Tu+|70@FSoO1b&$Ck5!}`H#CqzL!4)l3j-)g zu6E9AIyPFUQIE{{N9~H}04ujE!lTV|9RtQHnzlz{ePF>NI$NZddgNN?ce47)Olq`T z-`Dyl`;bRmf#4!{*SUPQpnS4&(wT6sq`Q(yOTxJvjKOW6=S(*H;-=B0WlcMTn}0>U ztd77q*TV<-SFD$~T*`{3&g|tBI#h%_I>T@*V<>4_tf#4P1UG-mAI8{T0a2e`3i*yk zkMU#Z(M!fN>dTQ#zTyU@*t6i!U*)gxJojpq2_*mTjpun`6Ow@x3S=(cDF*!gegOi} z;9HmG{b_z!q6Zp7Q1L}Z68CLedqWWjAYuDxoxWo`Loal8`nL8*QHr?Tr=~iJ+~OJp zpZz2E*+uSC&QNUssA1i;UDsY1-!)M(Q&^WQtebi=Rk->?=ju<4c={`m-uT6uI^Ju* zgzWE67j=$=L;XQn?2vLGUYBXW;t#}z@J1T{E_wVdovbpWg^TJ(eJGN&LQUpeMGI7! z*$u5T8xAKo9R6T`YD4=yssZ9@*bU0C>-!c%E)DxK+%VR}YXc80R8IRc?=>DfG5WNh zG1PjmCoFoPUVFr$!NI72Dd!1CJbq7iG}<5XN5DHV4Tobs``&QSGZG#03?R16)8!XQ zDM7+*+hkr_-|g+%0TsGn7kdQtoK0SNP6#d3Lj7a_&!Lwrn!-}#la!q1rowaP3YN_} zLkmWOOWy%hpmNbYqkDDBH)QxgF;aql+=X)bttKv z3|>B#aIHvKS3G3O{RpY`6E4oj)G8|4yU?i9+fm6gdRqhe=|U{=udEBP%tppFE^7YovSdtu=*O9&#haJ5hehb8F+UVrfoh!2muOoyyRtH*ozl~GX+}#^ zMPTesM;gLz6f|0)pCl5s%ADXd9*g@CV6HfpO!!|fn|9R96?>)%ubIAKy>7i3{C3}a zeYayjxbXgk>EiZ;vwhCBbaL0!hId-uYQ0(V?eh1^Z`b``?fYw|UB?pEV~dBArizY| z^is;pk4B%RIq$*z_khiN2K`|{uQqsMQD_;m(g8EU&U6j%DbI;M%r*Q!Bx05!RalX8 zteh^1Idox$LFq+4%0id08P!=tx55F$j#lYAX%R2#MiP-I2CA=}bJePnP|r8;SZXX0 z(G_k4%x!e*Q~t0R!t6c)<$_IPPq>HGQc?On=luOcA%%JP=_eQj=_0;k(nQ;W;&dWg zAO&KyR9|0Be+g4sqJWn;HX7HB$Bymynk4wV)5kj#cA(A#7i$+3Vvdq_P&j5m*?>4w46>jg=b(ylt(y$K#8Xr?sb#;6Y&pV#;5I_odeO}P(R%} zJ3C(h2bZ7Zb#?}$0YtkxI~hdKIn*;0p@Y=7vr~Xz_d{{6i1ZKMv`@p)N!P%Fp(yZbYcNxo`>-3748!QZ?2!Wa%D>|J~3Od47&O~ zlf}Gr!Ofd%7c6G8eWAc;wts3jneFohoV(=Wfdvz9u3NAe%}?;3+Rf(Ldj-7tFwgp# z6`r9N5$eE5|L<^SE1_Kv2D5^)C&!t?%R8?40LWD|6 z`S}0emQ+a(SY6pJ12Zg5%!{IdFBVt%MMcWaG{f(}9K*k)}&N-wP z3G0!xojYZFZnaF8H^ESBnJM3tEZZY1H>DN4*CITHghDBTv^h2eWjt-7nGH)KENV6E z51$PQG;=Wn&r$~B<7q2ez-$uX41O|=sf(Bx*%>Ug6wou8eb9G4&MTa#`O1mc_g#K| zIDJ;@e4FPiNZ6?S4+?U z6Vw#U3mWm)9JDgJHNE15A!~+Nnm5i3^Bq>G!?v_TVrIvN1_nn)4;&8(us;VLXnCH8 zNf3Gv52YeNZa^M=i>M40<`<|lGZYK%4DrjX3nE{gnA)Q=G^w*LRlk=@8G~H_1IQ3G zyn#ss{(C=9lDYNwc`pW-##!Q4b+}7gVo_qU;HhMprJeLcmd>JHyJ9?Wv_#iYuqq`Z z?IJRYG)*_48v!JwP6`(lC#~9?#idITW%nvyMgORKp%n zma34UcJj5T?=3OXGTEiEF2KZj7Y&EFm-)Cko|j9NI}61?CSxlZTrXZdlFQB%H{Hsf2WlHJs{9Nsc7mV5=kYojC%oQV^$F8_7?o!`k zix&j*$T|@6bGK;B9x2!5wZ~n$Hy0@7kAmjZG680vf$^Jtp(S!{VB}sfad8KPno|~P zP>RN!u_`5xmR5_)?hmE3tQ5LFR8sU9AsAas+?X5hSgyV!CmqDyackUxKC-_nQ@<)Y zoe-Ti2|mwI6^M}*6K|@;4VnIA@MB!TZN`joL)=JRKW2EeT1*)$p z&GZ(gZx4hA!U34L#9>!M{ib992pc@zD4hU9!GB(Pfdn_R_lTKAG0hAJghQ%ICsieF zjRXOk(PDy`RXELunY^QSOE6U>8q~?2b@m9+0T`Op5SDv%TLx3q!#|q%esK0bMBj?5 z5M1P-=2fm7-#5FY>Qj!-ubJRy%c~~+SFM-L^G5u>TU_IWY7=jET!jzqcN9ZpV+__Fg5F=vP&Yk*0H~lx3Tzlag<;nU( zlBVgDqls3z?vmES)b>>?;(!NQny~gR(`!UY#yNW-NX%_>o=qY{xI3U@NC`LR}k$f5G=O0k^ zkHHx)azD0~|JYhQZ?fAeW-Dsvjr7;Ijs3lw?_MzD_k1~5S^Z|!)vB3_hGa#MaTD(pgx)E?>gxLwMIwk*HcXQ?)5swdsyy(_Qzz*`mtH z?TN}Y*H$Lh?U`A3FuCqvqUlh=eR$q%EGfXUOMU^?ZmMhFJbm@_OjT2|stM)KmM?qL zdeu5HGVaW{tsKvvU9xK8*_kD)lK8*=(5>y$^@kEm4u8toZ4Kl5K3cjlvFX4ED^p94 zk01G{azkPx{ir-Xes~rz#f6k>&D_%J$@t9j4awyjZeB<(-(bJU@t<2t zU7XK_s6kb7W)+K{xl%F}j2|(@V4X_WB`B4DR09N0XM?WbV$@Q0)p5cS*s-d;E3G>O zjc=&y>2c%Bh9E@fi@?IqbHm1Gx%0f&ED7LIcYt|AsJwgiXwhoo<&3xZWbcLjFiy-Y zCT$Ii5`3P{^NVcSr)~R&12AVHHq$m0-w}QvwNp6Q5acq$EU@}S`n_-nLD~=*O!J|% zt+O-G5BL|$NYwo_^Djy>{xE)y7Gm3(4EzXW{b-|c9mQ@ zf9b`^o|J24!n*Rad-+@m6M;|jbEV4@wJkR{-K?`PMl1*PyQbgiBI$Q zMN%Ut3wJeg?>E|ZJB;sda__bnziTn$K7&F@z^xz({R2>_B;c4qLCVhO(5P_a0Th}O zm(7LuDvBJGD%1f=)q^lyrfQub11RfKOpP9cEP7Xx==~km3t$zi*XPy%b4r~9S|ueV z@sVo2Nl_YrL`%LL7U54&lJI2&c7G^|9q65%o^ zwK{`v7h$PCLJsk51WcnKGIny#g4Z6q^w`9?X-Dyc zIeaB@Ig%<}1(-Ykdj3s+vc7euep|ABTdICXqI5^fy@Lwd$^VM++n0Cm*u(R}bEvb7 zk&YJ23p*FxQ|RvEtpK^wF}PeX8S+yG!&Ru{leok@01ZqN=|Q7FQatF zFYLz6BD^L~#%ruHUSrF~Yl3zdpKv>1xQz{Fh(lSTI0`KXmcZ!9Z-ddS)9MU33x10acwz;dWs4Om zspIB{$td&*fROA<>64)KhE4H(g(Ux)G%wlQw2wL9t>}n5Wq8X42r0*yyfhehz7(-> zG4f?&S3FM#N9D{Dcj|#K6{O0`{f3}j2E%gp$Cx$Jq%a<9IiX!4A!8piSgsySqxenW0pMiwlK zff@40^xd}|2}OEieQ-Gyeh=>&En4KLNqZ}=QM1!SKPfs6qixZ#p?v~3jo3I%Ud@NW?;0yEGlxp0fL9a5M;57bNeW6J#+ zEhP70sy(!mrUrc&cud&3HN>tM*u3}bV)_bxN4HFnv`or z%29X2ca+vl+yK^zuov=<4nuGWXrx(%j3T^8mjFdt?^x}x%TAvuJPE6 zt2XJXooa^Blyt6~r>DR&KH>29o|UVvNz|-QRc&}UyfL@A4c7jQvo7hZOF36vv(MCT zN!D*k)jxK7FZ`5R?VoZ>Y|TmM`lNH!c*o?jYYynQ?qv+ym@QsDQ(T`cu1^$ixV1E~ z<@mh8wWMlp^_tmb^*2^r>!0!MN&5EOKAZGCnOgG{mVfHu`-bex>Sva&OD(wJ4ul3D(8ssy8#)Eg9hvtn)OE3ujU_c)M23a`BqlZ7cw~1TU2&-U8)ko#a zV2duP`t_$)6e1GO`RmeU2bwM4uiMplV6))|^^FHDrVlpT4jRorH1G&B^c&LVn6`Ku zH-wx(I>=m=u!o0fi7GeL(&yF$R9y+LkS|51DP=Hif*;}WcB_&GOWxccK~g;g0bx9G zL`65Q8{q9>5&j)Y5aBOG7*P7&Rvbsv{igmT#nJSRooXzW%%N@U4`8Hg|=N)#`l-;2s4I`1pEq8 z^a&YOF!SGB)jtgmODisM1My}kNeHtq>6vQ$S1Kkn^M?c6UWW{<$7MZU>QdsqnYuig zd!wE#Cb`H&Zl>-fwV#>DvxQ5pR9&u`DO`~(T#+hVN&Vy--!)rQK3RC#J9&bz)N9Sj zs>Vc7W6IfxnQHUN(ER2te8wt+9c40N=kNwn<^k>HZ12O!9;#QTY!bm8MCXEM)Tz)p zOI3(nQCF}I2{Qo7rm6_XO}W%VS_9J@@^UGfxm%&WPo99v@0WQRKjyyqoV5SJCrLec zaR#ICf7khODoa{H2y$8=6W|};MrP=?3pK)+WSg>Ey9X<9aAkvFB{O#0g)y&HqKmJi z*CkJo0lNDtg0zKP9Q@dn0%;qL#ISZDMJPZaEdvSfQ4pu(#sRTcMG0lQ5KErwx_~-8 zHCM2WL`1zvqd0!f0{Cyd+Bj3$n5=9}RW@ONhV2>t(xLG^6HDe?6_YF9E}06xGw{|x zs%FE7u8p&$u?ZvazbWH8_P6ZU*55RL^W3*zeDB5E!5<8~KakpdBIP-mDt$WPe)^L< zDoFB!(t{+Wa;=_|_SZCcditaOST;@~fS9=JcA*P@9sw+o{JZ`&C4Liu9wm8;A}RP8 zJ)kKGl!V;8HZNFsb0@!GX#-F)%;%X`eOhEN`+jLPo0WP~wgCRAmrOFhL$pK&blc?} z5=Y?mGy2qT2bm~NPWsIj853TWZeKyeGQH5?x(;0|`%;+E!lSI(H~i@ z&RMISR0XmfuqZ%2U#2(nujZ$USB+a|9mTKjdwav=wxpweu59V}0YF4o%TqPSC7fuw z=6I^&c%r;L(S9P~I+?JZTV^$OfpZUR5`~x z?u3I>9(u*1p$$M7$hU!AD#CC!3t_MVVSsZxv9;0W+z11$0XzU+5Z*yP1}vNquF>tw z2)qSZV1UleVI0rXwh#lqvFT2@L75sUU`POI57}maezw{F4N638DcdW7)(l4f4C^2a9*%t`J5sTJENj=*uDe8p7YPMLRh_1bra-x|K^{CM@6 zYg=!Xe{atRj?~^0$t@>Rt505aYTg1*e&}93zZ@@yw?OF%*)T4Y4CBHtJ^hvT$;zo+ zSF2J>R^KR2H14>~CmVOCynF6A_x{F)@n`pza^*F!4GJqhDj^%Hu;SOBy2&CYviWXX z-9E43d!EL9t)}mLZTmKwzrTS;xX7LYE94i51Q(ma3e(b=#}8E@bSAC3V988cu(T|b zR%Or1B-Bh=;m=VSXvHdS#`I^+h84br=b3_>Ok2faCX;BH@k%PAcnE1AFg4@#OBY@b zOx3;9_*P@8dfgq@I@wrl29}d;tima}BFk7MRP2j28@X@UcJapdIUZp~*Q$CL>Gc`z z$7FyJYstET{0)O5EubY~ry1iOP!kwGV9LWmICRhn6k`v zpWN`~wyWE2mQ1@gC9InyMF2_EOSK$-fhfsxd{jDkM0+tuCnyxBh_Oeu7vVLjiQxy> ziw}FBtPm*g*V&7wAK6~S83G>Qzwk#J63Ap^=Au6)IZk>mjM-&-QIzcgq#%p&uhGAf z+4vOQsTQMfi++BGg8zsh!%nQF*nJfI1%jM*qP!@dIUO`(@kJts05U7a;#_%+JR8c_ zk)e2!zjR=H-$XMRiZ!pFke^kp`_R=qTY64!C^laUee>yWf8o6^+}`_xqwgP0ZG0-_ zX-}1&NVrdsp?ETfq3BYo_1uFkMBy)~ec2}8f1*eVwowD9AK1e9^RzJjG{4|n3xltN zx4_`Tww}$u#I_#g{gjCstX}e%^9$7P+Gd86KuMI{_#tzU9sCib-S|fQvTQ}#po8th z&kH)S6CVrYK^#OAvSSCny7#xkF=F%PvkgFSji zwvD=2&%qr*-Y=N0+`Di8u9HVjbne>S{=|`!C-!xoc;d*ur*<9Nv#+IM8viGxq;5@i@<#J|x${KCtaH5u$HC-c$_M|I|gK95^yCZz^|W^9Stt8H1z z4mViO`l?^yJ!iAk;m)_jA`w=?M{M zI|R+6&@ma_i1ub*(t}8{W3Lb_tQ|H9Se|infalI3ajV2SUt=oqSAm>wn!LDr&$V4C%I? z?uWB`XxLAiP8&R3UCq9Ap6|ZrX%01R>guA1`t@r!obgiJM#Opk+xQgjn_Jg?_hwg@ z<_+Kb>+7G~Iof@E2Vry^xAV1~X+Cn_=xYuQIq_Sc?Y?oxjvfC@N)i?7bxL3&(*aH) zQXngxZ5n1)1>8!omAFT;8J@?Zv`uXze7Oevy=nW?wCPm8tvu~M7mATfy_z&bvtT|b zOJvQ0?KxIakv}HR`_Fg{_QsZQ`DGKaD`S_(-VRPX*UptxPsZLHyE=BG_b>C3z8%vg zJLg=*uZ>lNOz9EANYBZ-2LRw{7n_?tAMTd$$|E zw~a@bTl@PeNS{zRc#ZJbHf7LFS{1*77ohQ_Y7LW6kTq5NR4DyjA)!)8BD%RD*^c9=cJ}u zYi$3FTPacdt05Y>>!b9C@>gg;Iu#Mn${FT`Y6uO|^4az9cWM2ql#BBOw7e>FQ%b}6 zV=8L%i1nnFe@>n_uSbSZF4=)u7l|MsT_#u{FGexaPkGE7!*Q{B8~(L_k`I-eVNf_y6X{9qbd9XLDZSLMUEpM zutciK=3FG6%c)8`v||`6UP7sE+R+lPN@iN`C^E908!-Z+KEWJhX=#-y<-~NcWqw3V z=SiE~aiS1*7^ThZ$PGHv1YN?MWc5rG$9swFqz>g+73uH>Fw)F}RpeCPBEI?<-h_?4 zP`X#V9-DU6&6X{lTT*`Ixy#R8>Ac)I^-O9>W5U%qE+CjItC?CcB@i%?DqELuuY+gz zVmpp{dp-5`PTc%Xpmi@VdAQ5q`d*a#AaH$EmH^3Yw+gDiPc0{VU*2L}y7gKOc>jgV z7p9u8yqGNA`YYjmJl%f+PCCsRxI{>FlY+q7tV-Nusie zGW|UT2?UCPb%-K{wwPZY>5n<+$M?{6pp16(fAvEC?Xoj?m z1U(Xuvaj5Fsxn7u1A(kX7ig=|L0|yTCsoJXmw9bPWaoEujz zl2)EwVf=mxwPK?90r`+i6@glIrA>Mcl?%U61j{IZB~lo2wBH;z{zmOrmp(GuZ&BWa z{?nEs(6>rWAv*F#zb}gpV*08AnM0q}DbXkB43|K32nWExl{*BXL8nGwKdN%jqgD>o zZNCR6wn?Wvs__k~$ibl!*yKl=I*y+>-v^L|DuUBta2}m1eo)H!zHp$=b6$BV&cf=2 z5v>-Jp|gQ7C9y;fue2!_1B!Ej;=KM4B*1Y5yQK4ip&mc61u|`9o{)CHzpqn%FuFE_ z^{H*pmK1e~=ZR-fX9J8Wub9lAXihj=u5CuZrjoQ0u_{ML!7qMwjJh)t>SdjUbJcQn zsG9DW{}_B57k4dr*e@>WVd_fgt>r6t(RI84(yC9er@>DFpjpGE%PSa-cJhfLfY1M;L?GK;7sYpWa&l} zT5bQ>>6$QI$-kVRa#nw0HdHT1C8(h4e{nTXLw9ik#?tC7DOX#<+9naob7;M^7sV-b z(Y>F7ZVCbv&_JY|AG3cGH5X#@xEFpI>h-oyS@}9j$ZMod1cT!Ti{Qk+T%A zJE;X!C{s2R3@bIYiSAbXM@zMShGKSY2|Ypp!*t>)T5!wYF;G80S^2tU&i%9an~; zSs8jpC=+2}#t1elyo3dO=~r$^%g*^A2}w>UmSe(yLHpSVu1_1AL+gdV#c$#75P(>i z(oY;LZDug5bU>S(okSo-Fuq5eO<`&*wTbq4r}My)IqsCS%oe=Tce(Fv^R#2tTv5eLQC+gAZtBE4r`|f1D)L?0bGNAUO8w>f@jV~o3^!fU z;>wxgmC53jaO&!Nt1ngDbZPHh=y*uG=eeL&du9#l__+OUY zUh)^!A1;3!$6-yjPuea$KAT^d$gi5MsG1B++Q%QKH_6b*)H72DrG4O%dME9Uv*u(o zM7NCB=vWH@=h75kO?NaMS%BluIp)Jl``eEHw5I-AS*oTPoZ}WJI7Z}+V8HG9?~Y@std*ZJ0~5BKAk(vXyvskacCI3Z+!kQ2hD zqY)gIhLg*qLpTbQdI(4h^i`a6N2)6w!!0&=LcU&~^gud*nH4T#2~lc@(#l5GOl4~% zRhj9%bg-z>q&~cvwae%6JP|efMAECLm2cZ>sQ(%iZ3MKboOun>bcVVA)GZM&{>-}d)~8uNFdu| zASwXOSG3SvuL!}qK!M#j-;UnJDmWwp5W}bj!y<-Nz>(@?&IJ6hJy0nOGET~g04`J4 z7+9~Kj|ykuE`;5_%tk^9tT3F+J|rFaj%s0Dfh{Ny@$^QcL6%bMd(cpj($I}*I15`k@GsYimQuA84PM`$tUP(xz(Q!)Fsu;|jX=g&$wL^33OLp;hF+!? z;2hlmj@0ZQk@><2RIICu(c5rWmxrCG;DM@2?$kKQoIH*(yX5Jjt?ba@p)~9K9&scR z^AF37^ivO@bt;RXAIecb+4M{D-y``0&=I6K^jAK!jul2$$|7zd)e}U&-m50k2%Op? z;`~gxPT)Wy=$H`;4cNmNYj)kVdDXx;74fSLUfm(XR0n#r$(yStU0oox!4Y&QJL*Er zE?#afOGI518Kma^%dQphxyM%{#8{hWZTsPa$JLIZ-ir+SSPkl*1EXL~Iqe1W)kn8b z^aez0Cn)P5ORYTZ^?DxoB5eeAfx@V({h+Cg9`N{k@sSG=BkYlm0r?&F$OG&3n{+^P zMh7rP`oMCv_C7%(X)#^VoK(-T#!Qwunr@y&LZP6{8F2n?1fS#}ey5={m@^P-bQ-9{ z9274X$xD(BB=Zk|5kU@*_~i4&a2~c)VpkWy5RqsPqrtQT&rl?HC3PD$)DwdDhjENq zKMtBww``1f*041SaSb~QrE7CzpFJpr!3&rV59vEr5>v%Ujz@avD-@Ez1Tx2DEJH$}_61N`ChV0& zXfV@EmD+`ZT4B_3sb3nkJJEgBgp>g~H&(V7NQ0xMpdiZVsf8Lx-vz4mdF832_?M`?KoikdaPBiSK} zG$tvsI5mxD|29xowkGm<+0epV*;rc+_OKz8K=%=9NjLFv8d~DiU0rx5O+#vCHanyQ z3bC-lD3RK?YIk}_W$f=~R4xk#Xh=a=PI_CEso5mOq{*e0Co{WT<$|VF4#xPih|l3L zF0IrGy8Tdu(IYEQVmYwra0(b6QFXP|hQ)M1HO}lF;`*#4rJsm^GWtm4zKd!i%?s2) zS~`(hsPK21fH?hr5zRn=VZ>W=Qw`G!GDbf~a%LjY_&2val-_b?l__ph&O+GhSu@tG z!O`@MXgfRohYrkE<<+w?gJA5Grl@YRV5Z6v9mJNEzk$9@MEU>|rR*)5MuPFBFPM=$ z$dM7^OIkM~#z{=62jmAc2_FFhbC8N+$2>^%IBjSZe!B_E>coc|1+B_mIcwA#Qf zB8lgyum;_&zP(sv8`V0aSm=?1|-fsJU~=0OsI;>A8%mt?i`_X;7+9dRHEvx77#$)v`s zAT>z=f%-?H33N*YrhJ+%0B3VaLN#J`(0QspfD@#~)ca8aR_% zSDQ3wiaMpRUwK3|CM@w6&v~EVi}{q*TMPmzDFR;GE!{{gQWc61C?OwvrTJSr_WOW9yr$Yn|O zMh?FRb432kpfvMpy{IK|A4xd}*uoZ;NySo|r>Q}5d1yXqGztYO+zE_I(K!{%3WExQ z!J9L{qV5s7AX#jo3P%YffxX3ck3*qu^Rx|)w9(PeZ5d+?M^l!UXaE#DIFd~SG6jHF z_~qMHHvTkcmEqS&O0I|e(KnPyBdzu~Rh;RC5@tw(aA@GcN}^ntOgvJjQ&}WIbIQ$! zQ-@p}>L97H*@PmIV-`*LHKL$b%ZjI56k{<|f4Od?5|S>GK@k{?cIi`3S-f2pELz0? zhSG=jfDMl5#!DI(NE0?YyK;@LTtu4~3reUr%hRS-rHMY(CD5#B${1z+@WUk)RK8_5T~JEa%mEhiC&<^2)uk3z_SL(YXEwTF8t^4wNXEv z*bRv;4s|o65NAEGVP4cnU0q6(K^?4+p9=zVCSJ!$uIZ zl)gvQ*0tzWi-$u$dhEksnVXIz9!~r_Lz5!zt)IP*$h;rKtTk>XYmYj`yusJN7p^~&ePkaZ014eD;wguJ- zd@G850*eG)ANvXbLD!xKvkL(#6Ta0e`iLPwaZsq-LoA>AiEupy`qT3@`XXNg8N>)N z?@k^$-YBUzs*qM-CZ_noXl95M9e@_;Rls&22%+Hyh1rs3EO#=NJC3ps0E?cjeX-b} z*w)n43mHAs?L*s}{9+W=et)P5{=^dRS|Q-mwCwYE>^;E4{IqHjX~YW~VNr{XXV250 z?OmPfD;QjlQBxzj)Iy+MvG3tN;@eRwMwXS6V`xuC*y2X_P`E$Vh|yq{8q5M&0;vp6 zArlFrqHZJiVM0L_GZQpN030)eMsfBV^*r=%pJzXA4Z5MuV`!YtoOA@twny!H3r2}-TddG_*PjME1JNy@IeN^E8QU| zNcehJ1Cw(wjoZ4^qp`a%Ih4*9=(}hV*51{%om4jb*(Uwns_GV75u<40M1cwm0%A;9 zAKQY1R}%k)tv-aohbWY!wY;XOYZ)pJMk9S~k`nqC%D4kr*0mL-Z`Zi7hu2Lr;7R$gfBm znAQeVkUq4QQ9)IJ(LfQ{jaJdOws*s3qJ<1VsTx4ZdV)&Ht`WK)3=I>Na95>vvT1|? zk)eAeQ3NA#MS7K|FATjz2=rljp~nN=yH7ukN=cIJBz?OlJWT45Og;D{cuqZ6lAzLY zLNqF9beB30bCB(3U$gQmrNhu7MtjtS8fA7#OYRWs>iRs@ip+FL{|C`4B`Nxy@~v`e zU^Z@`G5$rh+T;Tmr)AjmrBP2~USUFKHckeuM`0>3 zMNlRkfbk^AwEEW{pU$hB^E6&E&e@754!qtwZCf>0 zu#|q5PnJ*H@RhXMsn%3+!?<;>pn{?+CtIg&b#nzCCCiyzd%CmJYfk6kNK;5pK^kD{XX!-IPjH9L9W*xVlr4Fy0~=BbN>M%eyJH_)T@%*H z#+0=lU;nkW&^CnOx0j@xt3Ng3=f~KB(E4`!jg4sa6y}FjLINnDW&g`3QH(jhu`? zN&7+`Y~BHT9FZ6GXtLdG@^WcpUfj#prD%igj}`QheDl8C)cMO%co)q z$J&Hx?cx@jkdn3dpKyZ~Bb2UO-l4e;9tX+DJwpd}z0{7~Osc=AQWl+QsH7735rb}v z-RJCn5}3=o-kmL|ekxLQYVko+(5#T9P>4FHo85BN0eh+p*lsq?I#t=ji?2`x+475m zJ^a5IN2solPr`ZnBr5@#(zvp{UB~Ei9IOKH!Tb!>P8Tb`;R^zH5`6a^v&Aa>2+xEW z3T7$zG6hdjK)X4FUr_KB1lSosr7YS6CT&e3GFnBXLcZA2H)V@nJA3KuYtc*5$=+1K zs*Ag4^K2Jiyz9tkA4U+T;a*-`HkS`>Gx^k;9alT1^Xutz@rUN}OD5VU*G}iJy0QA^ z%EXpq(`%lXEnae^?Q+{>;LAG_&e~arixD2q9IC|I(iUlMuw#EltJ@)AAo4=bNgrr} z*nuf72d7W7Glb~lN-_3Pqfxh-Tq@+WbiUC4q?RtHfZaVyckDX|&)^Oov5cReqFDB5 zjTB3aMfkNEOV5#(_1fpejut4y5@+M+T~6Pd?HQtzHZXkXSh%$ZV3*S(IjN2Q?tmnK zn9w3NDbO^beH#I6i0;>Ip3VaoAOu0$RL#!YpdrVH8b|0@M`^E_gG%@l3fQRE(;c-+ zI>w1fzPIQnlYAuOBnMX#Rgy?#41=g3%a#7Vb*r?aaJyi{#HP6|{%-`>Y|QgN<7$7# zt@;VK;wN0)Pq><&a7%y8IarwgW6u5)uJk9I2fM~hg+DP>&0EU&vf0A2c_aSbEm}8k z#$OzHRIzM6kM68oN!`4S?(7_nTy@Z$lkW2Au7I<+7F@XdWm_wMnE!;M@Y9_JD_?T2 zG{p1xlE#{Q9J{%9+<21bpW!DC-Q(EJy+Ol%o-&Aq1#PZ;>eCfe?C^!HxS lRK_<=9J$Bg&%F(PBfoaxK%SYmEV#{l!##Ha?`HJ+e*wz9k)Hqn literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/raises.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/raises.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c9803da8a995336476264060b7ed827fdba68410 GIT binary patch literal 60047 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
60q~MEoKSAfgk!5Q~`_soGu1YnWS0*6@h7 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 Gb^ikmz%3L2 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/reports.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/reports.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..053708815093a34a3703900138c96a1caa563ca0 GIT binary patch literal 27013 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/runner.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/runner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..427d0a327b2c734319770a71973fededfddfb1df GIT binary patch literal 24219 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/setuponly.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/setuponly.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0433ac88b0ece1c268835509dd4b44ca3adc9d9 GIT binary patch literal 5369 zcmeHLO>7&-6`tktXZcS_q!i~zT3eE3>W{Y5#C9qxiEKH}kK{CV>Xw1cuDC0T^peZW zE+dJh3fVAV8Axlj2x0|pP!%Z91Wu6@J=DjbKu*1g7E>}?8&J^%Xl`uO0;$nM-^}h( zG9l;ELoXePGjHB|^Z(|1Z}yLVzXw73V*j3W!iUgbNySR6K`@&47)%f?wsv$8$wh&w24mz`Nx+(l^z&|I9Ov{QCxJ#i1EU9vapi~A_e0qu|bDeaa6 z*9HoZRf@h3{5N5CB)PZ!iD5Bt*?!Xypm^JTvWOUj zKEvC2gcjSy5bz`4Fjk8-me?V-ZtK5&-V=T3OEOxN(Y-=0r)YwfR&pv--Ooum2@9Hn zp~9)cw1lNRp`FK++;locxZ`ta?U*7;g!{69Rf)(wC(?6T0ZYfFX;M2~n0iHey&$O? z)EuXiN?w{wnoFnabV3eC#%vaWxQh74k|>o-g{$$`GmP9u>AlN@%zs z6{Dws!&6Ou+E}?d$cqXOo5@RaQnH{)yavlxqc+_sB+35j_8CdeH%3rrl-aEjXl?K` z@XTJ7=|;!=>`Ypg%x=@URHI`qC21+F6!N-T5Je?VMj_+n>8a=rDpPH+F+6|od~8O^ zO0iUKE_Pf}FK9|Wc2dzKd1y>gEguVLiFIH9;&{#O zzH;i~sTvolaqYF>BQ^iwx*Ivd|3D6hv+hFglto0!(0fcqk1z`WqTpto(;i4j>^+se%L)+Rq=n$tElgny!uzG*5kV4P+6|Fd>As|poRP$Y(RVmOnkP(vOBgT~Wp$6q5v6I}g$YDuLHjfR61fczMoL<&3?G>{=zzc^c}~B#i_=|%J9!01Ky+EznFMu zVtKsQA6xcRLVb69{OZH+o&E6q`{!?+{nR&J--UbwUoRc{ACCTS^)c9#OU!tbxy+6| ziLQ*rfc(RgWBbSVvY+f_p>iL}IY1kBo7wyb2 z8h#YGnCuDeBpHdPFtHm$oyEO`-b*r~dB5d7)?|5kMb*HJbAp^MN*E77gC|I%S0MxY zb0V&_>gp=Hy4G9+wU&pjcmA|<$yxKZTswO8Xw|#tws+5Vi-}i!Cy8AaS4S(sgSBvH zIXqBjQRwly1Npik;|6gD^qe&J1W1I4=b-0BayYLY5p~B9XDbL|ODqnR*bF)QresBr zH2i6JsVyZ-zF;reck(oroIzXXr}?e+TEm-IzQ-yvEtbw!2|lTHt7cJFivGw&p}%Ddw$(eG0tGT4TbA*fRso`fy?<3*;KCLMwR(cq*WND6uKV#o?P2r6;W25y54TeOWFzQ^$R5uopTGLYN!I6=HYAdI-K zgw8>HL>>nsx^M_CH1npr#JtU1hgaX50Xv#Eyrqre#N_)3f@9n8ew`I?$_$ICr@H4J zNx{2kE{{ixeLMNQ;SpH@uc?j>Hx$1ElTgVmr!MSvWMU)6Lf)<<4?D_M?A{7WT) z)Fn@t`LQd^6vO;J>zTk8R7re_|KUQkXdC1yn%!ew`?4R@Nkp3!=2QTwd(63LhwjhI zg;Y9cZjtVWc;DdY?P@{IL$sbq$;y-qSui7?T zZX15DSdBeXjy<#9_Uz(IMDOIqlS^lpOJ#p|#on_Kdbk?m%OQTXr5x(6hN9(AbUie% z_`IpP%r1{D%`LyavTMb=dSEqP?LAcPJ+#(yc+G#LVt;DG50`rn zS0WSZ!HFgNXWn4V7r1uv>dCdpKs9o(969*Ww|;-*w?}TB`Q6j&kz*C#@y}g>rQ*tH z#noTqJlD9Z-1339zPFO8c0N(=eBu_aM8?*_$38x=)-qn_cKP{32vJt7@2z zVrbfp*oeyM*pYNj<4e5tVFpM#`3~KaNK6+9$R-k)z^5KeDzXgIC!c$Ug{dUA6K>di z&<`il%CPm(2a}7e2P~2Cy1;Z$5?|=M?w1S$CXnwA@Fl_6$dDzsfDe-}5|dTyeoIC# zwt+-W<0$D@1D%^8A0@JRnLZb2z-C-=a<+Is$q4ou=*^8}6|o>oqj(B<#5&YZAgi+s z!`wxCzCgWSpy*w+`!4Ffiw5taKKkGFXXM{>`Wa`PN4A#5_A6Z%yH>&#rn7EqWlk`) z(4IO=iJ?OVvEk_X%0W1rEV6YOTJ?U88KkPDB$`leINX~KDC_NyFnv@=%E5zmmXt?E dXi1fzULV;Sy{E!-ePv^r5E+60JE7<@{{u9EhYtV% literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/setupplan.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/setupplan.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1fef7454aea554162867e547416064eed702c35 GIT binary patch literal 1914 zcma)6%}*Og6rWwMch_IoB?O8}!(tGwlNk3)>ZR==3H>+(qLP!VcC~oMU#xfC+1-GV zi9m`3IYsIvIrf-}`~m$RdVvH9Yb&HkRSvzmLA~VEH?wPFXe)K>o%y|a@6EjV&71dY zDiuf2UcH~K|0E&wI}`dX5&+@!0Kj8}5f)6;6iR}?uxN_SNGZZ`1h7<+IF`(4Q!dFI zM@^-vmQ;>qz_C({W5tY@;y8uX2T~~^A`)-B-QVcq9~zcY z|HmJvadOCKq^;yLf3Owuf}b!f%XSU7Zd(qB@=e>S)T@Bh#f`dq)5e6&-Z7{{D4_Tk z^$pjfsDm2AT5f`X(Eb6pgpSY%2@jQg zz#@)d2}hsF`*1z138;Ly1YEk9g-a{EQ{>h@w+itR4%EO*t*U3I*hey1l2 zBQw1iijMt-!(;+iN zbC4^_@JxU^XSpI`)Pb)O=^%Zu0(hza^jcxrA}r;jz8s{AF9$c{#|C`)v9eLF5q*uc z{S=2-r^NA0Hy`oiVWMDC@f8|mx}R*e^?{GWj<0DN9fi>!3yBZibiNCn2Guxx&LVW% zs24|SdiVa${bOb7P?D-HB{PJ6H0q_?G_08o;duj| z#ihG>iLz2gC!q@|wk@+ALI<8-UZ4}8bXYkEIlB~cR&HXmZV|m{)Gay-`YbcN3f(q( zsibzdcD9a{E6b)-(8!yzvaMFzYmD?+|Zp)yc-r%Sd)@-m%*$<5KT zD)z0t+8|~3JNhnjVL3$Sm`N&_*E$+^lz7;bv2o=8lBvo~R0$ZYJA{u(9w7A74gCUq?YcjUZ9u78l)Ki&CimlOZ0~-;}Gy A4FCWD literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/skipping.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/skipping.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d157298cc3803b3dada95457e6a0d11f447be608 GIT binary patch literal 14080 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 jElr#+TT8XM+9<=E$6a literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/stash.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/stash.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..de452b594f46d7b8b9ec059c350e9d56ef5e6159 GIT binary patch literal 4377 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 STXuh81w@TEJR%&X+5R8&V5tEB literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/terminal.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/terminal.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9137a109ee6c5ee6f5627bafb450010a8748ffd GIT binary patch literal 84511 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�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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/timing.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/timing.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c5b7c75e1cedd1e20c6b49e9e7582f7f4968a1d GIT binary patch literal 4796 zcmb7IUu+b|8K2$T+gsn+K4TJaFliSX2$zs^$UjAZh!Pxwo8m-3TJ>7>yuMl6YxZ`p zGkcIbQOQ*jsqIU7gQqHKD;3K~AyTDOYSrgTeZhs+^dzKINgjAJajH~#>hGJ~v(E=@ z(v_K=o%v?woA3L5-}gKI($|+|cs_k<#M|4;*vItIe$sJATQ6#iU1pq>7}vP&YgN6Z zYt&EqsZt7W!#Ao%$xvfy^wXuZ>YKh<&6F~#pF!U$S*mZLZW~y`^4_>C8}o5s`R2K6z01&u9-W9Bz@4B`Z zL~cM5j_)_@IpNIMGKAO&36E?_D?t%UMWOAS4?S*2u8gEz4J+roV6MG{W@l${Y_`@Q z0bA5@0Jii3C{cBkItM%{>#jX|=!NGBP-Mb;GpY-mN_&+djr~S!$CqJzu2aD|7rbcR zj^;f&k=K1(1$TI5Wmr^4Wz+j!lHD)VRYZO)3Y z=?0a=_3CBBAKLoQ(YefAR?+}q{SCmjlmax21qJ-hC5&+$j!j)N+O92`Jnd$*An zcsXd-r3+7o_Dl%#&$$s?;HoLeRcrQn$A=@GK*>~HN5YS4TN2}S5&JuwwVfD$vgQQv zI2{Z%Y5*v=XBzhDx{Tn(X&Lduhso@R4<}kq7nSf9+ft~d7}M5wpmUi;NiZ_j4!$#N zNt+Tqm|bG)XxW$FADaiR$L4}J$HrZGE(&X7C&S3~pL{tC3BF@5JC$>8z{k$J!TB-Y zn;8SN=ELBb;-ROf&AdDblHoDD@mypJNuX~Gp%al0)U!aY?|M26qcw*TO>JELjkCw?c8)3*(t>tw^&z)Z zDI!$7DO?EhL@QN~D&m{yby~o5WEtbl{dz0~8hmYW{HObF8n3Nc?2*T>B}x z@p(#r#9aK=rSv+})8@LRr=R^C&5LoI&>33-E`hYR{sLyY6FAphgQrxArJg}OH_bCV z{kG8#fs)xU3zjls^XZpt%6v8wA^DQLog_<@{@Ni>Oz?W3FkMs^7Q3;rAkS^V=tctk zfzk~q#Jl_X`gnbiWD^+(p{q5WSK^?G6{ibyW_HvG=~lvk%Y&OYsw1%HY{#ZVLN%YF zJgKj7)dnr`q$(qnK?^B$!x#kW!ridWBxf`S+Bu+@j2ywO@KuzLyd}6P5}Y@fco$GNC9?^Efq6qG?$*5#j<-Lp()e zU67>EP_BIz9ZAT2hyBwy`cZE1t*P7DZEv0WDBbs&uBVT#rjEX+jd5Rl6%}t(JPBP0v4#Hi}ChoWe($GVgaGr5D_yI5wiO?sMkn@Q@ z5It1Io4a6E81`i3RfuUO3?N~MUPOu9mh)lV=b(KToQ9-*JLo4RL3?_uYSW6WxDKbN znx?RtwqwS-all!ki@qFL=C-6xq6mw+Hx@)Ye zV9k7o7G#Grkd$ODD3Ra3n%}dM-*dy-6QAE{qYW1ICw0F1LCdPzkMV={qP9S@UC%{? zKeXJS6r0n_WN$qGW2J$W527Gny)yk(D;um}P6@gi{Q`mw5H`v~F-HNASRviYB!MDc z#CHpM9Pto|O*!O#n`UxV^^>YbKdF?gBoz@zF|$jni7mcYBj16LJb`ABeQJ$-WNlxy zhF7fND--XYxO(>e(o*S`^}_9r(TS_$?@umG-m;$W9G$v8{^8^YlYiTwKt`PN3Ga&i zw2UeIRuqTuT`reOi08yOjpiO$NDNH1dI%pedn0tK>auA7ah%4wstXkrLCs6} zC#l8};ps`2wR=0*?!E7hU)^){^!35(r++_qbNJ}$@WjgS#LeN8i>EF!y|-`P?+y{DyXevB@o)hZhD_!6Pm=Tc@AkpkicqL4;XRKE*XRJ*t+%+l?A|n~G4& zjI+2hNULXq@x}B-l}S1pKqw=@VgIdH#c!vWMIIBcLX4tQ{u0evO4GD^x~Uoe8erP! z$87Kpd$j$x;|_cH4zur>%-D9v7(wCJcPH^_4l%ti-suZ{7j1%urgyH<=Vymm?%+={Dy;tl77Jg; literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/tmpdir.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/tmpdir.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f81eefec28d9caa7608854e6ee2859c8158055c1 GIT binary patch literal 12726 zcmbVTX>c3YecuHZ2MLe>36kI`E)R)>cE+m1a zV%l{lRFYQII1|ZD(y-G^Myi#GGSiv*LqE9FnItp)0tj?~Y@``=QqR<1TG~?E)YEDD z`@h8kBt^MN*W&K~-4E~m&)<9g$EvDI4$sHKozdU+aon#dVLo<);2$2aaNKoH;)0yS zOO_ZPw*)Ob(bkwXE(8Te3o%>JhCCr=58Cmy#T;>G&{-_22v#7^9;=MIg05noJLpE9 zBUTl!4py@~CumR5!{`do-k=wBrQ||iHNhH2yFu3mYZ+Yy+86XOx*Bv{u#V9l(DlK3 z(7fc0HN^cvKcj16jq#>n6QgTEHwT*;?E~EsY+-a=tTo;iY-4nNY)ia7*v{yNm>BN} zb}-rxx-;0x=*C!AygS&<=%(1#__p9SMmNWL;=RFMMz_QQ@$JFwJZItL?Q);oFAq#w zny|d|_kMA`gM+eHYNeduknE9d(-XylH6IIif|J_b;iN6^V^8QYTbtkD4yjLSe@6)J zl=>wR-(Avx)Pe7AjNKu1-tKyzH(E-AQujMTsohi3khB%`_Q)L-T=_p~SlYIx)?R6c z)Pq|4&|8btEA`$Em@_fkNIRwNYg+723xQp8ht8&EXOoI1P9_y`R!LrnO3}o5QIq4y z4J&C;iYjtMODa)W6}2P^RMbr+B3d+=PzOJuDF=An6;33QT9{=bS#d&6$VwQ^#*h+T z3~N*P+LgGb$TBGF;Y1oA+hjBmj)8Ez5Y)hAu|6sZg;~)UYY>kTMCv zbzas&DOFZNQ3*-=kz`^rdLESX*rlj;Bq`Bsom0u=Of)`AGqt}MR!|*Ob!1A8%!H!q zY?>AYIgZKbB`u{;lj`FJ`I7vVlu_t9l}yaYX)HyA)btzjF6+NiomBmfBWRQD8*54RlIIyPk`czPVmKj*imb^A zV%~{C*3IWGnxQx^j$zq#Pw0H~f}9A^sMM9>z=G-akfwwqvR+Zj*R9cnrh7xB-a?UN z3OQ9Tnca`4XJx%+O@-NHEE-9V9lr&cDX~(dsNgx01{%xCas0!(Ag*&-QAu#zyxmk7 zJU7oTI3;eL3zbR}do&vwutzidTAIN~ibpH4iiF(nKuLYiYWDp$|AZcGW{u0jJQUdleqBF~d0}He76=6TDluYt zcGD#WEa#!7yPPtqX^%;jzmn6O30PZ@DsET4UtEE+T);IqengSOnkoG6aOWi1Rt9KC=(MoQUwBlspnlCVa3Fzuqy-p%cK zQn46M!C;@0#X|?h;X(08N>QNc#AsqRrHzQ6^MiWRCQS_iQCg|%lTR!!^)17?LP5h| z_2WmA!Ri;uU_CaTZbxTe|M7rL*@ey(VsK?Qy`G{M31Gl!sDo{wbvx`cgha1=B^*o1 z#}p;0==S6+Et;~IT2KeN6}193-5QChy4_e=)|GBE6tLa+Jt}PzTWDn;HFHfW$vAX0 z?ZjNmMj2dFcnY=EAK?WtY%s;}p4Yc@{Q9YDr!t)fa&-rD-b0JFg3G(K8#f2}`X+nKHHT;7+f?Ju-;6xzB9 z4b6{Sc5me>XRoMKMlj=&XoKd+XJEFZ00N>-dF+`s6@Zz6?`C3Mwg#8^d2YHS#^q+F zA283)!LCe~^#0>DHWk7z>~jV?u2iqJO%QqhdnL6*sskcu8JpWCMpba21|du&m3TN7 zog;sMS^5}6?t(0mECn3OASR{^;iWsNCbKo<6q+_ccPO%&j9rj*`)rs*M>&kj0gJMe zwV`T>a9mbt0Y%Z^tJaX5P~itcRE@zxTC(IM%=K?%v@fb1N4M&i@mk>S`RcFly|y>+ z>&p7NmPd2GfkkJ*?ajMev+mZMd&_cT)*Z;Zh?#P(-Fes3S=ZAaX!&POXP-Hpd*;kt z*BS8Gvh&7VCU82__F}H~OvZht;PEed3`Qwwbc{xV!WiXWL0pF(oh~^R$S6MRn8Bj% zLanuZ6l<9b@qe%r?7(L1B=NUP+Y2|xdY02lEcM0X>q$eZM#(SRZaPM@JIot!A8L z^dGQ=8?c!rC7zHkZW0Zp#dtUYyDo|6(#$a}>iV^s9Q+cFGgp;3q^!$H105|!RcXVO zr3(EebsC00x`s<1@6(KT*7y0`q&P7#uB7CNiGI=GKsGh#TNc5kNhnf@C?XP?by#91 z2@+C_o=V1sCa9(+5zUC_nJpvVJDh<0q(%8sRE0)9FG9VKU_4U*4t7o^lLMU-6HLk` zCd^I`8WURAcO0=Dg2^fP=h_tJWQLRC#b_)hMhx?5&YI#*S{@05u`z}*h~ezhJn1PG z?4SZOvD2K2n#5GsE+O2&l#)=_uiZFe3zB^n;)14KH)oJGVgefJlMf*W@YRJlMTS;g zP?AXvngLNp^pbKGEU(++GZZ5P?8<2*m6Md`oKC{vl0R#U&!Ag{+-v5|1_Y)^lrdJ# zTr{1ZRY_OVkW8g0uOKzouu*duL#ZfJ6}%R>)h%3IW8Swr>)UCh1$Wie6IV{$s9!v>;%8 zZp=jbENPleOwn5>H_wL=+SL0rb&ldHdzxi!Md8wBhG~PY;WyJ4X3v-TB|g!ul|07j z>c@Z1bj{;=QpwM~Y|(sX`Ml*4uL2>{C=O!Jbc0EizvkHQ7uWj>=V#Rjuwswd4r35H zD8PTVPPdq)<*#HTAG&N#FWV}(dE4eICE0H~%6e04H+wP1x@@1fzZF624zr~EE!A<% zJEl9!`6e}QXNKORwV63m#k>uszH(iZ;F#_<%gbNMHHX!a+^mPLva2vn-sFa7-iGlv z>!XVLDE~U}UhjscNM3d#fBWP4QuPdKD;CLp*{Kbf<&p=ZIW_V&*aKekno$FB@q*)d zaHY92?kElgj9nTMQI_PKFf7+8oBK3=|-eX7H!j=E=4exZNJ+Nzs`O0 z;YIF}^>ywdAE=#s3y21kC^Ii)0XDOc{e~tdTMorJc-< zg_1LKHRV#XK_giMQsV0o(6l%=V)~Wnw`}_(#h{9C#6c0rpJCR>-PC}fhR=tiiNQ~v zp$XG=ML+!QZ-1+h$jmv4gCiVznTp5p(uEmWp4Hs|sAfW1G%hDo8k_W-o2(Ko4KpxJ z4F6!LiEd{ zhprsTxV9F2{_BUX9m=>n9=JStS9{jgo^kaR{4IHZU)JBZI0~2e>YG>IT=9tay!A^{ z-|`gPbyvr(j4j(1$5!0i3jX%IzbEVO$@v3cAN^;4)AhM)bJxFm?W>t>2Xp>Ii=+4a zEjLE*wjR9g%=bQ>?R`4eOZj>K@vQ%N&OeF@zLs}K?)tVbH!U7rwR7mNp(oqWb94Kh z@jJ&eXwYzA@x-ddR<*z2^9aub4@YF88S!mz({rUIi zzw^~4cfr@5_ifAiw%zQy>l?V&+)-%Wd1vpPz$(Wd*ojxu&WBcOOT#K>t#5#xukXm$ z^=9jOZ$6c+8@_Yqu5Zu1x`ykc*G6x=x*W@I8_8}P`QVl8wj(QbN3mRaUvJjeyLhzV z@#Q^Rv!1Ot8}53B3jW3=;hz7gf?q7uw-)M~?zII9^%Q>Oif})mrTCKXmwx@8f>DZ{S3)^=Cai-U=z)fZMf^1JHwC0p06jJu1 zL){gLg;f=((G=uxltyHJCpnDF`Mkw3C(RHVnmhXev<~VAc>M|Ysoi38f+-q0a)S84 z?fK)A1y6HMXf8A~<%Fh!zdI*%uX?$Tt*ce_woXtj;-@vB9yE5X+9=1)HMgxgh;ni* zy{i>OxxuJr{kq0gs=r#xRaQUr5jDu!YgQ|%qlz|LM^(x}S!L5_E>I7x zRMkS%A;H%8$lYM8c{IG=)@b}F`aKc+l^$AG=6+6qxK5`UbTG^K2+y{asWT|0(%3P=*?x;FMmnDS zat->$b1C>%I3 z4{KZ@&lV?D8>=73A*DkJM^(6WV^DUDRLun_!8}~m1sv8TnT}!F2(AJEZB0`GR;E&Q z3(n8pMk|%JANHInCf#c|Lgk;QETT9^^`65ExN9|6S^tf;g=Y&wWnO5=3Jpuj^^4ao zt_bb-8hi4MyRwbD?i~M6$~5lEHJ;1}^##HHwddb@e(^%azdh4`@Wa%?^B)PP9uHVc z8iRbVCqZ4LMI`HO!Gx3GSZ@l6NjAwYIo8%o1J52)=!A7{ax{67U?XglvF56|V;5zS zd_Kko6XudnlmG}oApPqP5lWTToCX*e_r%~Qw6Fn-E=S=}MYli^fMQx4G-E*wb$ z!A!WgIfVPX)&V^zQ{X z4jJbFLv#!fP$-fE=hzwM6eSAa7_{LUrBq@8)F}e|SJCPL{!{|87dY(v*G|24s^IJ_ zRHX{unpFp9t9`@?HapCX&1IBzty=i%)(77D)e4mUJ7=@m*`5<0GDgSXHZ_uj*saxA zL^F*=F7uOMi_M@3;LZHpiJ}zs6$M!N7Fzrp{?t0WpvRq@+ef}`-qn|N^<{)UgXfD{5p~wU zyGuy?KED>A1<4D0rc1im%%NjH$ue&-rQ|YX2EeNYC)3tuj_KnPcw25$)Wya5VLk0* zbHbOt{OTJzANphq>fxS;Lar1HAFe?l=n%*=(k+UtTh-J#hK<1?dlyx31;aRVQ{JPL z{nX*tUOD{2dMr(MgyIOSpoabeb^Zf?>T$flU#{A#2d*5*yIQlZ)`g?@Tvb>1T-meK zb7R-CkaKNaI9jOkEF3SmJWGzHhQ)(OqX8EBM)%FS#X~vQU`7~xe60oa-9_j3{|(|K z$FH-8bU?YrURl1)U*{P-wRX4AjvfYKQ*Rrd&Gw<}w=|(P0X`DXV2Sp5VKbXZ+RdDY zQdNdcus5XjinC!_@Asa;K`w3kkrl9kJ^VDptRyNFc5V!?dvSlGBto#hI8h%YD?CxO zu>oev4AWT#vtim~GR*7lCPCCL6t8#`ZQ2updh}zeK8zj)@_o)!i-zS)PBO*(But{; zFz|*ifCF8!*N7#^_$CvZ+)9D7_y4?=qL4J`LImn{{1&fQMbG2NN)E|%Dyoj*FpC)4 zxEgVTYTBS{)G56dyiVXVY=sI-5W+1a0}CJl2J%3H!?XxH2UAW^Q&+LB6pd&bk4>GX z3Q~TEs{eyObr!FG;eNwzDY$)0do!*5IoALrq_X<$=Mf-OZ^>5;WvhlDKDAB8$z^x8 zwtHpk-fZo@g_92)RZER`9Zff8zMp(Ad2{lPw6f*tM|Q5lv#2fa{>a(;!0EZ_zT(b1 zo3qa5m6n05bMU`Zn*Q5|M-Ct0H$UXW^~c{+xy@TPb3f_k!UCV>QA$H@cpzL*dQ7+m zaT~zOr~vAgLWgpLg*M=Eo4BN=Tf9#6tjt9%i{2NvbZdnDum>kvMbiey7V{3+=avpn z;(nbWB}HS?4<;oy(KvuAaXf`8;4jAUBu^@iMd&h>!N}zF8h!@3xSfWL?}T$gP#4Nf zaLR~|*TTzv6_9?Xb3N0U) zmMoWE39nislqjVd49up7y*ako5!>Mhgy4#^EnGs~=RgnaupaS-TYeIsl^DrK!8_Ed_UL-rb*d z_h;O@AGmy1pSkkPimMfTT;*HZcXRhg?qP_6)BCkI-+FV!(Rk0d`$9o=PhSB()tD>1IvAJN=}g5N6e)B6>8{}hzv~clzvV? z&;?wXiYv=hYE=M&+F&EJ^alIgRNPAick95RA-e5}$_Rgzzd=j7wxpf}!4%SGPR`|B zI8ty`U)^_QUj|^G0KBig(AK_itZ?|~&zk?fdEqDp3VER^D>SV%_urIr!tOg~|Mswrmm?z?B$CH&Riufq!lj+!uepl%g`bQ?`RkvxqpU_LT;G>`V@1SzV(-K zJE*SR(84VDyHrHEYoMaF<=D9#xb%PXT11I)w$fQd>27X6Sq2iKBZ zU{yMj!=28eLtl)NRfW#y6xuG`XO`h&DoiWp5}7jM;wrO*yjGR1JV3l+PX@=TN;CCQ zs>v+2LWgs@K(^OF7#a9TBBJzCNkwUe4K|si5{j199=rkY@cchDi5>m6RGLM4l%z`OH&LzZv&Px^lv8dq_(nQsIdXz|m_u`00ioKV5n zJ#4Jx$M}cMZr=Xs7S2+!m|o$VKC@W)eyZBM$|$#u9|Cr1saUnt^FxKImQ^c#hYqpt z16$o^Hl(S6QRCro1K<1UPR>%f)V*A{!f#u(ICu~InN=&k4{ZIP+3=u D-{KB5 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/tracemalloc.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/tracemalloc.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f2fb41a4fde26e640bad20ed14866f456fc65341 GIT binary patch literal 995 zcmZuw&1(}u6o0d`*=Dyb(O8WiAd40uVzO2*rHEox(2EK^DZ;wRPV>>-oiHxv+V2XDc8ke+B+8P{X1C zUlQdvX{kWg_oA=@KKLYnkX$DOeBIaX>esa{EoQzYup)_vy(r?!Qw<);DoK4exsDuT zll7MEFM4-Oj zqYvScHUoZUN%xJO{t*13M|1`bgWBp7U>pP`a1FYo0}J#TbVxCKIR=pcHi)&j$GTE! zvZ}JZAmCL`G2iyol(l%Zu2f4-IgZb(vfSz_CRLe>n!_STDz6eSr_MVi#ht3xQf*FGPu(t%O%);buBpal6J;YN~kEIlEJ50wHx~^R%pv zC;ygKwq_W!7jyP37k0=I*@$L&xtNKEw?&PIFNG~G)CD29h{rI2n+9_?K}nuiZ}LVI z8yF|_6#5%!wIX?sSW}gFB)!6UxTw*#2xRi2c0wCBvGdRA6-4Rdhktd@=bQ~vvs3?ZlDu0x0cH==Euiy*&T)Wry1#6~*p*bodqMif)2M3Kgp z>&~{5pW?a#R|huRc@FC+g4lso4fn3RxDaOj6IMhsCv%P$)3 B5R(7^ literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/unittest.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/unittest.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1a6401861a56f7e5765a0ff94d7bb1c19466a4c GIT binary patch literal 25881 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 S)eBkzZt2~(^#m~F^nU?vme9`t literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/unraisableexception.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/unraisableexception.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f78525fb2b2631ba7ad9f147c554bda52501c97 GIT binary patch literal 6263 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 HV<-OupW<3S literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/warning_types.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/warning_types.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9494b61473a973fe150850579b043c085b4a3a9b GIT binary patch literal 6950 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/warnings.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/__pycache__/warnings.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..559ae5d1d8a537c0560473325c7a1f775b3d5470 GIT binary patch literal 6860 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_argcomplete.py b/Backend/venv/lib/python3.12/site-packages/_pytest/_argcomplete.py new file mode 100644 index 00000000..59426ef9 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/_argcomplete.py @@ -0,0 +1,117 @@ +"""Allow bash-completion for argparse with argcomplete if installed. + +Needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail +to find the magic string, so _ARGCOMPLETE env. var is never set, and +this does not need special code). + +Function try_argcomplete(parser) should be called directly before +the call to ArgumentParser.parse_args(). + +The filescompleter is what you normally would use on the positional +arguments specification, in order to get "dirname/" after "dirn" +instead of the default "dirname ": + + optparser.add_argument(Config._file_or_dir, nargs='*').completer=filescompleter + +Other, application specific, completers should go in the file +doing the add_argument calls as they need to be specified as .completer +attributes as well. (If argcomplete is not installed, the function the +attribute points to will not be used). + +SPEEDUP +======= + +The generic argcomplete script for bash-completion +(/etc/bash_completion.d/python-argcomplete.sh) +uses a python program to determine startup script generated by pip. +You can speed up completion somewhat by changing this script to include + # PYTHON_ARGCOMPLETE_OK +so the python-argcomplete-check-easy-install-script does not +need to be called to find the entry point of the code and see if that is +marked with PYTHON_ARGCOMPLETE_OK. + +INSTALL/DEBUGGING +================= + +To include this support in another application that has setup.py generated +scripts: + +- Add the line: + # PYTHON_ARGCOMPLETE_OK + near the top of the main python entry point. + +- Include in the file calling parse_args(): + from _argcomplete import try_argcomplete, filescompleter + Call try_argcomplete just before parse_args(), and optionally add + filescompleter to the positional arguments' add_argument(). + +If things do not work right away: + +- Switch on argcomplete debugging with (also helpful when doing custom + completers): + export _ARC_DEBUG=1 + +- Run: + python-argcomplete-check-easy-install-script $(which appname) + echo $? + will echo 0 if the magic line has been found, 1 if not. + +- Sometimes it helps to find early on errors using: + _ARGCOMPLETE=1 _ARC_DEBUG=1 appname + which should throw a KeyError: 'COMPLINE' (which is properly set by the + global argcomplete script). +""" + +from __future__ import annotations + +import argparse +from glob import glob +import os +import sys +from typing import Any + + +class FastFilesCompleter: + """Fast file completer class.""" + + def __init__(self, directories: bool = True) -> None: + self.directories = directories + + def __call__(self, prefix: str, **kwargs: Any) -> list[str]: + # Only called on non option completions. + if os.sep in prefix[1:]: + prefix_dir = len(os.path.dirname(prefix) + os.sep) + else: + prefix_dir = 0 + completion = [] + globbed = [] + if "*" not in prefix and "?" not in prefix: + # We are on unix, otherwise no bash. + if not prefix or prefix[-1] == os.sep: + globbed.extend(glob(prefix + ".*")) + prefix += "*" + globbed.extend(glob(prefix)) + for x in sorted(globbed): + if os.path.isdir(x): + x += "/" + # Append stripping the prefix (like bash, not like compgen). + completion.append(x[prefix_dir:]) + return completion + + +if os.environ.get("_ARGCOMPLETE"): + try: + import argcomplete.completers + except ImportError: + sys.exit(-1) + filescompleter: FastFilesCompleter | None = FastFilesCompleter() + + def try_argcomplete(parser: argparse.ArgumentParser) -> None: + argcomplete.autocomplete(parser, always_complete_options=False) + +else: + + def try_argcomplete(parser: argparse.ArgumentParser) -> None: + pass + + filescompleter = None diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_code/__init__.py b/Backend/venv/lib/python3.12/site-packages/_pytest/_code/__init__.py new file mode 100644 index 00000000..7f67a2e3 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/_code/__init__.py @@ -0,0 +1,26 @@ +"""Python inspection/code generation API.""" + +from __future__ import annotations + +from .code import Code +from .code import ExceptionInfo +from .code import filter_traceback +from .code import Frame +from .code import getfslineno +from .code import Traceback +from .code import TracebackEntry +from .source import getrawcode +from .source import Source + + +__all__ = [ + "Code", + "ExceptionInfo", + "Frame", + "Source", + "Traceback", + "TracebackEntry", + "filter_traceback", + "getfslineno", + "getrawcode", +] diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4cdfb64509760a9fe78efe65f23fbacd2f5e037f GIT binary patch literal 695 zcmYk3J#W-77{?#qOL9prR|Zrh5OBHy=_Kx4%8HOE6ww7$g2{`OCcZSf#Eu+a+hb$n z8?dwSS@;B;lnLF?4biO=kBzA6;jiEG_V*+EM;wQMXS+8kf)2nh8~i^Q+e5Ctpnio6 z7-S^tl%(pg4uPzj^~!G5V?D=x=_K-d1Irfo#<~trB57@wQfE=*EaflqU&~a2o z)sPJx50PUQ6UZTpzY#WCkET1Ho*b{usgmPDHnm7ip(IUJCdRptLMJvJzdAmeU)trT zWD_M)D&z9aCHa@@y~Y8o#fKNEsO`v+TqsQ3Srnxanj4*@;v`9znCrjQNoBjET$n{u z7E(yt8+^RgG`a~6rO|6FhuE!?bGsTw-UqePsh9@3k1J@;%D_r&#eRa0toT*}D?^m` z)18gad8Sg%HzDVXm04-Qx$a_d)3e^U{M@WFbp|)48?SMfJ4<(=HBHKr^Y3tlPH>&l zQ&kC?%M1EOG)tpu`c4^9&JL7X7BZ&?_!dHD^i0SzS{5g?cAxNk{%oH%g%PtF8egyTDbqm3!~0wU*E-vzuYpnd;kCd literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/code.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/code.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a33216c6a5a35bf4a37f9f8ab87d987bdeca35b3 GIT binary patch literal 67990 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/source.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/source.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4596af08f792f7e8fd3e94401393fb2475ee053e GIT binary patch literal 11955 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_code/code.py b/Backend/venv/lib/python3.12/site-packages/_pytest/_code/code.py new file mode 100644 index 00000000..add2a493 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/_code/code.py @@ -0,0 +1,1565 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import ast +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Mapping +from collections.abc import Sequence +import dataclasses +import inspect +from inspect import CO_VARARGS +from inspect import CO_VARKEYWORDS +from io import StringIO +import os +from pathlib import Path +import re +import sys +from traceback import extract_tb +from traceback import format_exception +from traceback import format_exception_only +from traceback import FrameSummary +from types import CodeType +from types import FrameType +from types import TracebackType +from typing import Any +from typing import ClassVar +from typing import Final +from typing import final +from typing import Generic +from typing import Literal +from typing import overload +from typing import SupportsIndex +from typing import TypeAlias +from typing import TypeVar + +import pluggy + +import _pytest +from _pytest._code.source import findsource +from _pytest._code.source import getrawcode +from _pytest._code.source import getstatementrange_ast +from _pytest._code.source import Source +from _pytest._io import TerminalWriter +from _pytest._io.saferepr import safeformat +from _pytest._io.saferepr import saferepr +from _pytest.compat import get_real_func +from _pytest.deprecated import check_ispytest +from _pytest.pathlib import absolutepath +from _pytest.pathlib import bestrelpath + + +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup + +TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] + +EXCEPTION_OR_MORE = type[BaseException] | tuple[type[BaseException], ...] + + +class Code: + """Wrapper around Python code objects.""" + + __slots__ = ("raw",) + + def __init__(self, obj: CodeType) -> None: + self.raw = obj + + @classmethod + def from_function(cls, obj: object) -> Code: + return cls(getrawcode(obj)) + + def __eq__(self, other): + return self.raw == other.raw + + # Ignore type because of https://github.com/python/mypy/issues/4266. + __hash__ = None # type: ignore + + @property + def firstlineno(self) -> int: + return self.raw.co_firstlineno - 1 + + @property + def name(self) -> str: + return self.raw.co_name + + @property + def path(self) -> Path | str: + """Return a path object pointing to source code, or an ``str`` in + case of ``OSError`` / non-existing file.""" + if not self.raw.co_filename: + return "" + try: + p = absolutepath(self.raw.co_filename) + # maybe don't try this checking + if not p.exists(): + raise OSError("path check failed.") + return p + except OSError: + # XXX maybe try harder like the weird logic + # in the standard lib [linecache.updatecache] does? + return self.raw.co_filename + + @property + def fullsource(self) -> Source | None: + """Return a _pytest._code.Source object for the full source file of the code.""" + full, _ = findsource(self.raw) + return full + + def source(self) -> Source: + """Return a _pytest._code.Source object for the code object's source only.""" + # return source only for that part of code + return Source(self.raw) + + def getargs(self, var: bool = False) -> tuple[str, ...]: + """Return a tuple with the argument names for the code object. + + If 'var' is set True also return the names of the variable and + keyword arguments when present. + """ + # Handy shortcut for getting args. + raw = self.raw + argcount = raw.co_argcount + if var: + argcount += raw.co_flags & CO_VARARGS + argcount += raw.co_flags & CO_VARKEYWORDS + return raw.co_varnames[:argcount] + + +class Frame: + """Wrapper around a Python frame holding f_locals and f_globals + in which expressions can be evaluated.""" + + __slots__ = ("raw",) + + def __init__(self, frame: FrameType) -> None: + self.raw = frame + + @property + def lineno(self) -> int: + return self.raw.f_lineno - 1 + + @property + def f_globals(self) -> dict[str, Any]: + return self.raw.f_globals + + @property + def f_locals(self) -> dict[str, Any]: + return self.raw.f_locals + + @property + def code(self) -> Code: + return Code(self.raw.f_code) + + @property + def statement(self) -> Source: + """Statement this frame is at.""" + if self.code.fullsource is None: + return Source("") + return self.code.fullsource.getstatement(self.lineno) + + def eval(self, code, **vars): + """Evaluate 'code' in the frame. + + 'vars' are optional additional local variables. + + Returns the result of the evaluation. + """ + f_locals = self.f_locals.copy() + f_locals.update(vars) + return eval(code, self.f_globals, f_locals) + + def repr(self, object: object) -> str: + """Return a 'safe' (non-recursive, one-line) string repr for 'object'.""" + return saferepr(object) + + def getargs(self, var: bool = False): + """Return a list of tuples (name, value) for all arguments. + + If 'var' is set True, also include the variable and keyword arguments + when present. + """ + retval = [] + for arg in self.code.getargs(var): + try: + retval.append((arg, self.f_locals[arg])) + except KeyError: + pass # this can occur when using Psyco + return retval + + +class TracebackEntry: + """A single entry in a Traceback.""" + + __slots__ = ("_rawentry", "_repr_style") + + def __init__( + self, + rawentry: TracebackType, + repr_style: Literal["short", "long"] | None = None, + ) -> None: + self._rawentry: Final = rawentry + self._repr_style: Final = repr_style + + def with_repr_style( + self, repr_style: Literal["short", "long"] | None + ) -> TracebackEntry: + return TracebackEntry(self._rawentry, repr_style) + + @property + def lineno(self) -> int: + return self._rawentry.tb_lineno - 1 + + def get_python_framesummary(self) -> FrameSummary: + # Python's built-in traceback module implements all the nitty gritty + # details to get column numbers of out frames. + stack_summary = extract_tb(self._rawentry, limit=1) + return stack_summary[0] + + # Column and end line numbers introduced in python 3.11 + if sys.version_info < (3, 11): + + @property + def end_lineno_relative(self) -> int | None: + return None + + @property + def colno(self) -> int | None: + return None + + @property + def end_colno(self) -> int | None: + return None + else: + + @property + def end_lineno_relative(self) -> int | None: + frame_summary = self.get_python_framesummary() + if frame_summary.end_lineno is None: # pragma: no cover + return None + return frame_summary.end_lineno - 1 - self.frame.code.firstlineno + + @property + def colno(self) -> int | None: + """Starting byte offset of the expression in the traceback entry.""" + return self.get_python_framesummary().colno + + @property + def end_colno(self) -> int | None: + """Ending byte offset of the expression in the traceback entry.""" + return self.get_python_framesummary().end_colno + + @property + def frame(self) -> Frame: + return Frame(self._rawentry.tb_frame) + + @property + def relline(self) -> int: + return self.lineno - self.frame.code.firstlineno + + def __repr__(self) -> str: + return f"" + + @property + def statement(self) -> Source: + """_pytest._code.Source object for the current statement.""" + source = self.frame.code.fullsource + assert source is not None + return source.getstatement(self.lineno) + + @property + def path(self) -> Path | str: + """Path to the source code.""" + return self.frame.code.path + + @property + def locals(self) -> dict[str, Any]: + """Locals of underlying frame.""" + return self.frame.f_locals + + def getfirstlinesource(self) -> int: + return self.frame.code.firstlineno + + def getsource( + self, astcache: dict[str | Path, ast.AST] | None = None + ) -> Source | None: + """Return failing source code.""" + # we use the passed in astcache to not reparse asttrees + # within exception info printing + source = self.frame.code.fullsource + if source is None: + return None + key = astnode = None + if astcache is not None: + key = self.frame.code.path + if key is not None: + astnode = astcache.get(key, None) + start = self.getfirstlinesource() + try: + astnode, _, end = getstatementrange_ast( + self.lineno, source, astnode=astnode + ) + except SyntaxError: + end = self.lineno + 1 + else: + if key is not None and astcache is not None: + astcache[key] = astnode + return source[start:end] + + source = property(getsource) + + def ishidden(self, excinfo: ExceptionInfo[BaseException] | None) -> bool: + """Return True if the current frame has a var __tracebackhide__ + resolving to True. + + If __tracebackhide__ is a callable, it gets called with the + ExceptionInfo instance and can decide whether to hide the traceback. + + Mostly for internal use. + """ + tbh: bool | Callable[[ExceptionInfo[BaseException] | None], bool] = False + for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals): + # in normal cases, f_locals and f_globals are dictionaries + # however via `exec(...)` / `eval(...)` they can be other types + # (even incorrect types!). + # as such, we suppress all exceptions while accessing __tracebackhide__ + try: + tbh = maybe_ns_dct["__tracebackhide__"] + except Exception: + pass + else: + break + if tbh and callable(tbh): + return tbh(excinfo) + return tbh + + def __str__(self) -> str: + name = self.frame.code.name + try: + line = str(self.statement).lstrip() + except KeyboardInterrupt: + raise + except BaseException: + line = "???" + # This output does not quite match Python's repr for traceback entries, + # but changing it to do so would break certain plugins. See + # https://github.com/pytest-dev/pytest/pull/7535/ for details. + return f" File '{self.path}':{self.lineno + 1} in {name}\n {line}\n" + + @property + def name(self) -> str: + """co_name of underlying code.""" + return self.frame.code.raw.co_name + + +class Traceback(list[TracebackEntry]): + """Traceback objects encapsulate and offer higher level access to Traceback entries.""" + + def __init__( + self, + tb: TracebackType | Iterable[TracebackEntry], + ) -> None: + """Initialize from given python traceback object and ExceptionInfo.""" + if isinstance(tb, TracebackType): + + def f(cur: TracebackType) -> Iterable[TracebackEntry]: + cur_: TracebackType | None = cur + while cur_ is not None: + yield TracebackEntry(cur_) + cur_ = cur_.tb_next + + super().__init__(f(tb)) + else: + super().__init__(tb) + + def cut( + self, + path: os.PathLike[str] | str | None = None, + lineno: int | None = None, + firstlineno: int | None = None, + excludepath: os.PathLike[str] | None = None, + ) -> Traceback: + """Return a Traceback instance wrapping part of this Traceback. + + By providing any combination of path, lineno and firstlineno, the + first frame to start the to-be-returned traceback is determined. + + This allows cutting the first part of a Traceback instance e.g. + for formatting reasons (removing some uninteresting bits that deal + with handling of the exception/traceback). + """ + path_ = None if path is None else os.fspath(path) + excludepath_ = None if excludepath is None else os.fspath(excludepath) + for x in self: + code = x.frame.code + codepath = code.path + if path is not None and str(codepath) != path_: + continue + if ( + excludepath is not None + and isinstance(codepath, Path) + and excludepath_ in (str(p) for p in codepath.parents) # type: ignore[operator] + ): + continue + if lineno is not None and x.lineno != lineno: + continue + if firstlineno is not None and x.frame.code.firstlineno != firstlineno: + continue + return Traceback(x._rawentry) + return self + + @overload + def __getitem__(self, key: SupportsIndex) -> TracebackEntry: ... + + @overload + def __getitem__(self, key: slice) -> Traceback: ... + + def __getitem__(self, key: SupportsIndex | slice) -> TracebackEntry | Traceback: + if isinstance(key, slice): + return self.__class__(super().__getitem__(key)) + else: + return super().__getitem__(key) + + def filter( + self, + excinfo_or_fn: ExceptionInfo[BaseException] | Callable[[TracebackEntry], bool], + /, + ) -> Traceback: + """Return a Traceback instance with certain items removed. + + If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s + which are hidden (see ishidden() above). + + Otherwise, the filter is a function that gets a single argument, a + ``TracebackEntry`` instance, and should return True when the item should + be added to the ``Traceback``, False when not. + """ + if isinstance(excinfo_or_fn, ExceptionInfo): + fn = lambda x: not x.ishidden(excinfo_or_fn) # noqa: E731 + else: + fn = excinfo_or_fn + return Traceback(filter(fn, self)) + + def recursionindex(self) -> int | None: + """Return the index of the frame/TracebackEntry where recursion originates if + appropriate, None if no recursion occurred.""" + cache: dict[tuple[Any, int, int], list[dict[str, Any]]] = {} + for i, entry in enumerate(self): + # id for the code.raw is needed to work around + # the strange metaprogramming in the decorator lib from pypi + # which generates code objects that have hash/value equality + # XXX needs a test + key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno + values = cache.setdefault(key, []) + # Since Python 3.13 f_locals is a proxy, freeze it. + loc = dict(entry.frame.f_locals) + if values: + for otherloc in values: + if otherloc == loc: + return i + values.append(loc) + return None + + +def stringify_exception( + exc: BaseException, include_subexception_msg: bool = True +) -> str: + try: + notes = getattr(exc, "__notes__", []) + except KeyError: + # Workaround for https://github.com/python/cpython/issues/98778 on + # some 3.10 and 3.11 patch versions. + HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ()) + if sys.version_info < (3, 12) and isinstance(exc, HTTPError): + notes = [] + else: # pragma: no cover + # exception not related to above bug, reraise + raise + if not include_subexception_msg and isinstance(exc, BaseExceptionGroup): + message = exc.message + else: + message = str(exc) + + return "\n".join( + [ + message, + *notes, + ] + ) + + +E = TypeVar("E", bound=BaseException, covariant=True) + + +@final +@dataclasses.dataclass +class ExceptionInfo(Generic[E]): + """Wraps sys.exc_info() objects and offers help for navigating the traceback.""" + + _assert_start_repr: ClassVar = "AssertionError('assert " + + _excinfo: tuple[type[E], E, TracebackType] | None + _striptext: str + _traceback: Traceback | None + + def __init__( + self, + excinfo: tuple[type[E], E, TracebackType] | None, + striptext: str = "", + traceback: Traceback | None = None, + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + self._excinfo = excinfo + self._striptext = striptext + self._traceback = traceback + + @classmethod + def from_exception( + cls, + # Ignoring error: "Cannot use a covariant type variable as a parameter". + # This is OK to ignore because this class is (conceptually) readonly. + # See https://github.com/python/mypy/issues/7049. + exception: E, # type: ignore[misc] + exprinfo: str | None = None, + ) -> ExceptionInfo[E]: + """Return an ExceptionInfo for an existing exception. + + The exception must have a non-``None`` ``__traceback__`` attribute, + otherwise this function fails with an assertion error. This means that + the exception must have been raised, or added a traceback with the + :py:meth:`~BaseException.with_traceback()` method. + + :param exprinfo: + A text string helping to determine if we should strip + ``AssertionError`` from the output. Defaults to the exception + message/``__str__()``. + + .. versionadded:: 7.4 + """ + assert exception.__traceback__, ( + "Exceptions passed to ExcInfo.from_exception(...)" + " must have a non-None __traceback__." + ) + exc_info = (type(exception), exception, exception.__traceback__) + return cls.from_exc_info(exc_info, exprinfo) + + @classmethod + def from_exc_info( + cls, + exc_info: tuple[type[E], E, TracebackType], + exprinfo: str | None = None, + ) -> ExceptionInfo[E]: + """Like :func:`from_exception`, but using old-style exc_info tuple.""" + _striptext = "" + if exprinfo is None and isinstance(exc_info[1], AssertionError): + exprinfo = getattr(exc_info[1], "msg", None) + if exprinfo is None: + exprinfo = saferepr(exc_info[1]) + if exprinfo and exprinfo.startswith(cls._assert_start_repr): + _striptext = "AssertionError: " + + return cls(exc_info, _striptext, _ispytest=True) + + @classmethod + def from_current(cls, exprinfo: str | None = None) -> ExceptionInfo[BaseException]: + """Return an ExceptionInfo matching the current traceback. + + .. warning:: + + Experimental API + + :param exprinfo: + A text string helping to determine if we should strip + ``AssertionError`` from the output. Defaults to the exception + message/``__str__()``. + """ + tup = sys.exc_info() + assert tup[0] is not None, "no current exception" + assert tup[1] is not None, "no current exception" + assert tup[2] is not None, "no current exception" + exc_info = (tup[0], tup[1], tup[2]) + return ExceptionInfo.from_exc_info(exc_info, exprinfo) + + @classmethod + def for_later(cls) -> ExceptionInfo[E]: + """Return an unfilled ExceptionInfo.""" + return cls(None, _ispytest=True) + + def fill_unfilled(self, exc_info: tuple[type[E], E, TracebackType]) -> None: + """Fill an unfilled ExceptionInfo created with ``for_later()``.""" + assert self._excinfo is None, "ExceptionInfo was already filled" + self._excinfo = exc_info + + @property + def type(self) -> type[E]: + """The exception class.""" + assert self._excinfo is not None, ( + ".type can only be used after the context manager exits" + ) + return self._excinfo[0] + + @property + def value(self) -> E: + """The exception value.""" + assert self._excinfo is not None, ( + ".value can only be used after the context manager exits" + ) + return self._excinfo[1] + + @property + def tb(self) -> TracebackType: + """The exception raw traceback.""" + assert self._excinfo is not None, ( + ".tb can only be used after the context manager exits" + ) + return self._excinfo[2] + + @property + def typename(self) -> str: + """The type name of the exception.""" + assert self._excinfo is not None, ( + ".typename can only be used after the context manager exits" + ) + return self.type.__name__ + + @property + def traceback(self) -> Traceback: + """The traceback.""" + if self._traceback is None: + self._traceback = Traceback(self.tb) + return self._traceback + + @traceback.setter + def traceback(self, value: Traceback) -> None: + self._traceback = value + + def __repr__(self) -> str: + if self._excinfo is None: + return "" + return f"<{self.__class__.__name__} {saferepr(self._excinfo[1])} tblen={len(self.traceback)}>" + + def exconly(self, tryshort: bool = False) -> str: + """Return the exception as a string. + + When 'tryshort' resolves to True, and the exception is an + AssertionError, only the actual exception part of the exception + representation is returned (so 'AssertionError: ' is removed from + the beginning). + """ + + def _get_single_subexc( + eg: BaseExceptionGroup[BaseException], + ) -> BaseException | None: + if len(eg.exceptions) != 1: + return None + if isinstance(e := eg.exceptions[0], BaseExceptionGroup): + return _get_single_subexc(e) + return e + + if ( + tryshort + and isinstance(self.value, BaseExceptionGroup) + and (subexc := _get_single_subexc(self.value)) is not None + ): + return f"{subexc!r} [single exception in {type(self.value).__name__}]" + + lines = format_exception_only(self.type, self.value) + text = "".join(lines) + text = text.rstrip() + if tryshort: + if text.startswith(self._striptext): + text = text[len(self._striptext) :] + return text + + def errisinstance(self, exc: EXCEPTION_OR_MORE) -> bool: + """Return True if the exception is an instance of exc. + + Consider using ``isinstance(excinfo.value, exc)`` instead. + """ + return isinstance(self.value, exc) + + def _getreprcrash(self) -> ReprFileLocation | None: + # Find last non-hidden traceback entry that led to the exception of the + # traceback, or None if all hidden. + for i in range(-1, -len(self.traceback) - 1, -1): + entry = self.traceback[i] + if not entry.ishidden(self): + path, lineno = entry.frame.code.raw.co_filename, entry.lineno + exconly = self.exconly(tryshort=True) + return ReprFileLocation(path, lineno + 1, exconly) + return None + + def getrepr( + self, + showlocals: bool = False, + style: TracebackStyle = "long", + abspath: bool = False, + tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True, + funcargs: bool = False, + truncate_locals: bool = True, + truncate_args: bool = True, + chain: bool = True, + ) -> ReprExceptionInfo | ExceptionChainRepr: + """Return str()able representation of this exception info. + + :param bool showlocals: + Show locals per traceback entry. + Ignored if ``style=="native"``. + + :param str style: + long|short|line|no|native|value traceback style. + + :param bool abspath: + If paths should be changed to absolute or left unchanged. + + :param tbfilter: + A filter for traceback entries. + + * If false, don't hide any entries. + * If true, hide internal entries and entries that contain a local + variable ``__tracebackhide__ = True``. + * If a callable, delegates the filtering to the callable. + + Ignored if ``style`` is ``"native"``. + + :param bool funcargs: + Show fixtures ("funcargs" for legacy purposes) per traceback entry. + + :param bool truncate_locals: + With ``showlocals==True``, make sure locals can be safely represented as strings. + + :param bool truncate_args: + With ``showargs==True``, make sure args can be safely represented as strings. + + :param bool chain: + If chained exceptions in Python 3 should be shown. + + .. versionchanged:: 3.9 + + Added the ``chain`` parameter. + """ + if style == "native": + return ReprExceptionInfo( + reprtraceback=ReprTracebackNative( + format_exception( + self.type, + self.value, + self.traceback[0]._rawentry if self.traceback else None, + ) + ), + reprcrash=self._getreprcrash(), + ) + + fmt = FormattedExcinfo( + showlocals=showlocals, + style=style, + abspath=abspath, + tbfilter=tbfilter, + funcargs=funcargs, + truncate_locals=truncate_locals, + truncate_args=truncate_args, + chain=chain, + ) + return fmt.repr_excinfo(self) + + def match(self, regexp: str | re.Pattern[str]) -> Literal[True]: + """Check whether the regular expression `regexp` matches the string + representation of the exception using :func:`python:re.search`. + + If it matches `True` is returned, otherwise an `AssertionError` is raised. + """ + __tracebackhide__ = True + value = stringify_exception(self.value) + msg = ( + f"Regex pattern did not match.\n" + f" Expected regex: {regexp!r}\n" + f" Actual message: {value!r}" + ) + if regexp == value: + msg += "\n Did you mean to `re.escape()` the regex?" + assert re.search(regexp, value), msg + # Return True to allow for "assert excinfo.match()". + return True + + def _group_contains( + self, + exc_group: BaseExceptionGroup[BaseException], + expected_exception: EXCEPTION_OR_MORE, + match: str | re.Pattern[str] | None, + target_depth: int | None = None, + current_depth: int = 1, + ) -> bool: + """Return `True` if a `BaseExceptionGroup` contains a matching exception.""" + if (target_depth is not None) and (current_depth > target_depth): + # already descended past the target depth + return False + for exc in exc_group.exceptions: + if isinstance(exc, BaseExceptionGroup): + if self._group_contains( + exc, expected_exception, match, target_depth, current_depth + 1 + ): + return True + if (target_depth is not None) and (current_depth != target_depth): + # not at the target depth, no match + continue + if not isinstance(exc, expected_exception): + continue + if match is not None: + value = stringify_exception(exc) + if not re.search(match, value): + continue + return True + return False + + def group_contains( + self, + expected_exception: EXCEPTION_OR_MORE, + *, + match: str | re.Pattern[str] | None = None, + depth: int | None = None, + ) -> bool: + """Check whether a captured exception group contains a matching exception. + + :param Type[BaseException] | Tuple[Type[BaseException]] expected_exception: + The expected exception type, or a tuple if one of multiple possible + exception types are expected. + + :param str | re.Pattern[str] | None match: + If specified, a string containing a regular expression, + or a regular expression object, that is tested against the string + representation of the exception and its `PEP-678 ` `__notes__` + using :func:`re.search`. + + To match a literal string that may contain :ref:`special characters + `, the pattern can first be escaped with :func:`re.escape`. + + :param Optional[int] depth: + If `None`, will search for a matching exception at any nesting depth. + If >= 1, will only match an exception if it's at the specified depth (depth = 1 being + the exceptions contained within the topmost exception group). + + .. versionadded:: 8.0 + + .. warning:: + This helper makes it easy to check for the presence of specific exceptions, + but it is very bad for checking that the group does *not* contain + *any other exceptions*. + You should instead consider using :class:`pytest.RaisesGroup` + + """ + msg = "Captured exception is not an instance of `BaseExceptionGroup`" + assert isinstance(self.value, BaseExceptionGroup), msg + msg = "`depth` must be >= 1 if specified" + assert (depth is None) or (depth >= 1), msg + return self._group_contains(self.value, expected_exception, match, depth) + + +# Type alias for the `tbfilter` setting: +# bool: If True, it should be filtered using Traceback.filter() +# callable: A callable that takes an ExceptionInfo and returns the filtered traceback. +TracebackFilter: TypeAlias = bool | Callable[[ExceptionInfo[BaseException]], Traceback] + + +@dataclasses.dataclass +class FormattedExcinfo: + """Presenting information about failing Functions and Generators.""" + + # for traceback entries + flow_marker: ClassVar = ">" + fail_marker: ClassVar = "E" + + showlocals: bool = False + style: TracebackStyle = "long" + abspath: bool = True + tbfilter: TracebackFilter = True + funcargs: bool = False + truncate_locals: bool = True + truncate_args: bool = True + chain: bool = True + astcache: dict[str | Path, ast.AST] = dataclasses.field( + default_factory=dict, init=False, repr=False + ) + + def _getindent(self, source: Source) -> int: + # Figure out indent for the given source. + try: + s = str(source.getstatement(len(source) - 1)) + except KeyboardInterrupt: + raise + except BaseException: + try: + s = str(source[-1]) + except KeyboardInterrupt: + raise + except BaseException: + return 0 + return 4 + (len(s) - len(s.lstrip())) + + def _getentrysource(self, entry: TracebackEntry) -> Source | None: + source = entry.getsource(self.astcache) + if source is not None: + source = source.deindent() + return source + + def repr_args(self, entry: TracebackEntry) -> ReprFuncArgs | None: + if self.funcargs: + args = [] + for argname, argvalue in entry.frame.getargs(var=True): + if self.truncate_args: + str_repr = saferepr(argvalue) + else: + str_repr = saferepr(argvalue, maxsize=None) + args.append((argname, str_repr)) + return ReprFuncArgs(args) + return None + + def get_source( + self, + source: Source | None, + line_index: int = -1, + excinfo: ExceptionInfo[BaseException] | None = None, + short: bool = False, + end_line_index: int | None = None, + colno: int | None = None, + end_colno: int | None = None, + ) -> list[str]: + """Return formatted and marked up source lines.""" + lines = [] + if source is not None and line_index < 0: + line_index += len(source) + if source is None or line_index >= len(source.lines) or line_index < 0: + # `line_index` could still be outside `range(len(source.lines))` if + # we're processing AST with pathological position attributes. + source = Source("???") + line_index = 0 + space_prefix = " " + if short: + lines.append(space_prefix + source.lines[line_index].strip()) + lines.extend( + self.get_highlight_arrows_for_line( + raw_line=source.raw_lines[line_index], + line=source.lines[line_index].strip(), + lineno=line_index, + end_lineno=end_line_index, + colno=colno, + end_colno=end_colno, + ) + ) + else: + for line in source.lines[:line_index]: + lines.append(space_prefix + line) + lines.append(self.flow_marker + " " + source.lines[line_index]) + lines.extend( + self.get_highlight_arrows_for_line( + raw_line=source.raw_lines[line_index], + line=source.lines[line_index], + lineno=line_index, + end_lineno=end_line_index, + colno=colno, + end_colno=end_colno, + ) + ) + for line in source.lines[line_index + 1 :]: + lines.append(space_prefix + line) + if excinfo is not None: + indent = 4 if short else self._getindent(source) + lines.extend(self.get_exconly(excinfo, indent=indent, markall=True)) + return lines + + def get_highlight_arrows_for_line( + self, + line: str, + raw_line: str, + lineno: int | None, + end_lineno: int | None, + colno: int | None, + end_colno: int | None, + ) -> list[str]: + """Return characters highlighting a source line. + + Example with colno and end_colno pointing to the bar expression: + "foo() + bar()" + returns " ^^^^^" + """ + if lineno != end_lineno: + # Don't handle expressions that span multiple lines. + return [] + if colno is None or end_colno is None: + # Can't do anything without column information. + return [] + + num_stripped_chars = len(raw_line) - len(line) + + start_char_offset = _byte_offset_to_character_offset(raw_line, colno) + end_char_offset = _byte_offset_to_character_offset(raw_line, end_colno) + num_carets = end_char_offset - start_char_offset + # If the highlight would span the whole line, it is redundant, don't + # show it. + if num_carets >= len(line.strip()): + return [] + + highlights = " " + highlights += " " * (start_char_offset - num_stripped_chars + 1) + highlights += "^" * num_carets + return [highlights] + + def get_exconly( + self, + excinfo: ExceptionInfo[BaseException], + indent: int = 4, + markall: bool = False, + ) -> list[str]: + lines = [] + indentstr = " " * indent + # Get the real exception information out. + exlines = excinfo.exconly(tryshort=True).split("\n") + failindent = self.fail_marker + indentstr[1:] + for line in exlines: + lines.append(failindent + line) + if not markall: + failindent = indentstr + return lines + + def repr_locals(self, locals: Mapping[str, object]) -> ReprLocals | None: + if self.showlocals: + lines = [] + keys = [loc for loc in locals if loc[0] != "@"] + keys.sort() + for name in keys: + value = locals[name] + if name == "__builtins__": + lines.append("__builtins__ = ") + else: + # This formatting could all be handled by the + # _repr() function, which is only reprlib.Repr in + # disguise, so is very configurable. + if self.truncate_locals: + str_repr = saferepr(value) + else: + str_repr = safeformat(value) + # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)): + lines.append(f"{name:<10} = {str_repr}") + # else: + # self._line("%-10s =\\" % (name,)) + # # XXX + # pprint.pprint(value, stream=self.excinfowriter) + return ReprLocals(lines) + return None + + def repr_traceback_entry( + self, + entry: TracebackEntry | None, + excinfo: ExceptionInfo[BaseException] | None = None, + ) -> ReprEntry: + lines: list[str] = [] + style = ( + entry._repr_style + if entry is not None and entry._repr_style is not None + else self.style + ) + if style in ("short", "long") and entry is not None: + source = self._getentrysource(entry) + if source is None: + source = Source("???") + line_index = 0 + end_line_index, colno, end_colno = None, None, None + else: + line_index = entry.relline + end_line_index = entry.end_lineno_relative + colno = entry.colno + end_colno = entry.end_colno + short = style == "short" + reprargs = self.repr_args(entry) if not short else None + s = self.get_source( + source=source, + line_index=line_index, + excinfo=excinfo, + short=short, + end_line_index=end_line_index, + colno=colno, + end_colno=end_colno, + ) + lines.extend(s) + if short: + message = f"in {entry.name}" + else: + message = (excinfo and excinfo.typename) or "" + entry_path = entry.path + path = self._makepath(entry_path) + reprfileloc = ReprFileLocation(path, entry.lineno + 1, message) + localsrepr = self.repr_locals(entry.locals) + return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style) + elif style == "value": + if excinfo: + lines.extend(str(excinfo.value).split("\n")) + return ReprEntry(lines, None, None, None, style) + else: + if excinfo: + lines.extend(self.get_exconly(excinfo, indent=4)) + return ReprEntry(lines, None, None, None, style) + + def _makepath(self, path: Path | str) -> str: + if not self.abspath and isinstance(path, Path): + try: + np = bestrelpath(Path.cwd(), path) + except OSError: + return str(path) + if len(np) < len(str(path)): + return np + return str(path) + + def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback: + traceback = filter_excinfo_traceback(self.tbfilter, excinfo) + + if isinstance(excinfo.value, RecursionError): + traceback, extraline = self._truncate_recursive_traceback(traceback) + else: + extraline = None + + if not traceback: + if extraline is None: + extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames." + entries = [self.repr_traceback_entry(None, excinfo)] + return ReprTraceback(entries, extraline, style=self.style) + + last = traceback[-1] + if self.style == "value": + entries = [self.repr_traceback_entry(last, excinfo)] + return ReprTraceback(entries, None, style=self.style) + + entries = [ + self.repr_traceback_entry(entry, excinfo if last == entry else None) + for entry in traceback + ] + return ReprTraceback(entries, extraline, style=self.style) + + def _truncate_recursive_traceback( + self, traceback: Traceback + ) -> tuple[Traceback, str | None]: + """Truncate the given recursive traceback trying to find the starting + point of the recursion. + + The detection is done by going through each traceback entry and + finding the point in which the locals of the frame are equal to the + locals of a previous frame (see ``recursionindex()``). + + Handle the situation where the recursion process might raise an + exception (for example comparing numpy arrays using equality raises a + TypeError), in which case we do our best to warn the user of the + error and show a limited traceback. + """ + try: + recursionindex = traceback.recursionindex() + except Exception as e: + max_frames = 10 + extraline: str | None = ( + "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n" + " The following exception happened when comparing locals in the stack frame:\n" + f" {type(e).__name__}: {e!s}\n" + f" Displaying first and last {max_frames} stack frames out of {len(traceback)}." + ) + # Type ignored because adding two instances of a List subtype + # currently incorrectly has type List instead of the subtype. + traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore + else: + if recursionindex is not None: + extraline = "!!! Recursion detected (same locals & position)" + traceback = traceback[: recursionindex + 1] + else: + extraline = None + + return traceback, extraline + + def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainRepr: + repr_chain: list[tuple[ReprTraceback, ReprFileLocation | None, str | None]] = [] + e: BaseException | None = excinfo.value + excinfo_: ExceptionInfo[BaseException] | None = excinfo + descr = None + seen: set[int] = set() + while e is not None and id(e) not in seen: + seen.add(id(e)) + + if excinfo_: + # Fall back to native traceback as a temporary workaround until + # full support for exception groups added to ExceptionInfo. + # See https://github.com/pytest-dev/pytest/issues/9159 + reprtraceback: ReprTraceback | ReprTracebackNative + if isinstance(e, BaseExceptionGroup): + # don't filter any sub-exceptions since they shouldn't have any internal frames + traceback = filter_excinfo_traceback(self.tbfilter, excinfo) + reprtraceback = ReprTracebackNative( + format_exception( + type(excinfo.value), + excinfo.value, + traceback[0]._rawentry, + ) + ) + else: + reprtraceback = self.repr_traceback(excinfo_) + reprcrash = excinfo_._getreprcrash() + else: + # Fallback to native repr if the exception doesn't have a traceback: + # ExceptionInfo objects require a full traceback to work. + reprtraceback = ReprTracebackNative(format_exception(type(e), e, None)) + reprcrash = None + repr_chain += [(reprtraceback, reprcrash, descr)] + + if e.__cause__ is not None and self.chain: + e = e.__cause__ + excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None + descr = "The above exception was the direct cause of the following exception:" + elif ( + e.__context__ is not None and not e.__suppress_context__ and self.chain + ): + e = e.__context__ + excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None + descr = "During handling of the above exception, another exception occurred:" + else: + e = None + repr_chain.reverse() + return ExceptionChainRepr(repr_chain) + + +@dataclasses.dataclass(eq=False) +class TerminalRepr: + def __str__(self) -> str: + # FYI this is called from pytest-xdist's serialization of exception + # information. + io = StringIO() + tw = TerminalWriter(file=io) + self.toterminal(tw) + return io.getvalue().strip() + + def __repr__(self) -> str: + return f"<{self.__class__} instance at {id(self):0x}>" + + def toterminal(self, tw: TerminalWriter) -> None: + raise NotImplementedError() + + +# This class is abstract -- only subclasses are instantiated. +@dataclasses.dataclass(eq=False) +class ExceptionRepr(TerminalRepr): + # Provided by subclasses. + reprtraceback: ReprTraceback + reprcrash: ReprFileLocation | None + sections: list[tuple[str, str, str]] = dataclasses.field( + init=False, default_factory=list + ) + + def addsection(self, name: str, content: str, sep: str = "-") -> None: + self.sections.append((name, content, sep)) + + def toterminal(self, tw: TerminalWriter) -> None: + for name, content, sep in self.sections: + tw.sep(sep, name) + tw.line(content) + + +@dataclasses.dataclass(eq=False) +class ExceptionChainRepr(ExceptionRepr): + chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]] + + def __init__( + self, + chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]], + ) -> None: + # reprcrash and reprtraceback of the outermost (the newest) exception + # in the chain. + super().__init__( + reprtraceback=chain[-1][0], + reprcrash=chain[-1][1], + ) + self.chain = chain + + def toterminal(self, tw: TerminalWriter) -> None: + for element in self.chain: + element[0].toterminal(tw) + if element[2] is not None: + tw.line("") + tw.line(element[2], yellow=True) + super().toterminal(tw) + + +@dataclasses.dataclass(eq=False) +class ReprExceptionInfo(ExceptionRepr): + reprtraceback: ReprTraceback + reprcrash: ReprFileLocation | None + + def toterminal(self, tw: TerminalWriter) -> None: + self.reprtraceback.toterminal(tw) + super().toterminal(tw) + + +@dataclasses.dataclass(eq=False) +class ReprTraceback(TerminalRepr): + reprentries: Sequence[ReprEntry | ReprEntryNative] + extraline: str | None + style: TracebackStyle + + entrysep: ClassVar = "_ " + + def toterminal(self, tw: TerminalWriter) -> None: + # The entries might have different styles. + for i, entry in enumerate(self.reprentries): + if entry.style == "long": + tw.line("") + entry.toterminal(tw) + if i < len(self.reprentries) - 1: + next_entry = self.reprentries[i + 1] + if entry.style == "long" or ( + entry.style == "short" and next_entry.style == "long" + ): + tw.sep(self.entrysep) + + if self.extraline: + tw.line(self.extraline) + + +class ReprTracebackNative(ReprTraceback): + def __init__(self, tblines: Sequence[str]) -> None: + self.reprentries = [ReprEntryNative(tblines)] + self.extraline = None + self.style = "native" + + +@dataclasses.dataclass(eq=False) +class ReprEntryNative(TerminalRepr): + lines: Sequence[str] + + style: ClassVar[TracebackStyle] = "native" + + def toterminal(self, tw: TerminalWriter) -> None: + tw.write("".join(self.lines)) + + +@dataclasses.dataclass(eq=False) +class ReprEntry(TerminalRepr): + lines: Sequence[str] + reprfuncargs: ReprFuncArgs | None + reprlocals: ReprLocals | None + reprfileloc: ReprFileLocation | None + style: TracebackStyle + + def _write_entry_lines(self, tw: TerminalWriter) -> None: + """Write the source code portions of a list of traceback entries with syntax highlighting. + + Usually entries are lines like these: + + " x = 1" + "> assert x == 2" + "E assert 1 == 2" + + This function takes care of rendering the "source" portions of it (the lines without + the "E" prefix) using syntax highlighting, taking care to not highlighting the ">" + character, as doing so might break line continuations. + """ + if not self.lines: + return + + if self.style == "value": + # Using tw.write instead of tw.line for testing purposes due to TWMock implementation; + # lines written with TWMock.line and TWMock._write_source cannot be distinguished + # from each other, whereas lines written with TWMock.write are marked with TWMock.WRITE + for line in self.lines: + tw.write(line) + tw.write("\n") + return + + # separate indents and source lines that are not failures: we want to + # highlight the code but not the indentation, which may contain markers + # such as "> assert 0" + fail_marker = f"{FormattedExcinfo.fail_marker} " + indent_size = len(fail_marker) + indents: list[str] = [] + source_lines: list[str] = [] + failure_lines: list[str] = [] + for index, line in enumerate(self.lines): + is_failure_line = line.startswith(fail_marker) + if is_failure_line: + # from this point on all lines are considered part of the failure + failure_lines.extend(self.lines[index:]) + break + else: + indents.append(line[:indent_size]) + source_lines.append(line[indent_size:]) + + tw._write_source(source_lines, indents) + + # failure lines are always completely red and bold + for line in failure_lines: + tw.line(line, bold=True, red=True) + + def toterminal(self, tw: TerminalWriter) -> None: + if self.style == "short": + if self.reprfileloc: + self.reprfileloc.toterminal(tw) + self._write_entry_lines(tw) + if self.reprlocals: + self.reprlocals.toterminal(tw, indent=" " * 8) + return + + if self.reprfuncargs: + self.reprfuncargs.toterminal(tw) + + self._write_entry_lines(tw) + + if self.reprlocals: + tw.line("") + self.reprlocals.toterminal(tw) + if self.reprfileloc: + if self.lines: + tw.line("") + self.reprfileloc.toterminal(tw) + + def __str__(self) -> str: + return "{}\n{}\n{}".format( + "\n".join(self.lines), self.reprlocals, self.reprfileloc + ) + + +@dataclasses.dataclass(eq=False) +class ReprFileLocation(TerminalRepr): + path: str + lineno: int + message: str + + def __post_init__(self) -> None: + self.path = str(self.path) + + def toterminal(self, tw: TerminalWriter) -> None: + # Filename and lineno output for each entry, using an output format + # that most editors understand. + msg = self.message + i = msg.find("\n") + if i != -1: + msg = msg[:i] + tw.write(self.path, bold=True, red=True) + tw.line(f":{self.lineno}: {msg}") + + +@dataclasses.dataclass(eq=False) +class ReprLocals(TerminalRepr): + lines: Sequence[str] + + def toterminal(self, tw: TerminalWriter, indent="") -> None: + for line in self.lines: + tw.line(indent + line) + + +@dataclasses.dataclass(eq=False) +class ReprFuncArgs(TerminalRepr): + args: Sequence[tuple[str, object]] + + def toterminal(self, tw: TerminalWriter) -> None: + if self.args: + linesofar = "" + for name, value in self.args: + ns = f"{name} = {value}" + if len(ns) + len(linesofar) + 2 > tw.fullwidth: + if linesofar: + tw.line(linesofar) + linesofar = ns + else: + if linesofar: + linesofar += ", " + ns + else: + linesofar = ns + if linesofar: + tw.line(linesofar) + tw.line("") + + +def getfslineno(obj: object) -> tuple[str | Path, int]: + """Return source location (path, lineno) for the given object. + + If the source cannot be determined return ("", -1). + + The line number is 0-based. + """ + # xxx let decorators etc specify a sane ordering + # NOTE: this used to be done in _pytest.compat.getfslineno, initially added + # in 6ec13a2b9. It ("place_as") appears to be something very custom. + obj = get_real_func(obj) + if hasattr(obj, "place_as"): + obj = obj.place_as + + try: + code = Code.from_function(obj) + except TypeError: + try: + fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type] + except TypeError: + return "", -1 + + fspath = (fn and absolutepath(fn)) or "" + lineno = -1 + if fspath: + try: + _, lineno = findsource(obj) + except OSError: + pass + return fspath, lineno + + return code.path, code.firstlineno + + +def _byte_offset_to_character_offset(str, offset): + """Converts a byte based offset in a string to a code-point.""" + as_utf8 = str.encode("utf-8") + return len(as_utf8[:offset].decode("utf-8", errors="replace")) + + +# Relative paths that we use to filter traceback entries from appearing to the user; +# see filter_traceback. +# note: if we need to add more paths than what we have now we should probably use a list +# for better maintenance. + +_PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc")) +# pluggy is either a package or a single module depending on the version +if _PLUGGY_DIR.name == "__init__.py": + _PLUGGY_DIR = _PLUGGY_DIR.parent +_PYTEST_DIR = Path(_pytest.__file__).parent + + +def filter_traceback(entry: TracebackEntry) -> bool: + """Return True if a TracebackEntry instance should be included in tracebacks. + + We hide traceback entries of: + + * dynamically generated code (no code to show up for it); + * internal traceback from pytest or its internal libraries, py and pluggy. + """ + # entry.path might sometimes return a str object when the entry + # points to dynamically generated code. + # See https://bitbucket.org/pytest-dev/py/issues/71. + raw_filename = entry.frame.code.raw.co_filename + is_generated = "<" in raw_filename and ">" in raw_filename + if is_generated: + return False + + # entry.path might point to a non-existing file, in which case it will + # also return a str object. See #1133. + p = Path(entry.path) + + parents = p.parents + if _PLUGGY_DIR in parents: + return False + if _PYTEST_DIR in parents: + return False + + return True + + +def filter_excinfo_traceback( + tbfilter: TracebackFilter, excinfo: ExceptionInfo[BaseException] +) -> Traceback: + """Filter the exception traceback in ``excinfo`` according to ``tbfilter``.""" + if callable(tbfilter): + return tbfilter(excinfo) + elif tbfilter: + return excinfo.traceback.filter(excinfo) + else: + return excinfo.traceback diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_code/source.py b/Backend/venv/lib/python3.12/site-packages/_pytest/_code/source.py new file mode 100644 index 00000000..99c242dd --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/_code/source.py @@ -0,0 +1,225 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import ast +from bisect import bisect_right +from collections.abc import Iterable +from collections.abc import Iterator +import inspect +import textwrap +import tokenize +import types +from typing import overload +import warnings + + +class Source: + """An immutable object holding a source code fragment. + + When using Source(...), the source lines are deindented. + """ + + def __init__(self, obj: object = None) -> None: + if not obj: + self.lines: list[str] = [] + self.raw_lines: list[str] = [] + elif isinstance(obj, Source): + self.lines = obj.lines + self.raw_lines = obj.raw_lines + elif isinstance(obj, tuple | list): + self.lines = deindent(x.rstrip("\n") for x in obj) + self.raw_lines = list(x.rstrip("\n") for x in obj) + elif isinstance(obj, str): + self.lines = deindent(obj.split("\n")) + self.raw_lines = obj.split("\n") + else: + try: + rawcode = getrawcode(obj) + src = inspect.getsource(rawcode) + except TypeError: + src = inspect.getsource(obj) # type: ignore[arg-type] + self.lines = deindent(src.split("\n")) + self.raw_lines = src.split("\n") + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Source): + return NotImplemented + return self.lines == other.lines + + # Ignore type because of https://github.com/python/mypy/issues/4266. + __hash__ = None # type: ignore + + @overload + def __getitem__(self, key: int) -> str: ... + + @overload + def __getitem__(self, key: slice) -> Source: ... + + def __getitem__(self, key: int | slice) -> str | Source: + if isinstance(key, int): + return self.lines[key] + else: + if key.step not in (None, 1): + raise IndexError("cannot slice a Source with a step") + newsource = Source() + newsource.lines = self.lines[key.start : key.stop] + newsource.raw_lines = self.raw_lines[key.start : key.stop] + return newsource + + def __iter__(self) -> Iterator[str]: + return iter(self.lines) + + def __len__(self) -> int: + return len(self.lines) + + def strip(self) -> Source: + """Return new Source object with trailing and leading blank lines removed.""" + start, end = 0, len(self) + while start < end and not self.lines[start].strip(): + start += 1 + while end > start and not self.lines[end - 1].strip(): + end -= 1 + source = Source() + source.raw_lines = self.raw_lines + source.lines[:] = self.lines[start:end] + return source + + def indent(self, indent: str = " " * 4) -> Source: + """Return a copy of the source object with all lines indented by the + given indent-string.""" + newsource = Source() + newsource.raw_lines = self.raw_lines + newsource.lines = [(indent + line) for line in self.lines] + return newsource + + def getstatement(self, lineno: int) -> Source: + """Return Source statement which contains the given linenumber + (counted from 0).""" + start, end = self.getstatementrange(lineno) + return self[start:end] + + def getstatementrange(self, lineno: int) -> tuple[int, int]: + """Return (start, end) tuple which spans the minimal statement region + which containing the given lineno.""" + if not (0 <= lineno < len(self)): + raise IndexError("lineno out of range") + _ast, start, end = getstatementrange_ast(lineno, self) + return start, end + + def deindent(self) -> Source: + """Return a new Source object deindented.""" + newsource = Source() + newsource.lines[:] = deindent(self.lines) + newsource.raw_lines = self.raw_lines + return newsource + + def __str__(self) -> str: + return "\n".join(self.lines) + + +# +# helper functions +# + + +def findsource(obj) -> tuple[Source | None, int]: + try: + sourcelines, lineno = inspect.findsource(obj) + except Exception: + return None, -1 + source = Source() + source.lines = [line.rstrip() for line in sourcelines] + source.raw_lines = sourcelines + return source, lineno + + +def getrawcode(obj: object, trycall: bool = True) -> types.CodeType: + """Return code object for given function.""" + try: + return obj.__code__ # type: ignore[attr-defined,no-any-return] + except AttributeError: + pass + if trycall: + call = getattr(obj, "__call__", None) + if call and not isinstance(obj, type): + return getrawcode(call, trycall=False) + raise TypeError(f"could not get code object for {obj!r}") + + +def deindent(lines: Iterable[str]) -> list[str]: + return textwrap.dedent("\n".join(lines)).splitlines() + + +def get_statement_startend2(lineno: int, node: ast.AST) -> tuple[int, int | None]: + # Flatten all statements and except handlers into one lineno-list. + # AST's line numbers start indexing at 1. + values: list[int] = [] + for x in ast.walk(node): + if isinstance(x, ast.stmt | ast.ExceptHandler): + # The lineno points to the class/def, so need to include the decorators. + if isinstance(x, ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef): + for d in x.decorator_list: + values.append(d.lineno - 1) + values.append(x.lineno - 1) + for name in ("finalbody", "orelse"): + val: list[ast.stmt] | None = getattr(x, name, None) + if val: + # Treat the finally/orelse part as its own statement. + values.append(val[0].lineno - 1 - 1) + values.sort() + insert_index = bisect_right(values, lineno) + start = values[insert_index - 1] + if insert_index >= len(values): + end = None + else: + end = values[insert_index] + return start, end + + +def getstatementrange_ast( + lineno: int, + source: Source, + assertion: bool = False, + astnode: ast.AST | None = None, +) -> tuple[ast.AST, int, int]: + if astnode is None: + content = str(source) + # See #4260: + # Don't produce duplicate warnings when compiling source to find AST. + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + astnode = ast.parse(content, "source", "exec") + + start, end = get_statement_startend2(lineno, astnode) + # We need to correct the end: + # - ast-parsing strips comments + # - there might be empty lines + # - we might have lesser indented code blocks at the end + if end is None: + end = len(source.lines) + + if end > start + 1: + # Make sure we don't span differently indented code blocks + # by using the BlockFinder helper used which inspect.getsource() uses itself. + block_finder = inspect.BlockFinder() + # If we start with an indented line, put blockfinder to "started" mode. + block_finder.started = ( + bool(source.lines[start]) and source.lines[start][0].isspace() + ) + it = ((x + "\n") for x in source.lines[start:end]) + try: + for tok in tokenize.generate_tokens(lambda: next(it)): + block_finder.tokeneater(*tok) + except (inspect.EndOfBlock, IndentationError): + end = block_finder.last + start + except Exception: + pass + + # The end might still point to a comment or empty line, correct it. + while end: + line = source.lines[end - 1].lstrip() + if line.startswith("#") or not line: + end -= 1 + else: + break + return astnode, start, end diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_io/__init__.py b/Backend/venv/lib/python3.12/site-packages/_pytest/_io/__init__.py new file mode 100644 index 00000000..b0155b18 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/_io/__init__.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from .terminalwriter import get_terminal_width +from .terminalwriter import TerminalWriter + + +__all__ = [ + "TerminalWriter", + "get_terminal_width", +] diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3850e74dfe511c5eebd31981a8e05ffd124d92d7 GIT binary patch literal 386 zcmXw!zfM9i6vo^82M7cP6B8E~HcX(Xt8pL>PEIB!cR8uHUK)Dao8AI4`V>BcFX0RL z0_G-6hJlTQjmdLC&T#tmJHIcd{cg9L1Zy?ysxLf$tIM&1gP_|XiVq@)pqgYfp_GU~ zggVH=B;4x=b(BP?V^K?kBwo~ejdd08(Xz#jv7UQnO|hm2?>4+j6;my5T^l!6zbwwNZ9+=vl5|&Oup}3o28mC9qO(mLwu1M%Ya#Im Dei&;+ literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/pprint.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/pprint.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13eca886282a7ffe383ccceba21ac467aa2d205d GIT binary patch literal 24441 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 G<@^UxlVTMB literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/terminalwriter.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/terminalwriter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0911069796a9a40fa7365a41271f75a578387625 GIT binary patch literal 11255 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/wcwidth.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/wcwidth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0983b666a322be43aad330f7998dbc68f68b8a8a GIT binary patch literal 1697 zcmbVM&1)M+6rb5$X(g@JlI?^zDQU(wxK^>HI3GQQf)h8Gk_OU33xc9-cE{F6s~L56 zWk*J)g65zkrRbI(bLb%@hyEcw*$LR)rVwaz&`pVb%B6j?Qf#ZGP&%-`eQ(}-^WK~H z-u!HuDFo}~*(o;}L+E$Oyj^OazY|<0;l5N>#dcEs_hCMhc zIzt8nAKpW-`w#^OvL<@#auYkaiEZVMQaOc?h&f6{fxps#Ae-3HDzgarvrcSCCoXmU z(@Z9%4@nY{BpXSRLy}Zefc2`i_B$|n>)1{A&; z;xD?=I;Vvy7fxu@nQL=M)(WgJVOs%R<)RU0spSVH%XckST6dkGoKr(=hnS0TF!p85 zAn7+Z3uRuVg;jQ^utfbzz-xtN9#C)oGUpYStrjj@c7-yhutwQh!E z*RNg?2IP}d#Tfj;B;%rLdF}>{Dh_Rm{V*1_gE%e~ly%#e7YXf0#0j~RfStYq0X7ex zzYNeq&;jO1>!au9#Ev<&WllZ1_;~WEIrkcAv14ug+&tE~@u1jNb`7(G?|<;i`0?F@ z*%`ZM+>7;+paSOU(bhEq*ADmB_pU;cVO&JR9;z_UAUF+jF)3k6!n9P^!&IrXQinHz zmP$g7OPE}#Gdtki^M#B*N;k?;5MX~qk)J4|H;GGC?$kZ{p?D8Oa^n3tfS!sm{tZpN kL}z|QW3S>!G1}+9dH1z4jm None: + """Handle pretty printing operations onto a stream using a set of + configured parameters. + + indent + Number of spaces to indent for each level of nesting. + + width + Attempted maximum number of columns in the output. + + depth + The maximum depth to print out nested structures. + + """ + if indent < 0: + raise ValueError("indent must be >= 0") + if depth is not None and depth <= 0: + raise ValueError("depth must be > 0") + if not width: + raise ValueError("width must be != 0") + self._depth = depth + self._indent_per_level = indent + self._width = width + + def pformat(self, object: Any) -> str: + sio = _StringIO() + self._format(object, sio, 0, 0, set(), 0) + return sio.getvalue() + + def _format( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + objid = id(object) + if objid in context: + stream.write(_recursion(object)) + return + + p = self._dispatch.get(type(object).__repr__, None) + if p is not None: + context.add(objid) + p(self, object, stream, indent, allowance, context, level + 1) + context.remove(objid) + elif ( + _dataclasses.is_dataclass(object) + and not isinstance(object, type) + and object.__dataclass_params__.repr # type:ignore[attr-defined] + and + # Check dataclass has generated repr method. + hasattr(object.__repr__, "__wrapped__") + and "__create_fn__" in object.__repr__.__wrapped__.__qualname__ + ): + context.add(objid) + self._pprint_dataclass( + object, stream, indent, allowance, context, level + 1 + ) + context.remove(objid) + else: + stream.write(self._repr(object, context, level)) + + def _pprint_dataclass( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + cls_name = object.__class__.__name__ + items = [ + (f.name, getattr(object, f.name)) + for f in _dataclasses.fields(object) + if f.repr + ] + stream.write(cls_name + "(") + self._format_namespace_items(items, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch: dict[ + Callable[..., str], + Callable[[PrettyPrinter, Any, IO[str], int, int, set[int], int], None], + ] = {} + + def _pprint_dict( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + write = stream.write + write("{") + items = sorted(object.items(), key=_safe_tuple) + self._format_dict_items(items, stream, indent, allowance, context, level) + write("}") + + _dispatch[dict.__repr__] = _pprint_dict + + def _pprint_ordered_dict( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + if not len(object): + stream.write(repr(object)) + return + cls = object.__class__ + stream.write(cls.__name__ + "(") + self._pprint_dict(object, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict + + def _pprint_list( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + stream.write("[") + self._format_items(object, stream, indent, allowance, context, level) + stream.write("]") + + _dispatch[list.__repr__] = _pprint_list + + def _pprint_tuple( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + stream.write("(") + self._format_items(object, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[tuple.__repr__] = _pprint_tuple + + def _pprint_set( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + if not len(object): + stream.write(repr(object)) + return + typ = object.__class__ + if typ is set: + stream.write("{") + endchar = "}" + else: + stream.write(typ.__name__ + "({") + endchar = "})" + object = sorted(object, key=_safe_key) + self._format_items(object, stream, indent, allowance, context, level) + stream.write(endchar) + + _dispatch[set.__repr__] = _pprint_set + _dispatch[frozenset.__repr__] = _pprint_set + + def _pprint_str( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + write = stream.write + if not len(object): + write(repr(object)) + return + chunks = [] + lines = object.splitlines(True) + if level == 1: + indent += 1 + allowance += 1 + max_width1 = max_width = self._width - indent + for i, line in enumerate(lines): + rep = repr(line) + if i == len(lines) - 1: + max_width1 -= allowance + if len(rep) <= max_width1: + chunks.append(rep) + else: + # A list of alternating (non-space, space) strings + parts = re.findall(r"\S*\s*", line) + assert parts + assert not parts[-1] + parts.pop() # drop empty last part + max_width2 = max_width + current = "" + for j, part in enumerate(parts): + candidate = current + part + if j == len(parts) - 1 and i == len(lines) - 1: + max_width2 -= allowance + if len(repr(candidate)) > max_width2: + if current: + chunks.append(repr(current)) + current = part + else: + current = candidate + if current: + chunks.append(repr(current)) + if len(chunks) == 1: + write(rep) + return + if level == 1: + write("(") + for i, rep in enumerate(chunks): + if i > 0: + write("\n" + " " * indent) + write(rep) + if level == 1: + write(")") + + _dispatch[str.__repr__] = _pprint_str + + def _pprint_bytes( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + write = stream.write + if len(object) <= 4: + write(repr(object)) + return + parens = level == 1 + if parens: + indent += 1 + allowance += 1 + write("(") + delim = "" + for rep in _wrap_bytes_repr(object, self._width - indent, allowance): + write(delim) + write(rep) + if not delim: + delim = "\n" + " " * indent + if parens: + write(")") + + _dispatch[bytes.__repr__] = _pprint_bytes + + def _pprint_bytearray( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + write = stream.write + write("bytearray(") + self._pprint_bytes( + bytes(object), stream, indent + 10, allowance + 1, context, level + 1 + ) + write(")") + + _dispatch[bytearray.__repr__] = _pprint_bytearray + + def _pprint_mappingproxy( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + stream.write("mappingproxy(") + self._format(object.copy(), stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy + + def _pprint_simplenamespace( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + if type(object) is _types.SimpleNamespace: + # The SimpleNamespace repr is "namespace" instead of the class + # name, so we do the same here. For subclasses; use the class name. + cls_name = "namespace" + else: + cls_name = object.__class__.__name__ + items = object.__dict__.items() + stream.write(cls_name + "(") + self._format_namespace_items(items, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace + + def _format_dict_items( + self, + items: list[tuple[Any, Any]], + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + if not items: + return + + write = stream.write + item_indent = indent + self._indent_per_level + delimnl = "\n" + " " * item_indent + for key, ent in items: + write(delimnl) + write(self._repr(key, context, level)) + write(": ") + self._format(ent, stream, item_indent, 1, context, level) + write(",") + + write("\n" + " " * indent) + + def _format_namespace_items( + self, + items: list[tuple[Any, Any]], + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + if not items: + return + + write = stream.write + item_indent = indent + self._indent_per_level + delimnl = "\n" + " " * item_indent + for key, ent in items: + write(delimnl) + write(key) + write("=") + if id(ent) in context: + # Special-case representation of recursion to match standard + # recursive dataclass repr. + write("...") + else: + self._format( + ent, + stream, + item_indent + len(key) + 1, + 1, + context, + level, + ) + + write(",") + + write("\n" + " " * indent) + + def _format_items( + self, + items: list[Any], + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + if not items: + return + + write = stream.write + item_indent = indent + self._indent_per_level + delimnl = "\n" + " " * item_indent + + for item in items: + write(delimnl) + self._format(item, stream, item_indent, 1, context, level) + write(",") + + write("\n" + " " * indent) + + def _repr(self, object: Any, context: set[int], level: int) -> str: + return self._safe_repr(object, context.copy(), self._depth, level) + + def _pprint_default_dict( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + rdf = self._repr(object.default_factory, context, level) + stream.write(f"{object.__class__.__name__}({rdf}, ") + self._pprint_dict(object, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict + + def _pprint_counter( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + stream.write(object.__class__.__name__ + "(") + + if object: + stream.write("{") + items = object.most_common() + self._format_dict_items(items, stream, indent, allowance, context, level) + stream.write("}") + + stream.write(")") + + _dispatch[_collections.Counter.__repr__] = _pprint_counter + + def _pprint_chain_map( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + if not len(object.maps) or (len(object.maps) == 1 and not len(object.maps[0])): + stream.write(repr(object)) + return + + stream.write(object.__class__.__name__ + "(") + self._format_items(object.maps, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map + + def _pprint_deque( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + stream.write(object.__class__.__name__ + "(") + if object.maxlen is not None: + stream.write(f"maxlen={object.maxlen}, ") + stream.write("[") + + self._format_items(object, stream, indent, allowance + 1, context, level) + stream.write("])") + + _dispatch[_collections.deque.__repr__] = _pprint_deque + + def _pprint_user_dict( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + self._format(object.data, stream, indent, allowance, context, level - 1) + + _dispatch[_collections.UserDict.__repr__] = _pprint_user_dict + + def _pprint_user_list( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + self._format(object.data, stream, indent, allowance, context, level - 1) + + _dispatch[_collections.UserList.__repr__] = _pprint_user_list + + def _pprint_user_string( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + self._format(object.data, stream, indent, allowance, context, level - 1) + + _dispatch[_collections.UserString.__repr__] = _pprint_user_string + + def _safe_repr( + self, object: Any, context: set[int], maxlevels: int | None, level: int + ) -> str: + typ = type(object) + if typ in _builtin_scalars: + return repr(object) + + r = getattr(typ, "__repr__", None) + + if issubclass(typ, dict) and r is dict.__repr__: + if not object: + return "{}" + objid = id(object) + if maxlevels and level >= maxlevels: + return "{...}" + if objid in context: + return _recursion(object) + context.add(objid) + components: list[str] = [] + append = components.append + level += 1 + for k, v in sorted(object.items(), key=_safe_tuple): + krepr = self._safe_repr(k, context, maxlevels, level) + vrepr = self._safe_repr(v, context, maxlevels, level) + append(f"{krepr}: {vrepr}") + context.remove(objid) + return "{{{}}}".format(", ".join(components)) + + if (issubclass(typ, list) and r is list.__repr__) or ( + issubclass(typ, tuple) and r is tuple.__repr__ + ): + if issubclass(typ, list): + if not object: + return "[]" + format = "[%s]" + elif len(object) == 1: + format = "(%s,)" + else: + if not object: + return "()" + format = "(%s)" + objid = id(object) + if maxlevels and level >= maxlevels: + return format % "..." + if objid in context: + return _recursion(object) + context.add(objid) + components = [] + append = components.append + level += 1 + for o in object: + orepr = self._safe_repr(o, context, maxlevels, level) + append(orepr) + context.remove(objid) + return format % ", ".join(components) + + return repr(object) + + +_builtin_scalars = frozenset( + {str, bytes, bytearray, float, complex, bool, type(None), int} +) + + +def _recursion(object: Any) -> str: + return f"" + + +def _wrap_bytes_repr(object: Any, width: int, allowance: int) -> Iterator[str]: + current = b"" + last = len(object) // 4 * 4 + for i in range(0, len(object), 4): + part = object[i : i + 4] + candidate = current + part + if i == last: + width -= allowance + if len(repr(candidate)) > width: + if current: + yield repr(current) + current = part + else: + current = candidate + if current: + yield repr(current) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_io/saferepr.py b/Backend/venv/lib/python3.12/site-packages/_pytest/_io/saferepr.py new file mode 100644 index 00000000..cee70e33 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/_io/saferepr.py @@ -0,0 +1,130 @@ +from __future__ import annotations + +import pprint +import reprlib + + +def _try_repr_or_str(obj: object) -> str: + try: + return repr(obj) + except (KeyboardInterrupt, SystemExit): + raise + except BaseException: + return f'{type(obj).__name__}("{obj}")' + + +def _format_repr_exception(exc: BaseException, obj: object) -> str: + try: + exc_info = _try_repr_or_str(exc) + except (KeyboardInterrupt, SystemExit): + raise + except BaseException as inner_exc: + exc_info = f"unpresentable exception ({_try_repr_or_str(inner_exc)})" + return ( + f"<[{exc_info} raised in repr()] {type(obj).__name__} object at 0x{id(obj):x}>" + ) + + +def _ellipsize(s: str, maxsize: int) -> str: + if len(s) > maxsize: + i = max(0, (maxsize - 3) // 2) + j = max(0, maxsize - 3 - i) + return s[:i] + "..." + s[len(s) - j :] + return s + + +class SafeRepr(reprlib.Repr): + """ + repr.Repr that limits the resulting size of repr() and includes + information on exceptions raised during the call. + """ + + def __init__(self, maxsize: int | None, use_ascii: bool = False) -> None: + """ + :param maxsize: + If not None, will truncate the resulting repr to that specific size, using ellipsis + somewhere in the middle to hide the extra text. + If None, will not impose any size limits on the returning repr. + """ + super().__init__() + # ``maxstring`` is used by the superclass, and needs to be an int; using a + # very large number in case maxsize is None, meaning we want to disable + # truncation. + self.maxstring = maxsize if maxsize is not None else 1_000_000_000 + self.maxsize = maxsize + self.use_ascii = use_ascii + + def repr(self, x: object) -> str: + try: + if self.use_ascii: + s = ascii(x) + else: + s = super().repr(x) + except (KeyboardInterrupt, SystemExit): + raise + except BaseException as exc: + s = _format_repr_exception(exc, x) + if self.maxsize is not None: + s = _ellipsize(s, self.maxsize) + return s + + def repr_instance(self, x: object, level: int) -> str: + try: + s = repr(x) + except (KeyboardInterrupt, SystemExit): + raise + except BaseException as exc: + s = _format_repr_exception(exc, x) + if self.maxsize is not None: + s = _ellipsize(s, self.maxsize) + return s + + +def safeformat(obj: object) -> str: + """Return a pretty printed string for the given object. + + Failing __repr__ functions of user instances will be represented + with a short exception info. + """ + try: + return pprint.pformat(obj) + except Exception as exc: + return _format_repr_exception(exc, obj) + + +# Maximum size of overall repr of objects to display during assertion errors. +DEFAULT_REPR_MAX_SIZE = 240 + + +def saferepr( + obj: object, maxsize: int | None = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False +) -> str: + """Return a size-limited safe repr-string for the given object. + + Failing __repr__ functions of user instances will be represented + with a short exception info and 'saferepr' generally takes + care to never raise exceptions itself. + + This function is a wrapper around the Repr/reprlib functionality of the + stdlib. + """ + return SafeRepr(maxsize, use_ascii).repr(obj) + + +def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str: + """Return an unlimited-size safe repr-string for the given object. + + As with saferepr, failing __repr__ functions of user instances + will be represented with a short exception info. + + This function is a wrapper around simple repr. + + Note: a cleaner solution would be to alter ``saferepr``this way + when maxsize=None, but that might affect some other code. + """ + try: + if use_ascii: + return ascii(obj) + return repr(obj) + except Exception as exc: + return _format_repr_exception(exc, obj) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_io/terminalwriter.py b/Backend/venv/lib/python3.12/site-packages/_pytest/_io/terminalwriter.py new file mode 100644 index 00000000..9191b4ed --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/_io/terminalwriter.py @@ -0,0 +1,258 @@ +"""Helper functions for writing to terminals and files.""" + +from __future__ import annotations + +from collections.abc import Sequence +import os +import shutil +import sys +from typing import final +from typing import Literal +from typing import TextIO + +import pygments +from pygments.formatters.terminal import TerminalFormatter +from pygments.lexer import Lexer +from pygments.lexers.diff import DiffLexer +from pygments.lexers.python import PythonLexer + +from ..compat import assert_never +from .wcwidth import wcswidth + + +# This code was initially copied from py 1.8.1, file _io/terminalwriter.py. + + +def get_terminal_width() -> int: + width, _ = shutil.get_terminal_size(fallback=(80, 24)) + + # The Windows get_terminal_size may be bogus, let's sanify a bit. + if width < 40: + width = 80 + + return width + + +def should_do_markup(file: TextIO) -> bool: + if os.environ.get("PY_COLORS") == "1": + return True + if os.environ.get("PY_COLORS") == "0": + return False + if os.environ.get("NO_COLOR"): + return False + if os.environ.get("FORCE_COLOR"): + return True + return ( + hasattr(file, "isatty") and file.isatty() and os.environ.get("TERM") != "dumb" + ) + + +@final +class TerminalWriter: + _esctable = dict( + black=30, + red=31, + green=32, + yellow=33, + blue=34, + purple=35, + cyan=36, + white=37, + Black=40, + Red=41, + Green=42, + Yellow=43, + Blue=44, + Purple=45, + Cyan=46, + White=47, + bold=1, + light=2, + blink=5, + invert=7, + ) + + def __init__(self, file: TextIO | None = None) -> None: + if file is None: + file = sys.stdout + if hasattr(file, "isatty") and file.isatty() and sys.platform == "win32": + try: + import colorama + except ImportError: + pass + else: + file = colorama.AnsiToWin32(file).stream + assert file is not None + self._file = file + self.hasmarkup = should_do_markup(file) + self._current_line = "" + self._terminal_width: int | None = None + self.code_highlight = True + + @property + def fullwidth(self) -> int: + if self._terminal_width is not None: + return self._terminal_width + return get_terminal_width() + + @fullwidth.setter + def fullwidth(self, value: int) -> None: + self._terminal_width = value + + @property + def width_of_current_line(self) -> int: + """Return an estimate of the width so far in the current line.""" + return wcswidth(self._current_line) + + def markup(self, text: str, **markup: bool) -> str: + for name in markup: + if name not in self._esctable: + raise ValueError(f"unknown markup: {name!r}") + if self.hasmarkup: + esc = [self._esctable[name] for name, on in markup.items() if on] + if esc: + text = "".join(f"\x1b[{cod}m" for cod in esc) + text + "\x1b[0m" + return text + + def sep( + self, + sepchar: str, + title: str | None = None, + fullwidth: int | None = None, + **markup: bool, + ) -> None: + if fullwidth is None: + fullwidth = self.fullwidth + # The goal is to have the line be as long as possible + # under the condition that len(line) <= fullwidth. + if sys.platform == "win32": + # If we print in the last column on windows we are on a + # new line but there is no way to verify/neutralize this + # (we may not know the exact line width). + # So let's be defensive to avoid empty lines in the output. + fullwidth -= 1 + if title is not None: + # we want 2 + 2*len(fill) + len(title) <= fullwidth + # i.e. 2 + 2*len(sepchar)*N + len(title) <= fullwidth + # 2*len(sepchar)*N <= fullwidth - len(title) - 2 + # N <= (fullwidth - len(title) - 2) // (2*len(sepchar)) + N = max((fullwidth - len(title) - 2) // (2 * len(sepchar)), 1) + fill = sepchar * N + line = f"{fill} {title} {fill}" + else: + # we want len(sepchar)*N <= fullwidth + # i.e. N <= fullwidth // len(sepchar) + line = sepchar * (fullwidth // len(sepchar)) + # In some situations there is room for an extra sepchar at the right, + # in particular if we consider that with a sepchar like "_ " the + # trailing space is not important at the end of the line. + if len(line) + len(sepchar.rstrip()) <= fullwidth: + line += sepchar.rstrip() + + self.line(line, **markup) + + def write(self, msg: str, *, flush: bool = False, **markup: bool) -> None: + if msg: + current_line = msg.rsplit("\n", 1)[-1] + if "\n" in msg: + self._current_line = current_line + else: + self._current_line += current_line + + msg = self.markup(msg, **markup) + + self.write_raw(msg, flush=flush) + + def write_raw(self, msg: str, *, flush: bool = False) -> None: + try: + self._file.write(msg) + except UnicodeEncodeError: + # Some environments don't support printing general Unicode + # strings, due to misconfiguration or otherwise; in that case, + # print the string escaped to ASCII. + # When the Unicode situation improves we should consider + # letting the error propagate instead of masking it (see #7475 + # for one brief attempt). + msg = msg.encode("unicode-escape").decode("ascii") + self._file.write(msg) + + if flush: + self.flush() + + def line(self, s: str = "", **markup: bool) -> None: + self.write(s, **markup) + self.write("\n") + + def flush(self) -> None: + self._file.flush() + + def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> None: + """Write lines of source code possibly highlighted. + + Keeping this private for now because the API is clunky. We should discuss how + to evolve the terminal writer so we can have more precise color support, for example + being able to write part of a line in one color and the rest in another, and so on. + """ + if indents and len(indents) != len(lines): + raise ValueError( + f"indents size ({len(indents)}) should have same size as lines ({len(lines)})" + ) + if not indents: + indents = [""] * len(lines) + source = "\n".join(lines) + new_lines = self._highlight(source).splitlines() + # Would be better to strict=True but that fails some CI jobs. + for indent, new_line in zip(indents, new_lines, strict=False): + self.line(indent + new_line) + + def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer: + if lexer == "python": + return PythonLexer() + elif lexer == "diff": + return DiffLexer() + else: + assert_never(lexer) + + def _get_pygments_formatter(self) -> TerminalFormatter: + from _pytest.config.exceptions import UsageError + + theme = os.getenv("PYTEST_THEME") + theme_mode = os.getenv("PYTEST_THEME_MODE", "dark") + + try: + return TerminalFormatter(bg=theme_mode, style=theme) + except pygments.util.ClassNotFound as e: + raise UsageError( + f"PYTEST_THEME environment variable has an invalid value: '{theme}'. " + "Hint: See available pygments styles with `pygmentize -L styles`." + ) from e + except pygments.util.OptionError as e: + raise UsageError( + f"PYTEST_THEME_MODE environment variable has an invalid value: '{theme_mode}'. " + "The allowed values are 'dark' (default) and 'light'." + ) from e + + def _highlight( + self, source: str, lexer: Literal["diff", "python"] = "python" + ) -> str: + """Highlight the given source if we have markup support.""" + if not source or not self.hasmarkup or not self.code_highlight: + return source + + pygments_lexer = self._get_pygments_lexer(lexer) + pygments_formatter = self._get_pygments_formatter() + + highlighted: str = pygments.highlight( + source, pygments_lexer, pygments_formatter + ) + # pygments terminal formatter may add a newline when there wasn't one. + # We don't want this, remove. + if highlighted[-1] == "\n" and source[-1] != "\n": + highlighted = highlighted[:-1] + + # Some lexers will not set the initial color explicitly + # which may lead to the previous color being propagated to the + # start of the expression, so reset first. + highlighted = "\x1b[0m" + highlighted + + return highlighted diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_io/wcwidth.py b/Backend/venv/lib/python3.12/site-packages/_pytest/_io/wcwidth.py new file mode 100644 index 00000000..23886ff1 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/_io/wcwidth.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from functools import lru_cache +import unicodedata + + +@lru_cache(100) +def wcwidth(c: str) -> int: + """Determine how many columns are needed to display a character in a terminal. + + Returns -1 if the character is not printable. + Returns 0, 1 or 2 for other characters. + """ + o = ord(c) + + # ASCII fast path. + if 0x20 <= o < 0x07F: + return 1 + + # Some Cf/Zp/Zl characters which should be zero-width. + if ( + o == 0x0000 + or 0x200B <= o <= 0x200F + or 0x2028 <= o <= 0x202E + or 0x2060 <= o <= 0x2063 + ): + return 0 + + category = unicodedata.category(c) + + # Control characters. + if category == "Cc": + return -1 + + # Combining characters with zero width. + if category in ("Me", "Mn"): + return 0 + + # Full/Wide east asian characters. + if unicodedata.east_asian_width(c) in ("F", "W"): + return 2 + + return 1 + + +def wcswidth(s: str) -> int: + """Determine how many columns are needed to display a string in a terminal. + + Returns -1 if the string contains non-printable characters. + """ + width = 0 + for c in unicodedata.normalize("NFC", s): + wc = wcwidth(c) + if wc < 0: + return -1 + width += wc + return width diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_py/__init__.py b/Backend/venv/lib/python3.12/site-packages/_pytest/_py/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_py/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/_py/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99cbfa42e3425af00d697f8f4973076d2bb246ec GIT binary patch literal 194 zcmX@j%ge<81Wyf=GC}lX5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!O4QHD&rQ`&&#TaP zNiEJU$uH3N$S+CF(RIqt&(6$C*LO-x&Q8rs(JxEQE7Q-(Owuo?EXl~vGuAUS(l5>| zN!2X?Do9LEE!K|*%A^*TfN1^r_{_Y_lK6PNg34bUHo5sJr8%i~MXW#@8G*PM#Q4a} L$jDg43}gWS?iMp` literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_py/__pycache__/error.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/_py/__pycache__/error.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6cc2e8bc9e1c0f059d25351a412e3b866e04e69a GIT binary patch literal 4962 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 X!Gz7qyAy?Kz5 str: + return "{}.{} {!r}: {} ".format( + self.__class__.__module__, + self.__class__.__name__, + self.__class__.__doc__, + " ".join(map(str, self.args)), + # repr(self.args) + ) + + def __str__(self) -> str: + s = "[{}]: {}".format( + self.__class__.__doc__, + " ".join(map(str, self.args)), + ) + return s + + +_winerrnomap = { + 2: errno.ENOENT, + 3: errno.ENOENT, + 17: errno.EEXIST, + 18: errno.EXDEV, + 13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailable + 22: errno.ENOTDIR, + 20: errno.ENOTDIR, + 267: errno.ENOTDIR, + 5: errno.EACCES, # anything better? +} + + +class ErrorMaker: + """lazily provides Exception classes for each possible POSIX errno + (as defined per the 'errno' module). All such instances + subclass EnvironmentError. + """ + + _errno2class: dict[int, type[Error]] = {} + + def __getattr__(self, name: str) -> type[Error]: + if name[0] == "_": + raise AttributeError(name) + eno = getattr(errno, name) + cls = self._geterrnoclass(eno) + setattr(self, name, cls) + return cls + + def _geterrnoclass(self, eno: int) -> type[Error]: + try: + return self._errno2class[eno] + except KeyError: + clsname = errno.errorcode.get(eno, f"UnknownErrno{eno}") + errorcls = type( + clsname, + (Error,), + {"__module__": "py.error", "__doc__": os.strerror(eno)}, + ) + self._errno2class[eno] = errorcls + return errorcls + + def checked_call( + self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs + ) -> R: + """Call a function and raise an errno-exception if applicable.""" + __tracebackhide__ = True + try: + return func(*args, **kwargs) + except Error: + raise + except OSError as value: + if not hasattr(value, "errno"): + raise + if sys.platform == "win32": + try: + # error: Invalid index type "Optional[int]" for "dict[int, int]"; expected type "int" [index] + # OK to ignore because we catch the KeyError below. + cls = self._geterrnoclass(_winerrnomap[value.errno]) # type:ignore[index] + except KeyError: + raise value + else: + # we are not on Windows, or we got a proper OSError + if value.errno is None: + cls = type( + "UnknownErrnoNone", + (Error,), + {"__module__": "py.error", "__doc__": None}, + ) + else: + cls = self._geterrnoclass(value.errno) + + raise cls(f"{func.__name__}{args!r}") + + +_error_maker = ErrorMaker() +checked_call = _error_maker.checked_call + + +def __getattr__(attr: str) -> type[Error]: + return getattr(_error_maker, attr) # type: ignore[no-any-return] diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_py/path.py b/Backend/venv/lib/python3.12/site-packages/_pytest/_py/path.py new file mode 100644 index 00000000..b7131b08 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/_py/path.py @@ -0,0 +1,1475 @@ +# mypy: allow-untyped-defs +"""local path implementation.""" + +from __future__ import annotations + +import atexit +from collections.abc import Callable +from contextlib import contextmanager +import fnmatch +import importlib.util +import io +import os +from os.path import abspath +from os.path import dirname +from os.path import exists +from os.path import isabs +from os.path import isdir +from os.path import isfile +from os.path import islink +from os.path import normpath +import posixpath +from stat import S_ISDIR +from stat import S_ISLNK +from stat import S_ISREG +import sys +from typing import Any +from typing import cast +from typing import Literal +from typing import overload +from typing import TYPE_CHECKING +import uuid +import warnings + +from . import error + + +# Moved from local.py. +iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt") + + +class Checkers: + _depend_on_existence = "exists", "link", "dir", "file" + + def __init__(self, path): + self.path = path + + def dotfile(self): + return self.path.basename.startswith(".") + + def ext(self, arg): + if not arg.startswith("."): + arg = "." + arg + return self.path.ext == arg + + def basename(self, arg): + return self.path.basename == arg + + def basestarts(self, arg): + return self.path.basename.startswith(arg) + + def relto(self, arg): + return self.path.relto(arg) + + def fnmatch(self, arg): + return self.path.fnmatch(arg) + + def endswith(self, arg): + return str(self.path).endswith(arg) + + def _evaluate(self, kw): + from .._code.source import getrawcode + + for name, value in kw.items(): + invert = False + meth = None + try: + meth = getattr(self, name) + except AttributeError: + if name[:3] == "not": + invert = True + try: + meth = getattr(self, name[3:]) + except AttributeError: + pass + if meth is None: + raise TypeError(f"no {name!r} checker available for {self.path!r}") + try: + if getrawcode(meth).co_argcount > 1: + if (not meth(value)) ^ invert: + return False + else: + if bool(value) ^ bool(meth()) ^ invert: + return False + except (error.ENOENT, error.ENOTDIR, error.EBUSY): + # EBUSY feels not entirely correct, + # but its kind of necessary since ENOMEDIUM + # is not accessible in python + for name in self._depend_on_existence: + if name in kw: + if kw.get(name): + return False + name = "not" + name + if name in kw: + if not kw.get(name): + return False + return True + + _statcache: Stat + + def _stat(self) -> Stat: + try: + return self._statcache + except AttributeError: + try: + self._statcache = self.path.stat() + except error.ELOOP: + self._statcache = self.path.lstat() + return self._statcache + + def dir(self): + return S_ISDIR(self._stat().mode) + + def file(self): + return S_ISREG(self._stat().mode) + + def exists(self): + return self._stat() + + def link(self): + st = self.path.lstat() + return S_ISLNK(st.mode) + + +class NeverRaised(Exception): + pass + + +class Visitor: + def __init__(self, fil, rec, ignore, bf, sort): + if isinstance(fil, str): + fil = FNMatcher(fil) + if isinstance(rec, str): + self.rec: Callable[[LocalPath], bool] = FNMatcher(rec) + elif not hasattr(rec, "__call__") and rec: + self.rec = lambda path: True + else: + self.rec = rec + self.fil = fil + self.ignore = ignore + self.breadthfirst = bf + self.optsort = cast(Callable[[Any], Any], sorted) if sort else (lambda x: x) + + def gen(self, path): + try: + entries = path.listdir() + except self.ignore: + return + rec = self.rec + dirs = self.optsort( + [p for p in entries if p.check(dir=1) and (rec is None or rec(p))] + ) + if not self.breadthfirst: + for subdir in dirs: + yield from self.gen(subdir) + for p in self.optsort(entries): + if self.fil is None or self.fil(p): + yield p + if self.breadthfirst: + for subdir in dirs: + yield from self.gen(subdir) + + +class FNMatcher: + def __init__(self, pattern): + self.pattern = pattern + + def __call__(self, path): + pattern = self.pattern + + if ( + pattern.find(path.sep) == -1 + and iswin32 + and pattern.find(posixpath.sep) != -1 + ): + # Running on Windows, the pattern has no Windows path separators, + # and the pattern has one or more Posix path separators. Replace + # the Posix path separators with the Windows path separator. + pattern = pattern.replace(posixpath.sep, path.sep) + + if pattern.find(path.sep) == -1: + name = path.basename + else: + name = str(path) # path.strpath # XXX svn? + if not os.path.isabs(pattern): + pattern = "*" + path.sep + pattern + return fnmatch.fnmatch(name, pattern) + + +def map_as_list(func, iter): + return list(map(func, iter)) + + +class Stat: + if TYPE_CHECKING: + + @property + def size(self) -> int: ... + + @property + def mtime(self) -> float: ... + + def __getattr__(self, name: str) -> Any: + return getattr(self._osstatresult, "st_" + name) + + def __init__(self, path, osstatresult): + self.path = path + self._osstatresult = osstatresult + + @property + def owner(self): + if iswin32: + raise NotImplementedError("XXX win32") + import pwd + + entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined,unused-ignore] + return entry[0] + + @property + def group(self): + """Return group name of file.""" + if iswin32: + raise NotImplementedError("XXX win32") + import grp + + entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined,unused-ignore] + return entry[0] + + def isdir(self): + return S_ISDIR(self._osstatresult.st_mode) + + def isfile(self): + return S_ISREG(self._osstatresult.st_mode) + + def islink(self): + self.path.lstat() + return S_ISLNK(self._osstatresult.st_mode) + + +def getuserid(user): + import pwd + + if not isinstance(user, int): + user = pwd.getpwnam(user)[2] # type:ignore[attr-defined,unused-ignore] + return user + + +def getgroupid(group): + import grp + + if not isinstance(group, int): + group = grp.getgrnam(group)[2] # type:ignore[attr-defined,unused-ignore] + return group + + +class LocalPath: + """Object oriented interface to os.path and other local filesystem + related information. + """ + + class ImportMismatchError(ImportError): + """raised on pyimport() if there is a mismatch of __file__'s""" + + sep = os.sep + + def __init__(self, path=None, expanduser=False): + """Initialize and return a local Path instance. + + Path can be relative to the current directory. + If path is None it defaults to the current working directory. + If expanduser is True, tilde-expansion is performed. + Note that Path instances always carry an absolute path. + Note also that passing in a local path object will simply return + the exact same path object. Use new() to get a new copy. + """ + if path is None: + self.strpath = error.checked_call(os.getcwd) + else: + try: + path = os.fspath(path) + except TypeError: + raise ValueError( + "can only pass None, Path instances " + "or non-empty strings to LocalPath" + ) + if expanduser: + path = os.path.expanduser(path) + self.strpath = abspath(path) + + if sys.platform != "win32": + + def chown(self, user, group, rec=0): + """Change ownership to the given user and group. + user and group may be specified by a number or + by a name. if rec is True change ownership + recursively. + """ + uid = getuserid(user) + gid = getgroupid(group) + if rec: + for x in self.visit(rec=lambda x: x.check(link=0)): + if x.check(link=0): + error.checked_call(os.chown, str(x), uid, gid) + error.checked_call(os.chown, str(self), uid, gid) + + def readlink(self) -> str: + """Return value of a symbolic link.""" + # https://github.com/python/mypy/issues/12278 + return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value,unused-ignore] + + def mklinkto(self, oldname): + """Posix style hard link to another name.""" + error.checked_call(os.link, str(oldname), str(self)) + + def mksymlinkto(self, value, absolute=1): + """Create a symbolic link with the given value (pointing to another name).""" + if absolute: + error.checked_call(os.symlink, str(value), self.strpath) + else: + base = self.common(value) + # with posix local paths '/' is always a common base + relsource = self.__class__(value).relto(base) + reldest = self.relto(base) + n = reldest.count(self.sep) + target = self.sep.join(("..",) * n + (relsource,)) + error.checked_call(os.symlink, target, self.strpath) + + def __div__(self, other): + return self.join(os.fspath(other)) + + __truediv__ = __div__ # py3k + + @property + def basename(self): + """Basename part of path.""" + return self._getbyspec("basename")[0] + + @property + def dirname(self): + """Dirname part of path.""" + return self._getbyspec("dirname")[0] + + @property + def purebasename(self): + """Pure base name of the path.""" + return self._getbyspec("purebasename")[0] + + @property + def ext(self): + """Extension of the path (including the '.').""" + return self._getbyspec("ext")[0] + + def read_binary(self): + """Read and return a bytestring from reading the path.""" + with self.open("rb") as f: + return f.read() + + def read_text(self, encoding): + """Read and return a Unicode string from reading the path.""" + with self.open("r", encoding=encoding) as f: + return f.read() + + def read(self, mode="r"): + """Read and return a bytestring from reading the path.""" + with self.open(mode) as f: + return f.read() + + def readlines(self, cr=1): + """Read and return a list of lines from the path. if cr is False, the + newline will be removed from the end of each line.""" + mode = "r" + + if not cr: + content = self.read(mode) + return content.split("\n") + else: + f = self.open(mode) + try: + return f.readlines() + finally: + f.close() + + def load(self): + """(deprecated) return object unpickled from self.read()""" + f = self.open("rb") + try: + import pickle + + return error.checked_call(pickle.load, f) + finally: + f.close() + + def move(self, target): + """Move this path to target.""" + if target.relto(self): + raise error.EINVAL(target, "cannot move path into a subdirectory of itself") + try: + self.rename(target) + except error.EXDEV: # invalid cross-device link + self.copy(target) + self.remove() + + def fnmatch(self, pattern): + """Return true if the basename/fullname matches the glob-'pattern'. + + valid pattern characters:: + + * matches everything + ? matches any single character + [seq] matches any character in seq + [!seq] matches any char not in seq + + If the pattern contains a path-separator then the full path + is used for pattern matching and a '*' is prepended to the + pattern. + + if the pattern doesn't contain a path-separator the pattern + is only matched against the basename. + """ + return FNMatcher(pattern)(self) + + def relto(self, relpath): + """Return a string which is the relative part of the path + to the given 'relpath'. + """ + if not isinstance(relpath, str | LocalPath): + raise TypeError(f"{relpath!r}: not a string or path object") + strrelpath = str(relpath) + if strrelpath and strrelpath[-1] != self.sep: + strrelpath += self.sep + # assert strrelpath[-1] == self.sep + # assert strrelpath[-2] != self.sep + strself = self.strpath + if sys.platform == "win32" or getattr(os, "_name", None) == "nt": + if os.path.normcase(strself).startswith(os.path.normcase(strrelpath)): + return strself[len(strrelpath) :] + elif strself.startswith(strrelpath): + return strself[len(strrelpath) :] + return "" + + def ensure_dir(self, *args): + """Ensure the path joined with args is a directory.""" + return self.ensure(*args, dir=True) + + def bestrelpath(self, dest): + """Return a string which is a relative path from self + (assumed to be a directory) to dest such that + self.join(bestrelpath) == dest and if not such + path can be determined return dest. + """ + try: + if self == dest: + return os.curdir + base = self.common(dest) + if not base: # can be the case on windows + return str(dest) + self2base = self.relto(base) + reldest = dest.relto(base) + if self2base: + n = self2base.count(self.sep) + 1 + else: + n = 0 + lst = [os.pardir] * n + if reldest: + lst.append(reldest) + target = dest.sep.join(lst) + return target + except AttributeError: + return str(dest) + + def exists(self): + return self.check() + + def isdir(self): + return self.check(dir=1) + + def isfile(self): + return self.check(file=1) + + def parts(self, reverse=False): + """Return a root-first list of all ancestor directories + plus the path itself. + """ + current = self + lst = [self] + while 1: + last = current + current = current.dirpath() + if last == current: + break + lst.append(current) + if not reverse: + lst.reverse() + return lst + + def common(self, other): + """Return the common part shared with the other path + or None if there is no common part. + """ + last = None + for x, y in zip(self.parts(), other.parts()): + if x != y: + return last + last = x + return last + + def __add__(self, other): + """Return new path object with 'other' added to the basename""" + return self.new(basename=self.basename + str(other)) + + def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False): + """Yields all paths below the current one + + fil is a filter (glob pattern or callable), if not matching the + path will not be yielded, defaulting to None (everything is + returned) + + rec is a filter (glob pattern or callable) that controls whether + a node is descended, defaulting to None + + ignore is an Exception class that is ignoredwhen calling dirlist() + on any of the paths (by default, all exceptions are reported) + + bf if True will cause a breadthfirst search instead of the + default depthfirst. Default: False + + sort if True will sort entries within each directory level. + """ + yield from Visitor(fil, rec, ignore, bf, sort).gen(self) + + def _sortlist(self, res, sort): + if sort: + if hasattr(sort, "__call__"): + warnings.warn( + DeprecationWarning( + "listdir(sort=callable) is deprecated and breaks on python3" + ), + stacklevel=3, + ) + res.sort(sort) + else: + res.sort() + + def __fspath__(self): + return self.strpath + + def __hash__(self): + s = self.strpath + if iswin32: + s = s.lower() + return hash(s) + + def __eq__(self, other): + s1 = os.fspath(self) + try: + s2 = os.fspath(other) + except TypeError: + return False + if iswin32: + s1 = s1.lower() + try: + s2 = s2.lower() + except AttributeError: + return False + return s1 == s2 + + def __ne__(self, other): + return not (self == other) + + def __lt__(self, other): + return os.fspath(self) < os.fspath(other) + + def __gt__(self, other): + return os.fspath(self) > os.fspath(other) + + def samefile(self, other): + """Return True if 'other' references the same file as 'self'.""" + other = os.fspath(other) + if not isabs(other): + other = abspath(other) + if self == other: + return True + if not hasattr(os.path, "samefile"): + return False + return error.checked_call(os.path.samefile, self.strpath, other) + + def remove(self, rec=1, ignore_errors=False): + """Remove a file or directory (or a directory tree if rec=1). + if ignore_errors is True, errors while removing directories will + be ignored. + """ + if self.check(dir=1, link=0): + if rec: + # force remove of readonly files on windows + if iswin32: + self.chmod(0o700, rec=1) + import shutil + + error.checked_call( + shutil.rmtree, self.strpath, ignore_errors=ignore_errors + ) + else: + error.checked_call(os.rmdir, self.strpath) + else: + if iswin32: + self.chmod(0o700) + error.checked_call(os.remove, self.strpath) + + def computehash(self, hashtype="md5", chunksize=524288): + """Return hexdigest of hashvalue for this file.""" + try: + try: + import hashlib as mod + except ImportError: + if hashtype == "sha1": + hashtype = "sha" + mod = __import__(hashtype) + hash = getattr(mod, hashtype)() + except (AttributeError, ImportError): + raise ValueError(f"Don't know how to compute {hashtype!r} hash") + f = self.open("rb") + try: + while 1: + buf = f.read(chunksize) + if not buf: + return hash.hexdigest() + hash.update(buf) + finally: + f.close() + + def new(self, **kw): + """Create a modified version of this path. + the following keyword arguments modify various path parts:: + + a:/some/path/to/a/file.ext + xx drive + xxxxxxxxxxxxxxxxx dirname + xxxxxxxx basename + xxxx purebasename + xxx ext + """ + obj = object.__new__(self.__class__) + if not kw: + obj.strpath = self.strpath + return obj + drive, dirname, _basename, purebasename, ext = self._getbyspec( + "drive,dirname,basename,purebasename,ext" + ) + if "basename" in kw: + if "purebasename" in kw or "ext" in kw: + raise ValueError(f"invalid specification {kw!r}") + else: + pb = kw.setdefault("purebasename", purebasename) + try: + ext = kw["ext"] + except KeyError: + pass + else: + if ext and not ext.startswith("."): + ext = "." + ext + kw["basename"] = pb + ext + + if "dirname" in kw and not kw["dirname"]: + kw["dirname"] = drive + else: + kw.setdefault("dirname", dirname) + kw.setdefault("sep", self.sep) + obj.strpath = normpath("{dirname}{sep}{basename}".format(**kw)) + return obj + + def _getbyspec(self, spec: str) -> list[str]: + """See new for what 'spec' can be.""" + res = [] + parts = self.strpath.split(self.sep) + + args = filter(None, spec.split(",")) + for name in args: + if name == "drive": + res.append(parts[0]) + elif name == "dirname": + res.append(self.sep.join(parts[:-1])) + else: + basename = parts[-1] + if name == "basename": + res.append(basename) + else: + i = basename.rfind(".") + if i == -1: + purebasename, ext = basename, "" + else: + purebasename, ext = basename[:i], basename[i:] + if name == "purebasename": + res.append(purebasename) + elif name == "ext": + res.append(ext) + else: + raise ValueError(f"invalid part specification {name!r}") + return res + + def dirpath(self, *args, **kwargs): + """Return the directory path joined with any given path arguments.""" + if not kwargs: + path = object.__new__(self.__class__) + path.strpath = dirname(self.strpath) + if args: + path = path.join(*args) + return path + return self.new(basename="").join(*args, **kwargs) + + def join(self, *args: os.PathLike[str], abs: bool = False) -> LocalPath: + """Return a new path by appending all 'args' as path + components. if abs=1 is used restart from root if any + of the args is an absolute path. + """ + sep = self.sep + strargs = [os.fspath(arg) for arg in args] + strpath = self.strpath + if abs: + newargs: list[str] = [] + for arg in reversed(strargs): + if isabs(arg): + strpath = arg + strargs = newargs + break + newargs.insert(0, arg) + # special case for when we have e.g. strpath == "/" + actual_sep = "" if strpath.endswith(sep) else sep + for arg in strargs: + arg = arg.strip(sep) + if iswin32: + # allow unix style paths even on windows. + arg = arg.strip("/") + arg = arg.replace("/", sep) + strpath = strpath + actual_sep + arg + actual_sep = sep + obj = object.__new__(self.__class__) + obj.strpath = normpath(strpath) + return obj + + def open(self, mode="r", ensure=False, encoding=None): + """Return an opened file with the given mode. + + If ensure is True, create parent directories if needed. + """ + if ensure: + self.dirpath().ensure(dir=1) + if encoding: + return error.checked_call( + io.open, + self.strpath, + mode, + encoding=encoding, + ) + return error.checked_call(open, self.strpath, mode) + + def _fastjoin(self, name): + child = object.__new__(self.__class__) + child.strpath = self.strpath + self.sep + name + return child + + def islink(self): + return islink(self.strpath) + + def check(self, **kw): + """Check a path for existence and properties. + + Without arguments, return True if the path exists, otherwise False. + + valid checkers:: + + file = 1 # is a file + file = 0 # is not a file (may not even exist) + dir = 1 # is a dir + link = 1 # is a link + exists = 1 # exists + + You can specify multiple checker definitions, for example:: + + path.check(file=1, link=1) # a link pointing to a file + """ + if not kw: + return exists(self.strpath) + if len(kw) == 1: + if "dir" in kw: + return not kw["dir"] ^ isdir(self.strpath) + if "file" in kw: + return not kw["file"] ^ isfile(self.strpath) + if not kw: + kw = {"exists": 1} + return Checkers(self)._evaluate(kw) + + _patternchars = set("*?[" + os.sep) + + def listdir(self, fil=None, sort=None): + """List directory contents, possibly filter by the given fil func + and possibly sorted. + """ + if fil is None and sort is None: + names = error.checked_call(os.listdir, self.strpath) + return map_as_list(self._fastjoin, names) + if isinstance(fil, str): + if not self._patternchars.intersection(fil): + child = self._fastjoin(fil) + if exists(child.strpath): + return [child] + return [] + fil = FNMatcher(fil) + names = error.checked_call(os.listdir, self.strpath) + res = [] + for name in names: + child = self._fastjoin(name) + if fil is None or fil(child): + res.append(child) + self._sortlist(res, sort) + return res + + def size(self) -> int: + """Return size of the underlying file object""" + return self.stat().size + + def mtime(self) -> float: + """Return last modification time of the path.""" + return self.stat().mtime + + def copy(self, target, mode=False, stat=False): + """Copy path to target. + + If mode is True, will copy permission from path to target. + If stat is True, copy permission, last modification + time, last access time, and flags from path to target. + """ + if self.check(file=1): + if target.check(dir=1): + target = target.join(self.basename) + assert self != target + copychunked(self, target) + if mode: + copymode(self.strpath, target.strpath) + if stat: + copystat(self, target) + else: + + def rec(p): + return p.check(link=0) + + for x in self.visit(rec=rec): + relpath = x.relto(self) + newx = target.join(relpath) + newx.dirpath().ensure(dir=1) + if x.check(link=1): + newx.mksymlinkto(x.readlink()) + continue + elif x.check(file=1): + copychunked(x, newx) + elif x.check(dir=1): + newx.ensure(dir=1) + if mode: + copymode(x.strpath, newx.strpath) + if stat: + copystat(x, newx) + + def rename(self, target): + """Rename this path to target.""" + target = os.fspath(target) + return error.checked_call(os.rename, self.strpath, target) + + def dump(self, obj, bin=1): + """Pickle object into path location""" + f = self.open("wb") + import pickle + + try: + error.checked_call(pickle.dump, obj, f, bin) + finally: + f.close() + + def mkdir(self, *args): + """Create & return the directory joined with args.""" + p = self.join(*args) + error.checked_call(os.mkdir, os.fspath(p)) + return p + + def write_binary(self, data, ensure=False): + """Write binary data into path. If ensure is True create + missing parent directories. + """ + if ensure: + self.dirpath().ensure(dir=1) + with self.open("wb") as f: + f.write(data) + + def write_text(self, data, encoding, ensure=False): + """Write text data into path using the specified encoding. + If ensure is True create missing parent directories. + """ + if ensure: + self.dirpath().ensure(dir=1) + with self.open("w", encoding=encoding) as f: + f.write(data) + + def write(self, data, mode="w", ensure=False): + """Write data into path. If ensure is True create + missing parent directories. + """ + if ensure: + self.dirpath().ensure(dir=1) + if "b" in mode: + if not isinstance(data, bytes): + raise ValueError("can only process bytes") + else: + if not isinstance(data, str): + if not isinstance(data, bytes): + data = str(data) + else: + data = data.decode(sys.getdefaultencoding()) + f = self.open(mode) + try: + f.write(data) + finally: + f.close() + + def _ensuredirs(self): + parent = self.dirpath() + if parent == self: + return self + if parent.check(dir=0): + parent._ensuredirs() + if self.check(dir=0): + try: + self.mkdir() + except error.EEXIST: + # race condition: file/dir created by another thread/process. + # complain if it is not a dir + if self.check(dir=0): + raise + return self + + def ensure(self, *args, **kwargs): + """Ensure that an args-joined path exists (by default as + a file). if you specify a keyword argument 'dir=True' + then the path is forced to be a directory path. + """ + p = self.join(*args) + if kwargs.get("dir", 0): + return p._ensuredirs() + else: + p.dirpath()._ensuredirs() + if not p.check(file=1): + p.open("wb").close() + return p + + @overload + def stat(self, raising: Literal[True] = ...) -> Stat: ... + + @overload + def stat(self, raising: Literal[False]) -> Stat | None: ... + + def stat(self, raising: bool = True) -> Stat | None: + """Return an os.stat() tuple.""" + if raising: + return Stat(self, error.checked_call(os.stat, self.strpath)) + try: + return Stat(self, os.stat(self.strpath)) + except KeyboardInterrupt: + raise + except Exception: + return None + + def lstat(self) -> Stat: + """Return an os.lstat() tuple.""" + return Stat(self, error.checked_call(os.lstat, self.strpath)) + + def setmtime(self, mtime=None): + """Set modification time for the given path. if 'mtime' is None + (the default) then the file's mtime is set to current time. + + Note that the resolution for 'mtime' is platform dependent. + """ + if mtime is None: + return error.checked_call(os.utime, self.strpath, mtime) + try: + return error.checked_call(os.utime, self.strpath, (-1, mtime)) + except error.EINVAL: + return error.checked_call(os.utime, self.strpath, (self.atime(), mtime)) + + def chdir(self): + """Change directory to self and return old current directory""" + try: + old = self.__class__() + except error.ENOENT: + old = None + error.checked_call(os.chdir, self.strpath) + return old + + @contextmanager + def as_cwd(self): + """ + Return a context manager, which changes to the path's dir during the + managed "with" context. + On __enter__ it returns the old dir, which might be ``None``. + """ + old = self.chdir() + try: + yield old + finally: + if old is not None: + old.chdir() + + def realpath(self): + """Return a new path which contains no symbolic links.""" + return self.__class__(os.path.realpath(self.strpath)) + + def atime(self): + """Return last access time of the path.""" + return self.stat().atime + + def __repr__(self): + return f"local({self.strpath!r})" + + def __str__(self): + """Return string representation of the Path.""" + return self.strpath + + def chmod(self, mode, rec=0): + """Change permissions to the given mode. If mode is an + integer it directly encodes the os-specific modes. + if rec is True perform recursively. + """ + if not isinstance(mode, int): + raise TypeError(f"mode {mode!r} must be an integer") + if rec: + for x in self.visit(rec=rec): + error.checked_call(os.chmod, str(x), mode) + error.checked_call(os.chmod, self.strpath, mode) + + def pypkgpath(self): + """Return the Python package path by looking for the last + directory upwards which still contains an __init__.py. + Return None if a pkgpath cannot be determined. + """ + pkgpath = None + for parent in self.parts(reverse=True): + if parent.isdir(): + if not parent.join("__init__.py").exists(): + break + if not isimportable(parent.basename): + break + pkgpath = parent + return pkgpath + + def _ensuresyspath(self, ensuremode, path): + if ensuremode: + s = str(path) + if ensuremode == "append": + if s not in sys.path: + sys.path.append(s) + else: + if s != sys.path[0]: + sys.path.insert(0, s) + + def pyimport(self, modname=None, ensuresyspath=True): + """Return path as an imported python module. + + If modname is None, look for the containing package + and construct an according module name. + The module will be put/looked up in sys.modules. + if ensuresyspath is True then the root dir for importing + the file (taking __init__.py files into account) will + be prepended to sys.path if it isn't there already. + If ensuresyspath=="append" the root dir will be appended + if it isn't already contained in sys.path. + if ensuresyspath is False no modification of syspath happens. + + Special value of ensuresyspath=="importlib" is intended + purely for using in pytest, it is capable only of importing + separate .py files outside packages, e.g. for test suite + without any __init__.py file. It effectively allows having + same-named test modules in different places and offers + mild opt-in via this option. Note that it works only in + recent versions of python. + """ + if not self.check(): + raise error.ENOENT(self) + + if ensuresyspath == "importlib": + if modname is None: + modname = self.purebasename + spec = importlib.util.spec_from_file_location(modname, str(self)) + if spec is None or spec.loader is None: + raise ImportError(f"Can't find module {modname} at location {self!s}") + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + pkgpath = None + if modname is None: + pkgpath = self.pypkgpath() + if pkgpath is not None: + pkgroot = pkgpath.dirpath() + names = self.new(ext="").relto(pkgroot).split(self.sep) + if names[-1] == "__init__": + names.pop() + modname = ".".join(names) + else: + pkgroot = self.dirpath() + modname = self.purebasename + + self._ensuresyspath(ensuresyspath, pkgroot) + __import__(modname) + mod = sys.modules[modname] + if self.basename == "__init__.py": + return mod # we don't check anything as we might + # be in a namespace package ... too icky to check + modfile = mod.__file__ + assert modfile is not None + if modfile[-4:] in (".pyc", ".pyo"): + modfile = modfile[:-1] + elif modfile.endswith("$py.class"): + modfile = modfile[:-9] + ".py" + if modfile.endswith(os.sep + "__init__.py"): + if self.basename != "__init__.py": + modfile = modfile[:-12] + try: + issame = self.samefile(modfile) + except error.ENOENT: + issame = False + if not issame: + ignore = os.getenv("PY_IGNORE_IMPORTMISMATCH") + if ignore != "1": + raise self.ImportMismatchError(modname, modfile, self) + return mod + else: + try: + return sys.modules[modname] + except KeyError: + # we have a custom modname, do a pseudo-import + import types + + mod = types.ModuleType(modname) + mod.__file__ = str(self) + sys.modules[modname] = mod + try: + with open(str(self), "rb") as f: + exec(f.read(), mod.__dict__) + except BaseException: + del sys.modules[modname] + raise + return mod + + def sysexec(self, *argv: os.PathLike[str], **popen_opts: Any) -> str: + """Return stdout text from executing a system child process, + where the 'self' path points to executable. + The process is directly invoked and not through a system shell. + """ + from subprocess import PIPE + from subprocess import Popen + + popen_opts.pop("stdout", None) + popen_opts.pop("stderr", None) + proc = Popen( + [str(self)] + [str(arg) for arg in argv], + **popen_opts, + stdout=PIPE, + stderr=PIPE, + ) + stdout: str | bytes + stdout, stderr = proc.communicate() + ret = proc.wait() + if isinstance(stdout, bytes): + stdout = stdout.decode(sys.getdefaultencoding()) + if ret != 0: + if isinstance(stderr, bytes): + stderr = stderr.decode(sys.getdefaultencoding()) + raise RuntimeError( + ret, + ret, + str(self), + stdout, + stderr, + ) + return stdout + + @classmethod + def sysfind(cls, name, checker=None, paths=None): + """Return a path object found by looking at the systems + underlying PATH specification. If the checker is not None + it will be invoked to filter matching paths. If a binary + cannot be found, None is returned + Note: This is probably not working on plain win32 systems + but may work on cygwin. + """ + if isabs(name): + p = local(name) + if p.check(file=1): + return p + else: + if paths is None: + if iswin32: + paths = os.environ["Path"].split(";") + if "" not in paths and "." not in paths: + paths.append(".") + try: + systemroot = os.environ["SYSTEMROOT"] + except KeyError: + pass + else: + paths = [ + path.replace("%SystemRoot%", systemroot) for path in paths + ] + else: + paths = os.environ["PATH"].split(":") + tryadd = [] + if iswin32: + tryadd += os.environ["PATHEXT"].split(os.pathsep) + tryadd.append("") + + for x in paths: + for addext in tryadd: + p = local(x).join(name, abs=True) + addext + try: + if p.check(file=1): + if checker: + if not checker(p): + continue + return p + except error.EACCES: + pass + return None + + @classmethod + def _gethomedir(cls): + try: + x = os.environ["HOME"] + except KeyError: + try: + x = os.environ["HOMEDRIVE"] + os.environ["HOMEPATH"] + except KeyError: + return None + return cls(x) + + # """ + # special class constructors for local filesystem paths + # """ + @classmethod + def get_temproot(cls): + """Return the system's temporary directory + (where tempfiles are usually created in) + """ + import tempfile + + return local(tempfile.gettempdir()) + + @classmethod + def mkdtemp(cls, rootdir=None): + """Return a Path object pointing to a fresh new temporary directory + (which we created ourselves). + """ + import tempfile + + if rootdir is None: + rootdir = cls.get_temproot() + path = error.checked_call(tempfile.mkdtemp, dir=str(rootdir)) + return cls(path) + + @classmethod + def make_numbered_dir( + cls, prefix="session-", rootdir=None, keep=3, lock_timeout=172800 + ): # two days + """Return unique directory with a number greater than the current + maximum one. The number is assumed to start directly after prefix. + if keep is true directories with a number less than (maxnum-keep) + will be removed. If .lock files are used (lock_timeout non-zero), + algorithm is multi-process safe. + """ + if rootdir is None: + rootdir = cls.get_temproot() + + nprefix = prefix.lower() + + def parse_num(path): + """Parse the number out of a path (if it matches the prefix)""" + nbasename = path.basename.lower() + if nbasename.startswith(nprefix): + try: + return int(nbasename[len(nprefix) :]) + except ValueError: + pass + + def create_lockfile(path): + """Exclusively create lockfile. Throws when failed""" + mypid = os.getpid() + lockfile = path.join(".lock") + if hasattr(lockfile, "mksymlinkto"): + lockfile.mksymlinkto(str(mypid)) + else: + fd = error.checked_call( + os.open, str(lockfile), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644 + ) + with os.fdopen(fd, "w") as f: + f.write(str(mypid)) + return lockfile + + def atexit_remove_lockfile(lockfile): + """Ensure lockfile is removed at process exit""" + mypid = os.getpid() + + def try_remove_lockfile(): + # in a fork() situation, only the last process should + # remove the .lock, otherwise the other processes run the + # risk of seeing their temporary dir disappear. For now + # we remove the .lock in the parent only (i.e. we assume + # that the children finish before the parent). + if os.getpid() != mypid: + return + try: + lockfile.remove() + except error.Error: + pass + + atexit.register(try_remove_lockfile) + + # compute the maximum number currently in use with the prefix + lastmax = None + while True: + maxnum = -1 + for path in rootdir.listdir(): + num = parse_num(path) + if num is not None: + maxnum = max(maxnum, num) + + # make the new directory + try: + udir = rootdir.mkdir(prefix + str(maxnum + 1)) + if lock_timeout: + lockfile = create_lockfile(udir) + atexit_remove_lockfile(lockfile) + except (error.EEXIST, error.ENOENT, error.EBUSY): + # race condition (1): another thread/process created the dir + # in the meantime - try again + # race condition (2): another thread/process spuriously acquired + # lock treating empty directory as candidate + # for removal - try again + # race condition (3): another thread/process tried to create the lock at + # the same time (happened in Python 3.3 on Windows) + # https://ci.appveyor.com/project/pytestbot/py/build/1.0.21/job/ffi85j4c0lqwsfwa + if lastmax == maxnum: + raise + lastmax = maxnum + continue + break + + def get_mtime(path): + """Read file modification time""" + try: + return path.lstat().mtime + except error.Error: + pass + + garbage_prefix = prefix + "garbage-" + + def is_garbage(path): + """Check if path denotes directory scheduled for removal""" + bn = path.basename + return bn.startswith(garbage_prefix) + + # prune old directories + udir_time = get_mtime(udir) + if keep and udir_time: + for path in rootdir.listdir(): + num = parse_num(path) + if num is not None and num <= (maxnum - keep): + try: + # try acquiring lock to remove directory as exclusive user + if lock_timeout: + create_lockfile(path) + except (error.EEXIST, error.ENOENT, error.EBUSY): + path_time = get_mtime(path) + if not path_time: + # assume directory doesn't exist now + continue + if abs(udir_time - path_time) < lock_timeout: + # assume directory with lockfile exists + # and lock timeout hasn't expired yet + continue + + # path dir locked for exclusive use + # and scheduled for removal to avoid another thread/process + # treating it as a new directory or removal candidate + garbage_path = rootdir.join(garbage_prefix + str(uuid.uuid4())) + try: + path.rename(garbage_path) + garbage_path.remove(rec=1) + except KeyboardInterrupt: + raise + except Exception: # this might be error.Error, WindowsError ... + pass + if is_garbage(path): + try: + path.remove(rec=1) + except KeyboardInterrupt: + raise + except Exception: # this might be error.Error, WindowsError ... + pass + + # make link... + try: + username = os.environ["USER"] # linux, et al + except KeyError: + try: + username = os.environ["USERNAME"] # windows + except KeyError: + username = "current" + + src = str(udir) + dest = src[: src.rfind("-")] + "-" + username + try: + os.unlink(dest) + except OSError: + pass + try: + os.symlink(src, dest) + except (OSError, AttributeError, NotImplementedError): + pass + + return udir + + +def copymode(src, dest): + """Copy permission from src to dst.""" + import shutil + + shutil.copymode(src, dest) + + +def copystat(src, dest): + """Copy permission, last modification time, + last access time, and flags from src to dst.""" + import shutil + + shutil.copystat(str(src), str(dest)) + + +def copychunked(src, dest): + chunksize = 524288 # half a meg of bytes + fsrc = src.open("rb") + try: + fdest = dest.open("wb") + try: + while 1: + buf = fsrc.read(chunksize) + if not buf: + break + fdest.write(buf) + finally: + fdest.close() + finally: + fsrc.close() + + +def isimportable(name): + if name and (name[0].isalpha() or name[0] == "_"): + name = name.replace("_", "") + return not name or name.isalnum() + + +local = LocalPath diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/_version.py b/Backend/venv/lib/python3.12/site-packages/_pytest/_version.py new file mode 100644 index 00000000..25a26b42 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/_version.py @@ -0,0 +1,34 @@ +# file generated by setuptools-scm +# don't change, don't track in version control + +__all__ = [ + "__version__", + "__version_tuple__", + "version", + "version_tuple", + "__commit_id__", + "commit_id", +] + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple + from typing import Union + + VERSION_TUPLE = Tuple[Union[int, str], ...] + COMMIT_ID = Union[str, None] +else: + VERSION_TUPLE = object + COMMIT_ID = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE +commit_id: COMMIT_ID +__commit_id__: COMMIT_ID + +__version__ = version = '9.0.1' +__version_tuple__ = version_tuple = (9, 0, 1) + +__commit_id__ = commit_id = None diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/assertion/__init__.py b/Backend/venv/lib/python3.12/site-packages/_pytest/assertion/__init__.py new file mode 100644 index 00000000..22f3ca8e --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/assertion/__init__.py @@ -0,0 +1,208 @@ +# mypy: allow-untyped-defs +"""Support for presenting detailed information in failing assertions.""" + +from __future__ import annotations + +from collections.abc import Generator +import sys +from typing import Any +from typing import Protocol +from typing import TYPE_CHECKING + +from _pytest.assertion import rewrite +from _pytest.assertion import truncate +from _pytest.assertion import util +from _pytest.assertion.rewrite import assertstate_key +from _pytest.config import Config +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.nodes import Item + + +if TYPE_CHECKING: + from _pytest.main import Session + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("debugconfig") + group.addoption( + "--assert", + action="store", + dest="assertmode", + choices=("rewrite", "plain"), + default="rewrite", + metavar="MODE", + help=( + "Control assertion debugging tools.\n" + "'plain' performs no assertion debugging.\n" + "'rewrite' (the default) rewrites assert statements in test modules" + " on import to provide assert expression information." + ), + ) + parser.addini( + "enable_assertion_pass_hook", + type="bool", + default=False, + help="Enables the pytest_assertion_pass hook. " + "Make sure to delete any previously generated pyc cache files.", + ) + + parser.addini( + "truncation_limit_lines", + default=None, + help="Set threshold of LINES after which truncation will take effect", + ) + parser.addini( + "truncation_limit_chars", + default=None, + help=("Set threshold of CHARS after which truncation will take effect"), + ) + + Config._add_verbosity_ini( + parser, + Config.VERBOSITY_ASSERTIONS, + help=( + "Specify a verbosity level for assertions, overriding the main level. " + "Higher levels will provide more detailed explanation when an assertion fails." + ), + ) + + +def register_assert_rewrite(*names: str) -> None: + """Register one or more module names to be rewritten on import. + + This function will make sure that this module or all modules inside + the package will get their assert statements rewritten. + Thus you should make sure to call this before the module is + actually imported, usually in your __init__.py if you are a plugin + using a package. + + :param names: The module names to register. + """ + for name in names: + if not isinstance(name, str): + msg = "expected module names as *args, got {0} instead" # type: ignore[unreachable] + raise TypeError(msg.format(repr(names))) + rewrite_hook: RewriteHook + for hook in sys.meta_path: + if isinstance(hook, rewrite.AssertionRewritingHook): + rewrite_hook = hook + break + else: + rewrite_hook = DummyRewriteHook() + rewrite_hook.mark_rewrite(*names) + + +class RewriteHook(Protocol): + def mark_rewrite(self, *names: str) -> None: ... + + +class DummyRewriteHook: + """A no-op import hook for when rewriting is disabled.""" + + def mark_rewrite(self, *names: str) -> None: + pass + + +class AssertionState: + """State for the assertion plugin.""" + + def __init__(self, config: Config, mode) -> None: + self.mode = mode + self.trace = config.trace.root.get("assertion") + self.hook: rewrite.AssertionRewritingHook | None = None + + +def install_importhook(config: Config) -> rewrite.AssertionRewritingHook: + """Try to install the rewrite hook, raise SystemError if it fails.""" + config.stash[assertstate_key] = AssertionState(config, "rewrite") + config.stash[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config) + sys.meta_path.insert(0, hook) + config.stash[assertstate_key].trace("installed rewrite import hook") + + def undo() -> None: + hook = config.stash[assertstate_key].hook + if hook is not None and hook in sys.meta_path: + sys.meta_path.remove(hook) + + config.add_cleanup(undo) + return hook + + +def pytest_collection(session: Session) -> None: + # This hook is only called when test modules are collected + # so for example not in the managing process of pytest-xdist + # (which does not collect test modules). + assertstate = session.config.stash.get(assertstate_key, None) + if assertstate: + if assertstate.hook is not None: + assertstate.hook.set_session(session) + + +@hookimpl(wrapper=True, tryfirst=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: + """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks. + + The rewrite module will use util._reprcompare if it exists to use custom + reporting via the pytest_assertrepr_compare hook. This sets up this custom + comparison for the test. + """ + ihook = item.ihook + + def callbinrepr(op, left: object, right: object) -> str | None: + """Call the pytest_assertrepr_compare hook and prepare the result. + + This uses the first result from the hook and then ensures the + following: + * Overly verbose explanations are truncated unless configured otherwise + (eg. if running in verbose mode). + * Embedded newlines are escaped to help util.format_explanation() + later. + * If the rewrite mode is used embedded %-characters are replaced + to protect later % formatting. + + The result can be formatted by util.format_explanation() for + pretty printing. + """ + hook_result = ihook.pytest_assertrepr_compare( + config=item.config, op=op, left=left, right=right + ) + for new_expl in hook_result: + if new_expl: + new_expl = truncate.truncate_if_required(new_expl, item) + new_expl = [line.replace("\n", "\\n") for line in new_expl] + res = "\n~".join(new_expl) + if item.config.getvalue("assertmode") == "rewrite": + res = res.replace("%", "%%") + return res + return None + + saved_assert_hooks = util._reprcompare, util._assertion_pass + util._reprcompare = callbinrepr + util._config = item.config + + if ihook.pytest_assertion_pass.get_hookimpls(): + + def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None: + ihook.pytest_assertion_pass(item=item, lineno=lineno, orig=orig, expl=expl) + + util._assertion_pass = call_assertion_pass_hook + + try: + return (yield) + finally: + util._reprcompare, util._assertion_pass = saved_assert_hooks + util._config = None + + +def pytest_sessionfinish(session: Session) -> None: + assertstate = session.config.stash.get(assertstate_key, None) + if assertstate: + if assertstate.hook is not None: + assertstate.hook.set_session(None) + + +def pytest_assertrepr_compare( + config: Config, op: str, left: Any, right: Any +) -> list[str] | None: + return util.assertrepr_compare(config=config, op=op, left=left, right=right) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca0c2513a6c0654661050f2d2b710c7ca75cecc6 GIT binary patch literal 10127 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 On0gPEIr%5pa`Atj_gY2( literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/truncate.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/truncate.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9706f34150dd04cc4277fcff7ef9b2e0efb4cecf GIT binary patch literal 4203 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 AcmMzZ literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/assertion/rewrite.py b/Backend/venv/lib/python3.12/site-packages/_pytest/assertion/rewrite.py new file mode 100644 index 00000000..566549d6 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/assertion/rewrite.py @@ -0,0 +1,1202 @@ +"""Rewrite assertion AST to produce nice error messages.""" + +from __future__ import annotations + +import ast +from collections import defaultdict +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Sequence +import errno +import functools +import importlib.abc +import importlib.machinery +import importlib.util +import io +import itertools +import marshal +import os +from pathlib import Path +from pathlib import PurePath +import struct +import sys +import tokenize +import types +from typing import IO +from typing import TYPE_CHECKING + + +if sys.version_info >= (3, 12): + from importlib.resources.abc import TraversableResources +else: + from importlib.abc import TraversableResources +if sys.version_info < (3, 11): + from importlib.readers import FileReader +else: + from importlib.resources.readers import FileReader + + +from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE +from _pytest._io.saferepr import saferepr +from _pytest._io.saferepr import saferepr_unlimited +from _pytest._version import version +from _pytest.assertion import util +from _pytest.config import Config +from _pytest.fixtures import FixtureFunctionDefinition +from _pytest.main import Session +from _pytest.pathlib import absolutepath +from _pytest.pathlib import fnmatch_ex +from _pytest.stash import StashKey + + +# fmt: off +from _pytest.assertion.util import format_explanation as _format_explanation # noqa:F401, isort:skip +# fmt:on + +if TYPE_CHECKING: + from _pytest.assertion import AssertionState + + +class Sentinel: + pass + + +assertstate_key = StashKey["AssertionState"]() + +# pytest caches rewritten pycs in pycache dirs +PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}" +PYC_EXT = ".py" + ((__debug__ and "c") or "o") +PYC_TAIL = "." + PYTEST_TAG + PYC_EXT + +# Special marker that denotes we have just left a scope definition +_SCOPE_END_MARKER = Sentinel() + + +class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader): + """PEP302/PEP451 import hook which rewrites asserts.""" + + def __init__(self, config: Config) -> None: + self.config = config + try: + self.fnpats = config.getini("python_files") + except ValueError: + self.fnpats = ["test_*.py", "*_test.py"] + self.session: Session | None = None + self._rewritten_names: dict[str, Path] = {} + self._must_rewrite: set[str] = set() + # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file, + # which might result in infinite recursion (#3506) + self._writing_pyc = False + self._basenames_to_check_rewrite = {"conftest"} + self._marked_for_rewrite_cache: dict[str, bool] = {} + self._session_paths_checked = False + + def set_session(self, session: Session | None) -> None: + self.session = session + self._session_paths_checked = False + + # Indirection so we can mock calls to find_spec originated from the hook during testing + _find_spec = importlib.machinery.PathFinder.find_spec + + def find_spec( + self, + name: str, + path: Sequence[str | bytes] | None = None, + target: types.ModuleType | None = None, + ) -> importlib.machinery.ModuleSpec | None: + if self._writing_pyc: + return None + state = self.config.stash[assertstate_key] + if self._early_rewrite_bailout(name, state): + return None + state.trace(f"find_module called for: {name}") + + # Type ignored because mypy is confused about the `self` binding here. + spec = self._find_spec(name, path) # type: ignore + + if spec is None and path is not None: + # With --import-mode=importlib, PathFinder cannot find spec without modifying `sys.path`, + # causing inability to assert rewriting (#12659). + # At this point, try using the file path to find the module spec. + for _path_str in path: + spec = importlib.util.spec_from_file_location(name, _path_str) + if spec is not None: + break + + if ( + # the import machinery could not find a file to import + spec is None + # this is a namespace package (without `__init__.py`) + # there's nothing to rewrite there + or spec.origin is None + # we can only rewrite source files + or not isinstance(spec.loader, importlib.machinery.SourceFileLoader) + # if the file doesn't exist, we can't rewrite it + or not os.path.exists(spec.origin) + ): + return None + else: + fn = spec.origin + + if not self._should_rewrite(name, fn, state): + return None + + return importlib.util.spec_from_file_location( + name, + fn, + loader=self, + submodule_search_locations=spec.submodule_search_locations, + ) + + def create_module( + self, spec: importlib.machinery.ModuleSpec + ) -> types.ModuleType | None: + return None # default behaviour is fine + + def exec_module(self, module: types.ModuleType) -> None: + assert module.__spec__ is not None + assert module.__spec__.origin is not None + fn = Path(module.__spec__.origin) + state = self.config.stash[assertstate_key] + + self._rewritten_names[module.__name__] = fn + + # The requested module looks like a test file, so rewrite it. This is + # the most magical part of the process: load the source, rewrite the + # asserts, and load the rewritten source. We also cache the rewritten + # module code in a special pyc. We must be aware of the possibility of + # concurrent pytest processes rewriting and loading pycs. To avoid + # tricky race conditions, we maintain the following invariant: The + # cached pyc is always a complete, valid pyc. Operations on it must be + # atomic. POSIX's atomic rename comes in handy. + write = not sys.dont_write_bytecode + cache_dir = get_cache_dir(fn) + if write: + ok = try_makedirs(cache_dir) + if not ok: + write = False + state.trace(f"read only directory: {cache_dir}") + + cache_name = fn.name[:-3] + PYC_TAIL + pyc = cache_dir / cache_name + # Notice that even if we're in a read-only directory, I'm going + # to check for a cached pyc. This may not be optimal... + co = _read_pyc(fn, pyc, state.trace) + if co is None: + state.trace(f"rewriting {fn!r}") + source_stat, co = _rewrite_test(fn, self.config) + if write: + self._writing_pyc = True + try: + _write_pyc(state, co, source_stat, pyc) + finally: + self._writing_pyc = False + else: + state.trace(f"found cached rewritten pyc for {fn}") + exec(co, module.__dict__) + + def _early_rewrite_bailout(self, name: str, state: AssertionState) -> bool: + """A fast way to get out of rewriting modules. + + Profiling has shown that the call to PathFinder.find_spec (inside of + the find_spec from this class) is a major slowdown, so, this method + tries to filter what we're sure won't be rewritten before getting to + it. + """ + if self.session is not None and not self._session_paths_checked: + self._session_paths_checked = True + for initial_path in self.session._initialpaths: + # Make something as c:/projects/my_project/path.py -> + # ['c:', 'projects', 'my_project', 'path.py'] + parts = str(initial_path).split(os.sep) + # add 'path' to basenames to be checked. + self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0]) + + # Note: conftest already by default in _basenames_to_check_rewrite. + parts = name.split(".") + if parts[-1] in self._basenames_to_check_rewrite: + return False + + # For matching the name it must be as if it was a filename. + path = PurePath(*parts).with_suffix(".py") + + for pat in self.fnpats: + # if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based + # on the name alone because we need to match against the full path + if os.path.dirname(pat): + return False + if fnmatch_ex(pat, path): + return False + + if self._is_marked_for_rewrite(name, state): + return False + + state.trace(f"early skip of rewriting module: {name}") + return True + + def _should_rewrite(self, name: str, fn: str, state: AssertionState) -> bool: + # always rewrite conftest files + if os.path.basename(fn) == "conftest.py": + state.trace(f"rewriting conftest file: {fn!r}") + return True + + if self.session is not None: + if self.session.isinitpath(absolutepath(fn)): + state.trace(f"matched test file (was specified on cmdline): {fn!r}") + return True + + # modules not passed explicitly on the command line are only + # rewritten if they match the naming convention for test files + fn_path = PurePath(fn) + for pat in self.fnpats: + if fnmatch_ex(pat, fn_path): + state.trace(f"matched test file {fn!r}") + return True + + return self._is_marked_for_rewrite(name, state) + + def _is_marked_for_rewrite(self, name: str, state: AssertionState) -> bool: + try: + return self._marked_for_rewrite_cache[name] + except KeyError: + for marked in self._must_rewrite: + if name == marked or name.startswith(marked + "."): + state.trace(f"matched marked file {name!r} (from {marked!r})") + self._marked_for_rewrite_cache[name] = True + return True + + self._marked_for_rewrite_cache[name] = False + return False + + def mark_rewrite(self, *names: str) -> None: + """Mark import names as needing to be rewritten. + + The named module or package as well as any nested modules will + be rewritten on import. + """ + already_imported = ( + set(names).intersection(sys.modules).difference(self._rewritten_names) + ) + for name in already_imported: + mod = sys.modules[name] + if not AssertionRewriter.is_rewrite_disabled( + mod.__doc__ or "" + ) and not isinstance(mod.__loader__, type(self)): + self._warn_already_imported(name) + self._must_rewrite.update(names) + self._marked_for_rewrite_cache.clear() + + def _warn_already_imported(self, name: str) -> None: + from _pytest.warning_types import PytestAssertRewriteWarning + + self.config.issue_config_time_warning( + PytestAssertRewriteWarning( + f"Module already imported so cannot be rewritten; {name}" + ), + stacklevel=5, + ) + + def get_data(self, pathname: str | bytes) -> bytes: + """Optional PEP302 get_data API.""" + with open(pathname, "rb") as f: + return f.read() + + def get_resource_reader(self, name: str) -> TraversableResources: + return FileReader(types.SimpleNamespace(path=self._rewritten_names[name])) # type: ignore[arg-type] + + +def _write_pyc_fp( + fp: IO[bytes], source_stat: os.stat_result, co: types.CodeType +) -> None: + # Technically, we don't have to have the same pyc format as + # (C)Python, since these "pycs" should never be seen by builtin + # import. However, there's little reason to deviate. + fp.write(importlib.util.MAGIC_NUMBER) + # https://www.python.org/dev/peps/pep-0552/ + flags = b"\x00\x00\x00\x00" + fp.write(flags) + # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903) + mtime = int(source_stat.st_mtime) & 0xFFFFFFFF + size = source_stat.st_size & 0xFFFFFFFF + # " bool: + proc_pyc = f"{pyc}.{os.getpid()}" + try: + with open(proc_pyc, "wb") as fp: + _write_pyc_fp(fp, source_stat, co) + except OSError as e: + state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}") + return False + + try: + os.replace(proc_pyc, pyc) + except OSError as e: + state.trace(f"error writing pyc file at {pyc}: {e}") + # we ignore any failure to write the cache file + # there are many reasons, permission-denied, pycache dir being a + # file etc. + return False + return True + + +def _rewrite_test(fn: Path, config: Config) -> tuple[os.stat_result, types.CodeType]: + """Read and rewrite *fn* and return the code object.""" + stat = os.stat(fn) + source = fn.read_bytes() + strfn = str(fn) + tree = ast.parse(source, filename=strfn) + rewrite_asserts(tree, source, strfn, config) + co = compile(tree, strfn, "exec", dont_inherit=True) + return stat, co + + +def _read_pyc( + source: Path, pyc: Path, trace: Callable[[str], None] = lambda x: None +) -> types.CodeType | None: + """Possibly read a pytest pyc containing rewritten code. + + Return rewritten code if successful or None if not. + """ + try: + fp = open(pyc, "rb") + except OSError: + return None + with fp: + try: + stat_result = os.stat(source) + mtime = int(stat_result.st_mtime) + size = stat_result.st_size + data = fp.read(16) + except OSError as e: + trace(f"_read_pyc({source}): OSError {e}") + return None + # Check for invalid or out of date pyc file. + if len(data) != (16): + trace(f"_read_pyc({source}): invalid pyc (too short)") + return None + if data[:4] != importlib.util.MAGIC_NUMBER: + trace(f"_read_pyc({source}): invalid pyc (bad magic number)") + return None + if data[4:8] != b"\x00\x00\x00\x00": + trace(f"_read_pyc({source}): invalid pyc (unsupported flags)") + return None + mtime_data = data[8:12] + if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF: + trace(f"_read_pyc({source}): out of date") + return None + size_data = data[12:16] + if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF: + trace(f"_read_pyc({source}): invalid pyc (incorrect size)") + return None + try: + co = marshal.load(fp) + except Exception as e: + trace(f"_read_pyc({source}): marshal.load error {e}") + return None + if not isinstance(co, types.CodeType): + trace(f"_read_pyc({source}): not a code object") + return None + return co + + +def rewrite_asserts( + mod: ast.Module, + source: bytes, + module_path: str | None = None, + config: Config | None = None, +) -> None: + """Rewrite the assert statements in mod.""" + AssertionRewriter(module_path, config, source).run(mod) + + +def _saferepr(obj: object) -> str: + r"""Get a safe repr of an object for assertion error messages. + + The assertion formatting (util.format_explanation()) requires + newlines to be escaped since they are a special character for it. + Normally assertion.util.format_explanation() does this but for a + custom repr it is possible to contain one of the special escape + sequences, especially '\n{' and '\n}' are likely to be present in + JSON reprs. + """ + if isinstance(obj, types.MethodType): + # for bound methods, skip redundant information + return obj.__name__ + + maxsize = _get_maxsize_for_saferepr(util._config) + if not maxsize: + return saferepr_unlimited(obj).replace("\n", "\\n") + return saferepr(obj, maxsize=maxsize).replace("\n", "\\n") + + +def _get_maxsize_for_saferepr(config: Config | None) -> int | None: + """Get `maxsize` configuration for saferepr based on the given config object.""" + if config is None: + verbosity = 0 + else: + verbosity = config.get_verbosity(Config.VERBOSITY_ASSERTIONS) + if verbosity >= 2: + return None + if verbosity >= 1: + return DEFAULT_REPR_MAX_SIZE * 10 + return DEFAULT_REPR_MAX_SIZE + + +def _format_assertmsg(obj: object) -> str: + r"""Format the custom assertion message given. + + For strings this simply replaces newlines with '\n~' so that + util.format_explanation() will preserve them instead of escaping + newlines. For other objects saferepr() is used first. + """ + # reprlib appears to have a bug which means that if a string + # contains a newline it gets escaped, however if an object has a + # .__repr__() which contains newlines it does not get escaped. + # However in either case we want to preserve the newline. + replaces = [("\n", "\n~"), ("%", "%%")] + if not isinstance(obj, str): + obj = saferepr(obj, _get_maxsize_for_saferepr(util._config)) + replaces.append(("\\n", "\n~")) + + for r1, r2 in replaces: + obj = obj.replace(r1, r2) + + return obj + + +def _should_repr_global_name(obj: object) -> bool: + if callable(obj): + # For pytest fixtures the __repr__ method provides more information than the function name. + return isinstance(obj, FixtureFunctionDefinition) + + try: + return not hasattr(obj, "__name__") + except Exception: + return True + + +def _format_boolop(explanations: Iterable[str], is_or: bool) -> str: + explanation = "(" + ((is_or and " or ") or " and ").join(explanations) + ")" + return explanation.replace("%", "%%") + + +def _call_reprcompare( + ops: Sequence[str], + results: Sequence[bool], + expls: Sequence[str], + each_obj: Sequence[object], +) -> str: + for i, res, expl in zip(range(len(ops)), results, expls, strict=True): + try: + done = not res + except Exception: + done = True + if done: + break + if util._reprcompare is not None: + custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1]) + if custom is not None: + return custom + return expl + + +def _call_assertion_pass(lineno: int, orig: str, expl: str) -> None: + if util._assertion_pass is not None: + util._assertion_pass(lineno, orig, expl) + + +def _check_if_assertion_pass_impl() -> bool: + """Check if any plugins implement the pytest_assertion_pass hook + in order not to generate explanation unnecessarily (might be expensive).""" + return True if util._assertion_pass else False + + +UNARY_MAP = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"} + +BINOP_MAP = { + ast.BitOr: "|", + ast.BitXor: "^", + ast.BitAnd: "&", + ast.LShift: "<<", + ast.RShift: ">>", + ast.Add: "+", + ast.Sub: "-", + ast.Mult: "*", + ast.Div: "/", + ast.FloorDiv: "//", + ast.Mod: "%%", # escaped for string formatting + ast.Eq: "==", + ast.NotEq: "!=", + ast.Lt: "<", + ast.LtE: "<=", + ast.Gt: ">", + ast.GtE: ">=", + ast.Pow: "**", + ast.Is: "is", + ast.IsNot: "is not", + ast.In: "in", + ast.NotIn: "not in", + ast.MatMult: "@", +} + + +def traverse_node(node: ast.AST) -> Iterator[ast.AST]: + """Recursively yield node and all its children in depth-first order.""" + yield node + for child in ast.iter_child_nodes(node): + yield from traverse_node(child) + + +@functools.lru_cache(maxsize=1) +def _get_assertion_exprs(src: bytes) -> dict[int, str]: + """Return a mapping from {lineno: "assertion test expression"}.""" + ret: dict[int, str] = {} + + depth = 0 + lines: list[str] = [] + assert_lineno: int | None = None + seen_lines: set[int] = set() + + def _write_and_reset() -> None: + nonlocal depth, lines, assert_lineno, seen_lines + assert assert_lineno is not None + ret[assert_lineno] = "".join(lines).rstrip().rstrip("\\") + depth = 0 + lines = [] + assert_lineno = None + seen_lines = set() + + tokens = tokenize.tokenize(io.BytesIO(src).readline) + for tp, source, (lineno, offset), _, line in tokens: + if tp == tokenize.NAME and source == "assert": + assert_lineno = lineno + elif assert_lineno is not None: + # keep track of depth for the assert-message `,` lookup + if tp == tokenize.OP and source in "([{": + depth += 1 + elif tp == tokenize.OP and source in ")]}": + depth -= 1 + + if not lines: + lines.append(line[offset:]) + seen_lines.add(lineno) + # a non-nested comma separates the expression from the message + elif depth == 0 and tp == tokenize.OP and source == ",": + # one line assert with message + if lineno in seen_lines and len(lines) == 1: + offset_in_trimmed = offset + len(lines[-1]) - len(line) + lines[-1] = lines[-1][:offset_in_trimmed] + # multi-line assert with message + elif lineno in seen_lines: + lines[-1] = lines[-1][:offset] + # multi line assert with escaped newline before message + else: + lines.append(line[:offset]) + _write_and_reset() + elif tp in {tokenize.NEWLINE, tokenize.ENDMARKER}: + _write_and_reset() + elif lines and lineno not in seen_lines: + lines.append(line) + seen_lines.add(lineno) + + return ret + + +class AssertionRewriter(ast.NodeVisitor): + """Assertion rewriting implementation. + + The main entrypoint is to call .run() with an ast.Module instance, + this will then find all the assert statements and rewrite them to + provide intermediate values and a detailed assertion error. See + http://pybites.blogspot.be/2011/07/behind-scenes-of-pytests-new-assertion.html + for an overview of how this works. + + The entry point here is .run() which will iterate over all the + statements in an ast.Module and for each ast.Assert statement it + finds call .visit() with it. Then .visit_Assert() takes over and + is responsible for creating new ast statements to replace the + original assert statement: it rewrites the test of an assertion + to provide intermediate values and replace it with an if statement + which raises an assertion error with a detailed explanation in + case the expression is false and calls pytest_assertion_pass hook + if expression is true. + + For this .visit_Assert() uses the visitor pattern to visit all the + AST nodes of the ast.Assert.test field, each visit call returning + an AST node and the corresponding explanation string. During this + state is kept in several instance attributes: + + :statements: All the AST statements which will replace the assert + statement. + + :variables: This is populated by .variable() with each variable + used by the statements so that they can all be set to None at + the end of the statements. + + :variable_counter: Counter to create new unique variables needed + by statements. Variables are created using .variable() and + have the form of "@py_assert0". + + :expl_stmts: The AST statements which will be executed to get + data from the assertion. This is the code which will construct + the detailed assertion message that is used in the AssertionError + or for the pytest_assertion_pass hook. + + :explanation_specifiers: A dict filled by .explanation_param() + with %-formatting placeholders and their corresponding + expressions to use in the building of an assertion message. + This is used by .pop_format_context() to build a message. + + :stack: A stack of the explanation_specifiers dicts maintained by + .push_format_context() and .pop_format_context() which allows + to build another %-formatted string while already building one. + + :scope: A tuple containing the current scope used for variables_overwrite. + + :variables_overwrite: A dict filled with references to variables + that change value within an assert. This happens when a variable is + reassigned with the walrus operator + + This state, except the variables_overwrite, is reset on every new assert + statement visited and used by the other visitors. + """ + + def __init__( + self, module_path: str | None, config: Config | None, source: bytes + ) -> None: + super().__init__() + self.module_path = module_path + self.config = config + if config is not None: + self.enable_assertion_pass_hook = config.getini( + "enable_assertion_pass_hook" + ) + else: + self.enable_assertion_pass_hook = False + self.source = source + self.scope: tuple[ast.AST, ...] = () + self.variables_overwrite: defaultdict[tuple[ast.AST, ...], dict[str, str]] = ( + defaultdict(dict) + ) + + def run(self, mod: ast.Module) -> None: + """Find all assert statements in *mod* and rewrite them.""" + if not mod.body: + # Nothing to do. + return + + # We'll insert some special imports at the top of the module, but after any + # docstrings and __future__ imports, so first figure out where that is. + doc = getattr(mod, "docstring", None) + expect_docstring = doc is None + if doc is not None and self.is_rewrite_disabled(doc): + return + pos = 0 + for item in mod.body: + match item: + case ast.Expr(value=ast.Constant(value=str() as doc)) if ( + expect_docstring + ): + if self.is_rewrite_disabled(doc): + return + expect_docstring = False + case ast.ImportFrom(level=0, module="__future__"): + pass + case _: + break + pos += 1 + # Special case: for a decorated function, set the lineno to that of the + # first decorator, not the `def`. Issue #4984. + if isinstance(item, ast.FunctionDef) and item.decorator_list: + lineno = item.decorator_list[0].lineno + else: + lineno = item.lineno + # Now actually insert the special imports. + aliases = [ + ast.alias("builtins", "@py_builtins", lineno=lineno, col_offset=0), + ast.alias( + "_pytest.assertion.rewrite", + "@pytest_ar", + lineno=lineno, + col_offset=0, + ), + ] + imports = [ + ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases + ] + mod.body[pos:pos] = imports + + # Collect asserts. + self.scope = (mod,) + nodes: list[ast.AST | Sentinel] = [mod] + while nodes: + node = nodes.pop() + if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef): + self.scope = tuple((*self.scope, node)) + nodes.append(_SCOPE_END_MARKER) + if node == _SCOPE_END_MARKER: + self.scope = self.scope[:-1] + continue + assert isinstance(node, ast.AST) + for name, field in ast.iter_fields(node): + if isinstance(field, list): + new: list[ast.AST] = [] + for i, child in enumerate(field): + if isinstance(child, ast.Assert): + # Transform assert. + new.extend(self.visit(child)) + else: + new.append(child) + if isinstance(child, ast.AST): + nodes.append(child) + setattr(node, name, new) + elif ( + isinstance(field, ast.AST) + # Don't recurse into expressions as they can't contain + # asserts. + and not isinstance(field, ast.expr) + ): + nodes.append(field) + + @staticmethod + def is_rewrite_disabled(docstring: str) -> bool: + return "PYTEST_DONT_REWRITE" in docstring + + def variable(self) -> str: + """Get a new variable.""" + # Use a character invalid in python identifiers to avoid clashing. + name = "@py_assert" + str(next(self.variable_counter)) + self.variables.append(name) + return name + + def assign(self, expr: ast.expr) -> ast.Name: + """Give *expr* a name.""" + name = self.variable() + self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr)) + return ast.copy_location(ast.Name(name, ast.Load()), expr) + + def display(self, expr: ast.expr) -> ast.expr: + """Call saferepr on the expression.""" + return self.helper("_saferepr", expr) + + def helper(self, name: str, *args: ast.expr) -> ast.expr: + """Call a helper in this module.""" + py_name = ast.Name("@pytest_ar", ast.Load()) + attr = ast.Attribute(py_name, name, ast.Load()) + return ast.Call(attr, list(args), []) + + def builtin(self, name: str) -> ast.Attribute: + """Return the builtin called *name*.""" + builtin_name = ast.Name("@py_builtins", ast.Load()) + return ast.Attribute(builtin_name, name, ast.Load()) + + def explanation_param(self, expr: ast.expr) -> str: + """Return a new named %-formatting placeholder for expr. + + This creates a %-formatting placeholder for expr in the + current formatting context, e.g. ``%(py0)s``. The placeholder + and expr are placed in the current format context so that it + can be used on the next call to .pop_format_context(). + """ + specifier = "py" + str(next(self.variable_counter)) + self.explanation_specifiers[specifier] = expr + return "%(" + specifier + ")s" + + def push_format_context(self) -> None: + """Create a new formatting context. + + The format context is used for when an explanation wants to + have a variable value formatted in the assertion message. In + this case the value required can be added using + .explanation_param(). Finally .pop_format_context() is used + to format a string of %-formatted values as added by + .explanation_param(). + """ + self.explanation_specifiers: dict[str, ast.expr] = {} + self.stack.append(self.explanation_specifiers) + + def pop_format_context(self, expl_expr: ast.expr) -> ast.Name: + """Format the %-formatted string with current format context. + + The expl_expr should be an str ast.expr instance constructed from + the %-placeholders created by .explanation_param(). This will + add the required code to format said string to .expl_stmts and + return the ast.Name instance of the formatted string. + """ + current = self.stack.pop() + if self.stack: + self.explanation_specifiers = self.stack[-1] + keys: list[ast.expr | None] = [ast.Constant(key) for key in current.keys()] + format_dict = ast.Dict(keys, list(current.values())) + form = ast.BinOp(expl_expr, ast.Mod(), format_dict) + name = "@py_format" + str(next(self.variable_counter)) + if self.enable_assertion_pass_hook: + self.format_variables.append(name) + self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form)) + return ast.Name(name, ast.Load()) + + def generic_visit(self, node: ast.AST) -> tuple[ast.Name, str]: + """Handle expressions we don't have custom code for.""" + assert isinstance(node, ast.expr) + res = self.assign(node) + return res, self.explanation_param(self.display(res)) + + def visit_Assert(self, assert_: ast.Assert) -> list[ast.stmt]: + """Return the AST statements to replace the ast.Assert instance. + + This rewrites the test of an assertion to provide + intermediate values and replace it with an if statement which + raises an assertion error with a detailed explanation in case + the expression is false. + """ + if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1: + import warnings + + from _pytest.warning_types import PytestAssertRewriteWarning + + # TODO: This assert should not be needed. + assert self.module_path is not None + warnings.warn_explicit( + PytestAssertRewriteWarning( + "assertion is always true, perhaps remove parentheses?" + ), + category=None, + filename=self.module_path, + lineno=assert_.lineno, + ) + + self.statements: list[ast.stmt] = [] + self.variables: list[str] = [] + self.variable_counter = itertools.count() + + if self.enable_assertion_pass_hook: + self.format_variables: list[str] = [] + + self.stack: list[dict[str, ast.expr]] = [] + self.expl_stmts: list[ast.stmt] = [] + self.push_format_context() + # Rewrite assert into a bunch of statements. + top_condition, explanation = self.visit(assert_.test) + + negation = ast.UnaryOp(ast.Not(), top_condition) + + if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook + msg = self.pop_format_context(ast.Constant(explanation)) + + # Failed + if assert_.msg: + assertmsg = self.helper("_format_assertmsg", assert_.msg) + gluestr = "\n>assert " + else: + assertmsg = ast.Constant("") + gluestr = "assert " + err_explanation = ast.BinOp(ast.Constant(gluestr), ast.Add(), msg) + err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation) + err_name = ast.Name("AssertionError", ast.Load()) + fmt = self.helper("_format_explanation", err_msg) + exc = ast.Call(err_name, [fmt], []) + raise_ = ast.Raise(exc, None) + statements_fail = [] + statements_fail.extend(self.expl_stmts) + statements_fail.append(raise_) + + # Passed + fmt_pass = self.helper("_format_explanation", msg) + orig = _get_assertion_exprs(self.source)[assert_.lineno] + hook_call_pass = ast.Expr( + self.helper( + "_call_assertion_pass", + ast.Constant(assert_.lineno), + ast.Constant(orig), + fmt_pass, + ) + ) + # If any hooks implement assert_pass hook + hook_impl_test = ast.If( + self.helper("_check_if_assertion_pass_impl"), + [*self.expl_stmts, hook_call_pass], + [], + ) + statements_pass: list[ast.stmt] = [hook_impl_test] + + # Test for assertion condition + main_test = ast.If(negation, statements_fail, statements_pass) + self.statements.append(main_test) + if self.format_variables: + variables: list[ast.expr] = [ + ast.Name(name, ast.Store()) for name in self.format_variables + ] + clear_format = ast.Assign(variables, ast.Constant(None)) + self.statements.append(clear_format) + + else: # Original assertion rewriting + # Create failure message. + body = self.expl_stmts + self.statements.append(ast.If(negation, body, [])) + if assert_.msg: + assertmsg = self.helper("_format_assertmsg", assert_.msg) + explanation = "\n>assert " + explanation + else: + assertmsg = ast.Constant("") + explanation = "assert " + explanation + template = ast.BinOp(assertmsg, ast.Add(), ast.Constant(explanation)) + msg = self.pop_format_context(template) + fmt = self.helper("_format_explanation", msg) + err_name = ast.Name("AssertionError", ast.Load()) + exc = ast.Call(err_name, [fmt], []) + raise_ = ast.Raise(exc, None) + + body.append(raise_) + + # Clear temporary variables by setting them to None. + if self.variables: + variables = [ast.Name(name, ast.Store()) for name in self.variables] + clear = ast.Assign(variables, ast.Constant(None)) + self.statements.append(clear) + # Fix locations (line numbers/column offsets). + for stmt in self.statements: + for node in traverse_node(stmt): + if getattr(node, "lineno", None) is None: + # apply the assertion location to all generated ast nodes without source location + # and preserve the location of existing nodes or generated nodes with an correct location. + ast.copy_location(node, assert_) + return self.statements + + def visit_NamedExpr(self, name: ast.NamedExpr) -> tuple[ast.NamedExpr, str]: + # This method handles the 'walrus operator' repr of the target + # name if it's a local variable or _should_repr_global_name() + # thinks it's acceptable. + locs = ast.Call(self.builtin("locals"), [], []) + target_id = name.target.id + inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs]) + dorepr = self.helper("_should_repr_global_name", name) + test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) + expr = ast.IfExp(test, self.display(name), ast.Constant(target_id)) + return name, self.explanation_param(expr) + + def visit_Name(self, name: ast.Name) -> tuple[ast.Name, str]: + # Display the repr of the name if it's a local variable or + # _should_repr_global_name() thinks it's acceptable. + locs = ast.Call(self.builtin("locals"), [], []) + inlocs = ast.Compare(ast.Constant(name.id), [ast.In()], [locs]) + dorepr = self.helper("_should_repr_global_name", name) + test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) + expr = ast.IfExp(test, self.display(name), ast.Constant(name.id)) + return name, self.explanation_param(expr) + + def visit_BoolOp(self, boolop: ast.BoolOp) -> tuple[ast.Name, str]: + res_var = self.variable() + expl_list = self.assign(ast.List([], ast.Load())) + app = ast.Attribute(expl_list, "append", ast.Load()) + is_or = int(isinstance(boolop.op, ast.Or)) + body = save = self.statements + fail_save = self.expl_stmts + levels = len(boolop.values) - 1 + self.push_format_context() + # Process each operand, short-circuiting if needed. + for i, v in enumerate(boolop.values): + if i: + fail_inner: list[ast.stmt] = [] + # cond is set in a prior loop iteration below + self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa: F821 + self.expl_stmts = fail_inner + match v: + # Check if the left operand is an ast.NamedExpr and the value has already been visited + case ast.Compare( + left=ast.NamedExpr(target=ast.Name(id=target_id)) + ) if target_id in [ + e.id for e in boolop.values[:i] if hasattr(e, "id") + ]: + pytest_temp = self.variable() + self.variables_overwrite[self.scope][target_id] = v.left # type:ignore[assignment] + # mypy's false positive, we're checking that the 'target' attribute exists. + v.left.target.id = pytest_temp # type:ignore[attr-defined] + self.push_format_context() + res, expl = self.visit(v) + body.append(ast.Assign([ast.Name(res_var, ast.Store())], res)) + expl_format = self.pop_format_context(ast.Constant(expl)) + call = ast.Call(app, [expl_format], []) + self.expl_stmts.append(ast.Expr(call)) + if i < levels: + cond: ast.expr = res + if is_or: + cond = ast.UnaryOp(ast.Not(), cond) + inner: list[ast.stmt] = [] + self.statements.append(ast.If(cond, inner, [])) + self.statements = body = inner + self.statements = save + self.expl_stmts = fail_save + expl_template = self.helper("_format_boolop", expl_list, ast.Constant(is_or)) + expl = self.pop_format_context(expl_template) + return ast.Name(res_var, ast.Load()), self.explanation_param(expl) + + def visit_UnaryOp(self, unary: ast.UnaryOp) -> tuple[ast.Name, str]: + pattern = UNARY_MAP[unary.op.__class__] + operand_res, operand_expl = self.visit(unary.operand) + res = self.assign(ast.copy_location(ast.UnaryOp(unary.op, operand_res), unary)) + return res, pattern % (operand_expl,) + + def visit_BinOp(self, binop: ast.BinOp) -> tuple[ast.Name, str]: + symbol = BINOP_MAP[binop.op.__class__] + left_expr, left_expl = self.visit(binop.left) + right_expr, right_expl = self.visit(binop.right) + explanation = f"({left_expl} {symbol} {right_expl})" + res = self.assign( + ast.copy_location(ast.BinOp(left_expr, binop.op, right_expr), binop) + ) + return res, explanation + + def visit_Call(self, call: ast.Call) -> tuple[ast.Name, str]: + new_func, func_expl = self.visit(call.func) + arg_expls = [] + new_args = [] + new_kwargs = [] + for arg in call.args: + if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get( + self.scope, {} + ): + arg = self.variables_overwrite[self.scope][arg.id] # type:ignore[assignment] + res, expl = self.visit(arg) + arg_expls.append(expl) + new_args.append(res) + for keyword in call.keywords: + match keyword.value: + case ast.Name(id=id) if id in self.variables_overwrite.get( + self.scope, {} + ): + keyword.value = self.variables_overwrite[self.scope][id] # type:ignore[assignment] + res, expl = self.visit(keyword.value) + new_kwargs.append(ast.keyword(keyword.arg, res)) + if keyword.arg: + arg_expls.append(keyword.arg + "=" + expl) + else: # **args have `arg` keywords with an .arg of None + arg_expls.append("**" + expl) + + expl = "{}({})".format(func_expl, ", ".join(arg_expls)) + new_call = ast.copy_location(ast.Call(new_func, new_args, new_kwargs), call) + res = self.assign(new_call) + res_expl = self.explanation_param(self.display(res)) + outer_expl = f"{res_expl}\n{{{res_expl} = {expl}\n}}" + return res, outer_expl + + def visit_Starred(self, starred: ast.Starred) -> tuple[ast.Starred, str]: + # A Starred node can appear in a function call. + res, expl = self.visit(starred.value) + new_starred = ast.Starred(res, starred.ctx) + return new_starred, "*" + expl + + def visit_Attribute(self, attr: ast.Attribute) -> tuple[ast.Name, str]: + if not isinstance(attr.ctx, ast.Load): + return self.generic_visit(attr) + value, value_expl = self.visit(attr.value) + res = self.assign( + ast.copy_location(ast.Attribute(value, attr.attr, ast.Load()), attr) + ) + res_expl = self.explanation_param(self.display(res)) + pat = "%s\n{%s = %s.%s\n}" + expl = pat % (res_expl, res_expl, value_expl, attr.attr) + return res, expl + + def visit_Compare(self, comp: ast.Compare) -> tuple[ast.expr, str]: + self.push_format_context() + # We first check if we have overwritten a variable in the previous assert + match comp.left: + case ast.Name(id=name_id) if name_id in self.variables_overwrite.get( + self.scope, {} + ): + comp.left = self.variables_overwrite[self.scope][name_id] # type: ignore[assignment] + case ast.NamedExpr(target=ast.Name(id=target_id)): + self.variables_overwrite[self.scope][target_id] = comp.left # type: ignore[assignment] + left_res, left_expl = self.visit(comp.left) + if isinstance(comp.left, ast.Compare | ast.BoolOp): + left_expl = f"({left_expl})" + res_variables = [self.variable() for i in range(len(comp.ops))] + load_names: list[ast.expr] = [ast.Name(v, ast.Load()) for v in res_variables] + store_names = [ast.Name(v, ast.Store()) for v in res_variables] + it = zip(range(len(comp.ops)), comp.ops, comp.comparators, strict=True) + expls: list[ast.expr] = [] + syms: list[ast.expr] = [] + results = [left_res] + for i, op, next_operand in it: + match (next_operand, left_res): + case ( + ast.NamedExpr(target=ast.Name(id=target_id)), + ast.Name(id=name_id), + ) if target_id == name_id: + next_operand.target.id = self.variable() + self.variables_overwrite[self.scope][name_id] = next_operand # type: ignore[assignment] + + next_res, next_expl = self.visit(next_operand) + if isinstance(next_operand, ast.Compare | ast.BoolOp): + next_expl = f"({next_expl})" + results.append(next_res) + sym = BINOP_MAP[op.__class__] + syms.append(ast.Constant(sym)) + expl = f"{left_expl} {sym} {next_expl}" + expls.append(ast.Constant(expl)) + res_expr = ast.copy_location(ast.Compare(left_res, [op], [next_res]), comp) + self.statements.append(ast.Assign([store_names[i]], res_expr)) + left_res, left_expl = next_res, next_expl + # Use pytest.assertion.util._reprcompare if that's available. + expl_call = self.helper( + "_call_reprcompare", + ast.Tuple(syms, ast.Load()), + ast.Tuple(load_names, ast.Load()), + ast.Tuple(expls, ast.Load()), + ast.Tuple(results, ast.Load()), + ) + if len(comp.ops) > 1: + res: ast.expr = ast.BoolOp(ast.And(), load_names) + else: + res = load_names[0] + + return res, self.explanation_param(self.pop_format_context(expl_call)) + + +def try_makedirs(cache_dir: Path) -> bool: + """Attempt to create the given directory and sub-directories exist. + + Returns True if successful or if it already exists. + """ + try: + os.makedirs(cache_dir, exist_ok=True) + except (FileNotFoundError, NotADirectoryError, FileExistsError): + # One of the path components was not a directory: + # - we're in a zip file + # - it is a file + return False + except PermissionError: + return False + except OSError as e: + # as of now, EROFS doesn't have an equivalent OSError-subclass + # + # squashfuse_ll returns ENOSYS "OSError: [Errno 38] Function not + # implemented" for a read-only error + if e.errno in {errno.EROFS, errno.ENOSYS}: + return False + raise + return True + + +def get_cache_dir(file_path: Path) -> Path: + """Return the cache directory to write .pyc files for the given .py file path.""" + if sys.pycache_prefix: + # given: + # prefix = '/tmp/pycs' + # path = '/home/user/proj/test_app.py' + # we want: + # '/tmp/pycs/home/user/proj' + return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1]) + else: + # classic pycache directory + return file_path.parent / "__pycache__" diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/assertion/truncate.py b/Backend/venv/lib/python3.12/site-packages/_pytest/assertion/truncate.py new file mode 100644 index 00000000..5820e6e8 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/assertion/truncate.py @@ -0,0 +1,137 @@ +"""Utilities for truncating assertion output. + +Current default behaviour is to truncate assertion explanations at +terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI. +""" + +from __future__ import annotations + +from _pytest.compat import running_on_ci +from _pytest.config import Config +from _pytest.nodes import Item + + +DEFAULT_MAX_LINES = 8 +DEFAULT_MAX_CHARS = DEFAULT_MAX_LINES * 80 +USAGE_MSG = "use '-vv' to show" + + +def truncate_if_required(explanation: list[str], item: Item) -> list[str]: + """Truncate this assertion explanation if the given test item is eligible.""" + should_truncate, max_lines, max_chars = _get_truncation_parameters(item) + if should_truncate: + return _truncate_explanation( + explanation, + max_lines=max_lines, + max_chars=max_chars, + ) + return explanation + + +def _get_truncation_parameters(item: Item) -> tuple[bool, int, int]: + """Return the truncation parameters related to the given item, as (should truncate, max lines, max chars).""" + # We do not need to truncate if one of conditions is met: + # 1. Verbosity level is 2 or more; + # 2. Test is being run in CI environment; + # 3. Both truncation_limit_lines and truncation_limit_chars + # .ini parameters are set to 0 explicitly. + max_lines = item.config.getini("truncation_limit_lines") + max_lines = int(max_lines if max_lines is not None else DEFAULT_MAX_LINES) + + max_chars = item.config.getini("truncation_limit_chars") + max_chars = int(max_chars if max_chars is not None else DEFAULT_MAX_CHARS) + + verbose = item.config.get_verbosity(Config.VERBOSITY_ASSERTIONS) + + should_truncate = verbose < 2 and not running_on_ci() + should_truncate = should_truncate and (max_lines > 0 or max_chars > 0) + + return should_truncate, max_lines, max_chars + + +def _truncate_explanation( + input_lines: list[str], + max_lines: int, + max_chars: int, +) -> list[str]: + """Truncate given list of strings that makes up the assertion explanation. + + Truncates to either max_lines, or max_chars - whichever the input reaches + first, taking the truncation explanation into account. The remaining lines + will be replaced by a usage message. + """ + # Check if truncation required + input_char_count = len("".join(input_lines)) + # The length of the truncation explanation depends on the number of lines + # removed but is at least 68 characters: + # The real value is + # 64 (for the base message: + # '...\n...Full output truncated (1 line hidden), use '-vv' to show")' + # ) + # + 1 (for plural) + # + int(math.log10(len(input_lines) - max_lines)) (number of hidden line, at least 1) + # + 3 for the '...' added to the truncated line + # But if there's more than 100 lines it's very likely that we're going to + # truncate, so we don't need the exact value using log10. + tolerable_max_chars = ( + max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...' + ) + # The truncation explanation add two lines to the output + tolerable_max_lines = max_lines + 2 + if ( + len(input_lines) <= tolerable_max_lines + and input_char_count <= tolerable_max_chars + ): + return input_lines + # Truncate first to max_lines, and then truncate to max_chars if necessary + if max_lines > 0: + truncated_explanation = input_lines[:max_lines] + else: + truncated_explanation = input_lines + truncated_char = True + # We reevaluate the need to truncate chars following removal of some lines + if len("".join(truncated_explanation)) > tolerable_max_chars and max_chars > 0: + truncated_explanation = _truncate_by_char_count( + truncated_explanation, max_chars + ) + else: + truncated_char = False + + if truncated_explanation == input_lines: + # No truncation happened, so we do not need to add any explanations + return truncated_explanation + + truncated_line_count = len(input_lines) - len(truncated_explanation) + if truncated_explanation[-1]: + # Add ellipsis and take into account part-truncated final line + truncated_explanation[-1] = truncated_explanation[-1] + "..." + if truncated_char: + # It's possible that we did not remove any char from this line + truncated_line_count += 1 + else: + # Add proper ellipsis when we were able to fit a full line exactly + truncated_explanation[-1] = "..." + return [ + *truncated_explanation, + "", + f"...Full output truncated ({truncated_line_count} line" + f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}", + ] + + +def _truncate_by_char_count(input_lines: list[str], max_chars: int) -> list[str]: + # Find point at which input length exceeds total allowed length + iterated_char_count = 0 + for iterated_index, input_line in enumerate(input_lines): + if iterated_char_count + len(input_line) > max_chars: + break + iterated_char_count += len(input_line) + + # Create truncated explanation with modified final line + truncated_result = input_lines[:iterated_index] + final_line = input_lines[iterated_index] + if final_line: + final_line_truncate_point = max_chars - iterated_char_count + final_line = final_line[:final_line_truncate_point] + truncated_result.append(final_line) + return truncated_result diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/assertion/util.py b/Backend/venv/lib/python3.12/site-packages/_pytest/assertion/util.py new file mode 100644 index 00000000..f35d83a6 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/assertion/util.py @@ -0,0 +1,615 @@ +# mypy: allow-untyped-defs +"""Utilities for assertion debugging.""" + +from __future__ import annotations + +import collections.abc +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Mapping +from collections.abc import Sequence +from collections.abc import Set as AbstractSet +import pprint +from typing import Any +from typing import Literal +from typing import Protocol +from unicodedata import normalize + +from _pytest import outcomes +import _pytest._code +from _pytest._io.pprint import PrettyPrinter +from _pytest._io.saferepr import saferepr +from _pytest._io.saferepr import saferepr_unlimited +from _pytest.compat import running_on_ci +from _pytest.config import Config + + +# The _reprcompare attribute on the util module is used by the new assertion +# interpretation code and assertion rewriter to detect this plugin was +# loaded and in turn call the hooks defined here as part of the +# DebugInterpreter. +_reprcompare: Callable[[str, object, object], str | None] | None = None + +# Works similarly as _reprcompare attribute. Is populated with the hook call +# when pytest_runtest_setup is called. +_assertion_pass: Callable[[int, str, str], None] | None = None + +# Config object which is assigned during pytest_runtest_protocol. +_config: Config | None = None + + +class _HighlightFunc(Protocol): + def __call__(self, source: str, lexer: Literal["diff", "python"] = "python") -> str: + """Apply highlighting to the given source.""" + + +def dummy_highlighter(source: str, lexer: Literal["diff", "python"] = "python") -> str: + """Dummy highlighter that returns the text unprocessed. + + Needed for _notin_text, as the diff gets post-processed to only show the "+" part. + """ + return source + + +def format_explanation(explanation: str) -> str: + r"""Format an explanation. + + Normally all embedded newlines are escaped, however there are + three exceptions: \n{, \n} and \n~. The first two are intended + cover nested explanations, see function and attribute explanations + for examples (.visit_Call(), visit_Attribute()). The last one is + for when one explanation needs to span multiple lines, e.g. when + displaying diffs. + """ + lines = _split_explanation(explanation) + result = _format_lines(lines) + return "\n".join(result) + + +def _split_explanation(explanation: str) -> list[str]: + r"""Return a list of individual lines in the explanation. + + This will return a list of lines split on '\n{', '\n}' and '\n~'. + Any other newlines will be escaped and appear in the line as the + literal '\n' characters. + """ + raw_lines = (explanation or "").split("\n") + lines = [raw_lines[0]] + for values in raw_lines[1:]: + if values and values[0] in ["{", "}", "~", ">"]: + lines.append(values) + else: + lines[-1] += "\\n" + values + return lines + + +def _format_lines(lines: Sequence[str]) -> list[str]: + """Format the individual lines. + + This will replace the '{', '}' and '~' characters of our mini formatting + language with the proper 'where ...', 'and ...' and ' + ...' text, taking + care of indentation along the way. + + Return a list of formatted lines. + """ + result = list(lines[:1]) + stack = [0] + stackcnt = [0] + for line in lines[1:]: + if line.startswith("{"): + if stackcnt[-1]: + s = "and " + else: + s = "where " + stack.append(len(result)) + stackcnt[-1] += 1 + stackcnt.append(0) + result.append(" +" + " " * (len(stack) - 1) + s + line[1:]) + elif line.startswith("}"): + stack.pop() + stackcnt.pop() + result[stack[-1]] += line[1:] + else: + assert line[0] in ["~", ">"] + stack[-1] += 1 + indent = len(stack) if line.startswith("~") else len(stack) - 1 + result.append(" " * indent + line[1:]) + assert len(stack) == 1 + return result + + +def issequence(x: Any) -> bool: + return isinstance(x, collections.abc.Sequence) and not isinstance(x, str) + + +def istext(x: Any) -> bool: + return isinstance(x, str) + + +def isdict(x: Any) -> bool: + return isinstance(x, dict) + + +def isset(x: Any) -> bool: + return isinstance(x, set | frozenset) + + +def isnamedtuple(obj: Any) -> bool: + return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None + + +def isdatacls(obj: Any) -> bool: + return getattr(obj, "__dataclass_fields__", None) is not None + + +def isattrs(obj: Any) -> bool: + return getattr(obj, "__attrs_attrs__", None) is not None + + +def isiterable(obj: Any) -> bool: + try: + iter(obj) + return not istext(obj) + except Exception: + return False + + +def has_default_eq( + obj: object, +) -> bool: + """Check if an instance of an object contains the default eq + + First, we check if the object's __eq__ attribute has __code__, + if so, we check the equally of the method code filename (__code__.co_filename) + to the default one generated by the dataclass and attr module + for dataclasses the default co_filename is , for attrs class, the __eq__ should contain "attrs eq generated" + """ + # inspired from https://github.com/willmcgugan/rich/blob/07d51ffc1aee6f16bd2e5a25b4e82850fb9ed778/rich/pretty.py#L68 + if hasattr(obj.__eq__, "__code__") and hasattr(obj.__eq__.__code__, "co_filename"): + code_filename = obj.__eq__.__code__.co_filename + + if isattrs(obj): + return "attrs generated " in code_filename + + return code_filename == "" # data class + return True + + +def assertrepr_compare( + config, op: str, left: Any, right: Any, use_ascii: bool = False +) -> list[str] | None: + """Return specialised explanations for some operators/operands.""" + verbose = config.get_verbosity(Config.VERBOSITY_ASSERTIONS) + + # Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier. + # See issue #3246. + use_ascii = ( + isinstance(left, str) + and isinstance(right, str) + and normalize("NFD", left) == normalize("NFD", right) + ) + + if verbose > 1: + left_repr = saferepr_unlimited(left, use_ascii=use_ascii) + right_repr = saferepr_unlimited(right, use_ascii=use_ascii) + else: + # XXX: "15 chars indentation" is wrong + # ("E AssertionError: assert "); should use term width. + maxsize = ( + 80 - 15 - len(op) - 2 + ) // 2 # 15 chars indentation, 1 space around op + + left_repr = saferepr(left, maxsize=maxsize, use_ascii=use_ascii) + right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii) + + summary = f"{left_repr} {op} {right_repr}" + highlighter = config.get_terminal_writer()._highlight + + explanation = None + try: + if op == "==": + explanation = _compare_eq_any(left, right, highlighter, verbose) + elif op == "not in": + if istext(left) and istext(right): + explanation = _notin_text(left, right, verbose) + elif op == "!=": + if isset(left) and isset(right): + explanation = ["Both sets are equal"] + elif op == ">=": + if isset(left) and isset(right): + explanation = _compare_gte_set(left, right, highlighter, verbose) + elif op == "<=": + if isset(left) and isset(right): + explanation = _compare_lte_set(left, right, highlighter, verbose) + elif op == ">": + if isset(left) and isset(right): + explanation = _compare_gt_set(left, right, highlighter, verbose) + elif op == "<": + if isset(left) and isset(right): + explanation = _compare_lt_set(left, right, highlighter, verbose) + + except outcomes.Exit: + raise + except Exception: + repr_crash = _pytest._code.ExceptionInfo.from_current()._getreprcrash() + explanation = [ + f"(pytest_assertion plugin: representation of details failed: {repr_crash}.", + " Probably an object has a faulty __repr__.)", + ] + + if not explanation: + return None + + if explanation[0] != "": + explanation = ["", *explanation] + return [summary, *explanation] + + +def _compare_eq_any( + left: Any, right: Any, highlighter: _HighlightFunc, verbose: int = 0 +) -> list[str]: + explanation = [] + if istext(left) and istext(right): + explanation = _diff_text(left, right, highlighter, verbose) + else: + from _pytest.python_api import ApproxBase + + if isinstance(left, ApproxBase) or isinstance(right, ApproxBase): + # Although the common order should be obtained == expected, this ensures both ways + approx_side = left if isinstance(left, ApproxBase) else right + other_side = right if isinstance(left, ApproxBase) else left + + explanation = approx_side._repr_compare(other_side) + elif type(left) is type(right) and ( + isdatacls(left) or isattrs(left) or isnamedtuple(left) + ): + # Note: unlike dataclasses/attrs, namedtuples compare only the + # field values, not the type or field names. But this branch + # intentionally only handles the same-type case, which was often + # used in older code bases before dataclasses/attrs were available. + explanation = _compare_eq_cls(left, right, highlighter, verbose) + elif issequence(left) and issequence(right): + explanation = _compare_eq_sequence(left, right, highlighter, verbose) + elif isset(left) and isset(right): + explanation = _compare_eq_set(left, right, highlighter, verbose) + elif isdict(left) and isdict(right): + explanation = _compare_eq_dict(left, right, highlighter, verbose) + + if isiterable(left) and isiterable(right): + expl = _compare_eq_iterable(left, right, highlighter, verbose) + explanation.extend(expl) + + return explanation + + +def _diff_text( + left: str, right: str, highlighter: _HighlightFunc, verbose: int = 0 +) -> list[str]: + """Return the explanation for the diff between text. + + Unless --verbose is used this will skip leading and trailing + characters which are identical to keep the diff minimal. + """ + from difflib import ndiff + + explanation: list[str] = [] + + if verbose < 1: + i = 0 # just in case left or right has zero length + for i in range(min(len(left), len(right))): + if left[i] != right[i]: + break + if i > 42: + i -= 10 # Provide some context + explanation = [ + f"Skipping {i} identical leading characters in diff, use -v to show" + ] + left = left[i:] + right = right[i:] + if len(left) == len(right): + for i in range(len(left)): + if left[-i] != right[-i]: + break + if i > 42: + i -= 10 # Provide some context + explanation += [ + f"Skipping {i} identical trailing " + "characters in diff, use -v to show" + ] + left = left[:-i] + right = right[:-i] + keepends = True + if left.isspace() or right.isspace(): + left = repr(str(left)) + right = repr(str(right)) + explanation += ["Strings contain only whitespace, escaping them using repr()"] + # "right" is the expected base against which we compare "left", + # see https://github.com/pytest-dev/pytest/issues/3333 + explanation.extend( + highlighter( + "\n".join( + line.strip("\n") + for line in ndiff(right.splitlines(keepends), left.splitlines(keepends)) + ), + lexer="diff", + ).splitlines() + ) + return explanation + + +def _compare_eq_iterable( + left: Iterable[Any], + right: Iterable[Any], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> list[str]: + if verbose <= 0 and not running_on_ci(): + return ["Use -v to get more diff"] + # dynamic import to speedup pytest + import difflib + + left_formatting = PrettyPrinter().pformat(left).splitlines() + right_formatting = PrettyPrinter().pformat(right).splitlines() + + explanation = ["", "Full diff:"] + # "right" is the expected base against which we compare "left", + # see https://github.com/pytest-dev/pytest/issues/3333 + explanation.extend( + highlighter( + "\n".join( + line.rstrip() + for line in difflib.ndiff(right_formatting, left_formatting) + ), + lexer="diff", + ).splitlines() + ) + return explanation + + +def _compare_eq_sequence( + left: Sequence[Any], + right: Sequence[Any], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> list[str]: + comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes) + explanation: list[str] = [] + len_left = len(left) + len_right = len(right) + for i in range(min(len_left, len_right)): + if left[i] != right[i]: + if comparing_bytes: + # when comparing bytes, we want to see their ascii representation + # instead of their numeric values (#5260) + # using a slice gives us the ascii representation: + # >>> s = b'foo' + # >>> s[0] + # 102 + # >>> s[0:1] + # b'f' + left_value = left[i : i + 1] + right_value = right[i : i + 1] + else: + left_value = left[i] + right_value = right[i] + + explanation.append( + f"At index {i} diff:" + f" {highlighter(repr(left_value))} != {highlighter(repr(right_value))}" + ) + break + + if comparing_bytes: + # when comparing bytes, it doesn't help to show the "sides contain one or more + # items" longer explanation, so skip it + + return explanation + + len_diff = len_left - len_right + if len_diff: + if len_diff > 0: + dir_with_more = "Left" + extra = saferepr(left[len_right]) + else: + len_diff = 0 - len_diff + dir_with_more = "Right" + extra = saferepr(right[len_left]) + + if len_diff == 1: + explanation += [ + f"{dir_with_more} contains one more item: {highlighter(extra)}" + ] + else: + explanation += [ + f"{dir_with_more} contains {len_diff} more items, first extra item: {highlighter(extra)}" + ] + return explanation + + +def _compare_eq_set( + left: AbstractSet[Any], + right: AbstractSet[Any], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> list[str]: + explanation = [] + explanation.extend(_set_one_sided_diff("left", left, right, highlighter)) + explanation.extend(_set_one_sided_diff("right", right, left, highlighter)) + return explanation + + +def _compare_gt_set( + left: AbstractSet[Any], + right: AbstractSet[Any], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> list[str]: + explanation = _compare_gte_set(left, right, highlighter) + if not explanation: + return ["Both sets are equal"] + return explanation + + +def _compare_lt_set( + left: AbstractSet[Any], + right: AbstractSet[Any], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> list[str]: + explanation = _compare_lte_set(left, right, highlighter) + if not explanation: + return ["Both sets are equal"] + return explanation + + +def _compare_gte_set( + left: AbstractSet[Any], + right: AbstractSet[Any], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> list[str]: + return _set_one_sided_diff("right", right, left, highlighter) + + +def _compare_lte_set( + left: AbstractSet[Any], + right: AbstractSet[Any], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> list[str]: + return _set_one_sided_diff("left", left, right, highlighter) + + +def _set_one_sided_diff( + posn: str, + set1: AbstractSet[Any], + set2: AbstractSet[Any], + highlighter: _HighlightFunc, +) -> list[str]: + explanation = [] + diff = set1 - set2 + if diff: + explanation.append(f"Extra items in the {posn} set:") + for item in diff: + explanation.append(highlighter(saferepr(item))) + return explanation + + +def _compare_eq_dict( + left: Mapping[Any, Any], + right: Mapping[Any, Any], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> list[str]: + explanation: list[str] = [] + set_left = set(left) + set_right = set(right) + common = set_left.intersection(set_right) + same = {k: left[k] for k in common if left[k] == right[k]} + if same and verbose < 2: + explanation += [f"Omitting {len(same)} identical items, use -vv to show"] + elif same: + explanation += ["Common items:"] + explanation += highlighter(pprint.pformat(same)).splitlines() + diff = {k for k in common if left[k] != right[k]} + if diff: + explanation += ["Differing items:"] + for k in diff: + explanation += [ + highlighter(saferepr({k: left[k]})) + + " != " + + highlighter(saferepr({k: right[k]})) + ] + extra_left = set_left - set_right + len_extra_left = len(extra_left) + if len_extra_left: + explanation.append( + f"Left contains {len_extra_left} more item{'' if len_extra_left == 1 else 's'}:" + ) + explanation.extend( + highlighter(pprint.pformat({k: left[k] for k in extra_left})).splitlines() + ) + extra_right = set_right - set_left + len_extra_right = len(extra_right) + if len_extra_right: + explanation.append( + f"Right contains {len_extra_right} more item{'' if len_extra_right == 1 else 's'}:" + ) + explanation.extend( + highlighter(pprint.pformat({k: right[k] for k in extra_right})).splitlines() + ) + return explanation + + +def _compare_eq_cls( + left: Any, right: Any, highlighter: _HighlightFunc, verbose: int +) -> list[str]: + if not has_default_eq(left): + return [] + if isdatacls(left): + import dataclasses + + all_fields = dataclasses.fields(left) + fields_to_check = [info.name for info in all_fields if info.compare] + elif isattrs(left): + all_fields = left.__attrs_attrs__ + fields_to_check = [field.name for field in all_fields if getattr(field, "eq")] + elif isnamedtuple(left): + fields_to_check = left._fields + else: + assert False + + indent = " " + same = [] + diff = [] + for field in fields_to_check: + if getattr(left, field) == getattr(right, field): + same.append(field) + else: + diff.append(field) + + explanation = [] + if same or diff: + explanation += [""] + if same and verbose < 2: + explanation.append(f"Omitting {len(same)} identical items, use -vv to show") + elif same: + explanation += ["Matching attributes:"] + explanation += highlighter(pprint.pformat(same)).splitlines() + if diff: + explanation += ["Differing attributes:"] + explanation += highlighter(pprint.pformat(diff)).splitlines() + for field in diff: + field_left = getattr(left, field) + field_right = getattr(right, field) + explanation += [ + "", + f"Drill down into differing attribute {field}:", + f"{indent}{field}: {highlighter(repr(field_left))} != {highlighter(repr(field_right))}", + ] + explanation += [ + indent + line + for line in _compare_eq_any( + field_left, field_right, highlighter, verbose + ) + ] + return explanation + + +def _notin_text(term: str, text: str, verbose: int = 0) -> list[str]: + index = text.find(term) + head = text[:index] + tail = text[index + len(term) :] + correct_text = head + tail + diff = _diff_text(text, correct_text, dummy_highlighter, verbose) + newdiff = [f"{saferepr(term, maxsize=42)} is contained here:"] + for line in diff: + if line.startswith("Skipping"): + continue + if line.startswith("- "): + continue + if line.startswith("+ "): + newdiff.append(" " + line[2:]) + else: + newdiff.append(line) + return newdiff diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/cacheprovider.py b/Backend/venv/lib/python3.12/site-packages/_pytest/cacheprovider.py new file mode 100644 index 00000000..4383f105 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/cacheprovider.py @@ -0,0 +1,646 @@ +# mypy: allow-untyped-defs +"""Implementation of the cache provider.""" + +# This plugin was not named "cache" to avoid conflicts with the external +# pytest-cache version. +from __future__ import annotations + +from collections.abc import Generator +from collections.abc import Iterable +import dataclasses +import errno +import json +import os +from pathlib import Path +import tempfile +from typing import final + +from .pathlib import resolve_from_str +from .pathlib import rm_rf +from .reports import CollectReport +from _pytest import nodes +from _pytest._io import TerminalWriter +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import fixture +from _pytest.fixtures import FixtureRequest +from _pytest.main import Session +from _pytest.nodes import Directory +from _pytest.nodes import File +from _pytest.reports import TestReport + + +README_CONTENT = """\ +# pytest cache directory # + +This directory contains data from the pytest's cache plugin, +which provides the `--lf` and `--ff` options, as well as the `cache` fixture. + +**Do not** commit this to version control. + +See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information. +""" + +CACHEDIR_TAG_CONTENT = b"""\ +Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by pytest. +# For information about cache directory tags, see: +# https://bford.info/cachedir/spec.html +""" + + +@final +@dataclasses.dataclass +class Cache: + """Instance of the `cache` fixture.""" + + _cachedir: Path = dataclasses.field(repr=False) + _config: Config = dataclasses.field(repr=False) + + # Sub-directory under cache-dir for directories created by `mkdir()`. + _CACHE_PREFIX_DIRS = "d" + + # Sub-directory under cache-dir for values created by `set()`. + _CACHE_PREFIX_VALUES = "v" + + def __init__( + self, cachedir: Path, config: Config, *, _ispytest: bool = False + ) -> None: + check_ispytest(_ispytest) + self._cachedir = cachedir + self._config = config + + @classmethod + def for_config(cls, config: Config, *, _ispytest: bool = False) -> Cache: + """Create the Cache instance for a Config. + + :meta private: + """ + check_ispytest(_ispytest) + cachedir = cls.cache_dir_from_config(config, _ispytest=True) + if config.getoption("cacheclear") and cachedir.is_dir(): + cls.clear_cache(cachedir, _ispytest=True) + return cls(cachedir, config, _ispytest=True) + + @classmethod + def clear_cache(cls, cachedir: Path, _ispytest: bool = False) -> None: + """Clear the sub-directories used to hold cached directories and values. + + :meta private: + """ + check_ispytest(_ispytest) + for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES): + d = cachedir / prefix + if d.is_dir(): + rm_rf(d) + + @staticmethod + def cache_dir_from_config(config: Config, *, _ispytest: bool = False) -> Path: + """Get the path to the cache directory for a Config. + + :meta private: + """ + check_ispytest(_ispytest) + return resolve_from_str(config.getini("cache_dir"), config.rootpath) + + def warn(self, fmt: str, *, _ispytest: bool = False, **args: object) -> None: + """Issue a cache warning. + + :meta private: + """ + check_ispytest(_ispytest) + import warnings + + from _pytest.warning_types import PytestCacheWarning + + warnings.warn( + PytestCacheWarning(fmt.format(**args) if args else fmt), + self._config.hook, + stacklevel=3, + ) + + def _mkdir(self, path: Path) -> None: + self._ensure_cache_dir_and_supporting_files() + path.mkdir(exist_ok=True, parents=True) + + def mkdir(self, name: str) -> Path: + """Return a directory path object with the given name. + + If the directory does not yet exist, it will be created. You can use + it to manage files to e.g. store/retrieve database dumps across test + sessions. + + .. versionadded:: 7.0 + + :param name: + Must be a string not containing a ``/`` separator. + Make sure the name contains your plugin or application + identifiers to prevent clashes with other cache users. + """ + path = Path(name) + if len(path.parts) > 1: + raise ValueError("name is not allowed to contain path separators") + res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path) + self._mkdir(res) + return res + + def _getvaluepath(self, key: str) -> Path: + return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key)) + + def get(self, key: str, default): + """Return the cached value for the given key. + + If no value was yet cached or the value cannot be read, the specified + default is returned. + + :param key: + Must be a ``/`` separated value. Usually the first + name is the name of your plugin or your application. + :param default: + The value to return in case of a cache-miss or invalid cache value. + """ + path = self._getvaluepath(key) + try: + with path.open("r", encoding="UTF-8") as f: + return json.load(f) + except (ValueError, OSError): + return default + + def set(self, key: str, value: object) -> None: + """Save value for the given key. + + :param key: + Must be a ``/`` separated value. Usually the first + name is the name of your plugin or your application. + :param value: + Must be of any combination of basic python types, + including nested types like lists of dictionaries. + """ + path = self._getvaluepath(key) + try: + self._mkdir(path.parent) + except OSError as exc: + self.warn( + f"could not create cache path {path}: {exc}", + _ispytest=True, + ) + return + data = json.dumps(value, ensure_ascii=False, indent=2) + try: + f = path.open("w", encoding="UTF-8") + except OSError as exc: + self.warn( + f"cache could not write path {path}: {exc}", + _ispytest=True, + ) + else: + with f: + f.write(data) + + def _ensure_cache_dir_and_supporting_files(self) -> None: + """Create the cache dir and its supporting files.""" + if self._cachedir.is_dir(): + return + + self._cachedir.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory( + prefix="pytest-cache-files-", + dir=self._cachedir.parent, + ) as newpath: + path = Path(newpath) + + # Reset permissions to the default, see #12308. + # Note: there's no way to get the current umask atomically, eek. + umask = os.umask(0o022) + os.umask(umask) + path.chmod(0o777 - umask) + + with open(path.joinpath("README.md"), "x", encoding="UTF-8") as f: + f.write(README_CONTENT) + with open(path.joinpath(".gitignore"), "x", encoding="UTF-8") as f: + f.write("# Created by pytest automatically.\n*\n") + with open(path.joinpath("CACHEDIR.TAG"), "xb") as f: + f.write(CACHEDIR_TAG_CONTENT) + + try: + path.rename(self._cachedir) + except OSError as e: + # If 2 concurrent pytests both race to the rename, the loser + # gets "Directory not empty" from the rename. In this case, + # everything is handled so just continue (while letting the + # temporary directory be cleaned up). + # On Windows, the error is a FileExistsError which translates to EEXIST. + if e.errno not in (errno.ENOTEMPTY, errno.EEXIST): + raise + else: + # Create a directory in place of the one we just moved so that + # `TemporaryDirectory`'s cleanup doesn't complain. + # + # TODO: pass ignore_cleanup_errors=True when we no longer support python < 3.10. + # See https://github.com/python/cpython/issues/74168. Note that passing + # delete=False would do the wrong thing in case of errors and isn't supported + # until python 3.12. + path.mkdir() + + +class LFPluginCollWrapper: + def __init__(self, lfplugin: LFPlugin) -> None: + self.lfplugin = lfplugin + self._collected_at_least_one_failure = False + + @hookimpl(wrapper=True) + def pytest_make_collect_report( + self, collector: nodes.Collector + ) -> Generator[None, CollectReport, CollectReport]: + res = yield + if isinstance(collector, Session | Directory): + # Sort any lf-paths to the beginning. + lf_paths = self.lfplugin._last_failed_paths + + # Use stable sort to prioritize last failed. + def sort_key(node: nodes.Item | nodes.Collector) -> bool: + return node.path in lf_paths + + res.result = sorted( + res.result, + key=sort_key, + reverse=True, + ) + + elif isinstance(collector, File): + if collector.path in self.lfplugin._last_failed_paths: + result = res.result + lastfailed = self.lfplugin.lastfailed + + # Only filter with known failures. + if not self._collected_at_least_one_failure: + if not any(x.nodeid in lastfailed for x in result): + return res + self.lfplugin.config.pluginmanager.register( + LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip" + ) + self._collected_at_least_one_failure = True + + session = collector.session + result[:] = [ + x + for x in result + if x.nodeid in lastfailed + # Include any passed arguments (not trivial to filter). + or session.isinitpath(x.path) + # Keep all sub-collectors. + or isinstance(x, nodes.Collector) + ] + + return res + + +class LFPluginCollSkipfiles: + def __init__(self, lfplugin: LFPlugin) -> None: + self.lfplugin = lfplugin + + @hookimpl + def pytest_make_collect_report( + self, collector: nodes.Collector + ) -> CollectReport | None: + if isinstance(collector, File): + if collector.path not in self.lfplugin._last_failed_paths: + self.lfplugin._skipped_files += 1 + + return CollectReport( + collector.nodeid, "passed", longrepr=None, result=[] + ) + return None + + +class LFPlugin: + """Plugin which implements the --lf (run last-failing) option.""" + + def __init__(self, config: Config) -> None: + self.config = config + active_keys = "lf", "failedfirst" + self.active = any(config.getoption(key) for key in active_keys) + assert config.cache + self.lastfailed: dict[str, bool] = config.cache.get("cache/lastfailed", {}) + self._previously_failed_count: int | None = None + self._report_status: str | None = None + self._skipped_files = 0 # count skipped files during collection due to --lf + + if config.getoption("lf"): + self._last_failed_paths = self.get_last_failed_paths() + config.pluginmanager.register( + LFPluginCollWrapper(self), "lfplugin-collwrapper" + ) + + def get_last_failed_paths(self) -> set[Path]: + """Return a set with all Paths of the previously failed nodeids and + their parents.""" + rootpath = self.config.rootpath + result = set() + for nodeid in self.lastfailed: + path = rootpath / nodeid.split("::")[0] + result.add(path) + result.update(path.parents) + return {x for x in result if x.exists()} + + def pytest_report_collectionfinish(self) -> str | None: + if self.active and self.config.get_verbosity() >= 0: + return f"run-last-failure: {self._report_status}" + return None + + def pytest_runtest_logreport(self, report: TestReport) -> None: + if (report.when == "call" and report.passed) or report.skipped: + self.lastfailed.pop(report.nodeid, None) + elif report.failed: + self.lastfailed[report.nodeid] = True + + def pytest_collectreport(self, report: CollectReport) -> None: + passed = report.outcome in ("passed", "skipped") + if passed: + if report.nodeid in self.lastfailed: + self.lastfailed.pop(report.nodeid) + self.lastfailed.update((item.nodeid, True) for item in report.result) + else: + self.lastfailed[report.nodeid] = True + + @hookimpl(wrapper=True, tryfirst=True) + def pytest_collection_modifyitems( + self, config: Config, items: list[nodes.Item] + ) -> Generator[None]: + res = yield + + if not self.active: + return res + + if self.lastfailed: + previously_failed = [] + previously_passed = [] + for item in items: + if item.nodeid in self.lastfailed: + previously_failed.append(item) + else: + previously_passed.append(item) + self._previously_failed_count = len(previously_failed) + + if not previously_failed: + # Running a subset of all tests with recorded failures + # only outside of it. + self._report_status = ( + f"{len(self.lastfailed)} known failures not in selected tests" + ) + else: + if self.config.getoption("lf"): + items[:] = previously_failed + config.hook.pytest_deselected(items=previously_passed) + else: # --failedfirst + items[:] = previously_failed + previously_passed + + noun = "failure" if self._previously_failed_count == 1 else "failures" + suffix = " first" if self.config.getoption("failedfirst") else "" + self._report_status = ( + f"rerun previous {self._previously_failed_count} {noun}{suffix}" + ) + + if self._skipped_files > 0: + files_noun = "file" if self._skipped_files == 1 else "files" + self._report_status += f" (skipped {self._skipped_files} {files_noun})" + else: + self._report_status = "no previously failed tests, " + if self.config.getoption("last_failed_no_failures") == "none": + self._report_status += "deselecting all items." + config.hook.pytest_deselected(items=items[:]) + items[:] = [] + else: + self._report_status += "not deselecting items." + + return res + + def pytest_sessionfinish(self, session: Session) -> None: + config = self.config + if config.getoption("cacheshow") or hasattr(config, "workerinput"): + return + + assert config.cache is not None + saved_lastfailed = config.cache.get("cache/lastfailed", {}) + if saved_lastfailed != self.lastfailed: + config.cache.set("cache/lastfailed", self.lastfailed) + + +class NFPlugin: + """Plugin which implements the --nf (run new-first) option.""" + + def __init__(self, config: Config) -> None: + self.config = config + self.active = config.option.newfirst + assert config.cache is not None + self.cached_nodeids = set(config.cache.get("cache/nodeids", [])) + + @hookimpl(wrapper=True, tryfirst=True) + def pytest_collection_modifyitems(self, items: list[nodes.Item]) -> Generator[None]: + res = yield + + if self.active: + new_items: dict[str, nodes.Item] = {} + other_items: dict[str, nodes.Item] = {} + for item in items: + if item.nodeid not in self.cached_nodeids: + new_items[item.nodeid] = item + else: + other_items[item.nodeid] = item + + items[:] = self._get_increasing_order( + new_items.values() + ) + self._get_increasing_order(other_items.values()) + self.cached_nodeids.update(new_items) + else: + self.cached_nodeids.update(item.nodeid for item in items) + + return res + + def _get_increasing_order(self, items: Iterable[nodes.Item]) -> list[nodes.Item]: + return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) + + def pytest_sessionfinish(self) -> None: + config = self.config + if config.getoption("cacheshow") or hasattr(config, "workerinput"): + return + + if config.getoption("collectonly"): + return + + assert config.cache is not None + config.cache.set("cache/nodeids", sorted(self.cached_nodeids)) + + +def pytest_addoption(parser: Parser) -> None: + """Add command-line options for cache functionality. + + :param parser: Parser object to add command-line options to. + """ + group = parser.getgroup("general") + group.addoption( + "--lf", + "--last-failed", + action="store_true", + dest="lf", + help="Rerun only the tests that failed at the last run (or all if none failed)", + ) + group.addoption( + "--ff", + "--failed-first", + action="store_true", + dest="failedfirst", + help="Run all tests, but run the last failures first. " + "This may re-order tests and thus lead to " + "repeated fixture setup/teardown.", + ) + group.addoption( + "--nf", + "--new-first", + action="store_true", + dest="newfirst", + help="Run tests from new files first, then the rest of the tests " + "sorted by file mtime", + ) + group.addoption( + "--cache-show", + action="append", + nargs="?", + dest="cacheshow", + help=( + "Show cache contents, don't perform collection or tests. " + "Optional argument: glob (default: '*')." + ), + ) + group.addoption( + "--cache-clear", + action="store_true", + dest="cacheclear", + help="Remove all cache contents at start of test run", + ) + cache_dir_default = ".pytest_cache" + if "TOX_ENV_DIR" in os.environ: + cache_dir_default = os.path.join(os.environ["TOX_ENV_DIR"], cache_dir_default) + parser.addini("cache_dir", default=cache_dir_default, help="Cache directory path") + group.addoption( + "--lfnf", + "--last-failed-no-failures", + action="store", + dest="last_failed_no_failures", + choices=("all", "none"), + default="all", + help="With ``--lf``, determines whether to execute tests when there " + "are no previously (known) failures or when no " + "cached ``lastfailed`` data was found. " + "``all`` (the default) runs the full test suite again. " + "``none`` just emits a message about no known failures and exits successfully.", + ) + + +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: + if config.option.cacheshow and not config.option.help: + from _pytest.main import wrap_session + + return wrap_session(config, cacheshow) + return None + + +@hookimpl(tryfirst=True) +def pytest_configure(config: Config) -> None: + """Configure cache system and register related plugins. + + Creates the Cache instance and registers the last-failed (LFPlugin) + and new-first (NFPlugin) plugins with the plugin manager. + + :param config: pytest configuration object. + """ + config.cache = Cache.for_config(config, _ispytest=True) + config.pluginmanager.register(LFPlugin(config), "lfplugin") + config.pluginmanager.register(NFPlugin(config), "nfplugin") + + +@fixture +def cache(request: FixtureRequest) -> Cache: + """Return a cache object that can persist state between testing sessions. + + cache.get(key, default) + cache.set(key, value) + + Keys must be ``/`` separated strings, where the first part is usually the + name of your plugin or application to avoid clashes with other cache users. + + Values can be any object handled by the json stdlib module. + """ + assert request.config.cache is not None + return request.config.cache + + +def pytest_report_header(config: Config) -> str | None: + """Display cachedir with --cache-show and if non-default.""" + if config.option.verbose > 0 or config.getini("cache_dir") != ".pytest_cache": + assert config.cache is not None + cachedir = config.cache._cachedir + # TODO: evaluate generating upward relative paths + # starting with .., ../.. if sensible + + try: + displaypath = cachedir.relative_to(config.rootpath) + except ValueError: + displaypath = cachedir + return f"cachedir: {displaypath}" + return None + + +def cacheshow(config: Config, session: Session) -> int: + """Display cache contents when --cache-show is used. + + Shows cached values and directories matching the specified glob pattern + (default: '*'). Displays cache location, cached test results, and + any cached directories created by plugins. + + :param config: pytest configuration object. + :param session: pytest session object. + :returns: Exit code (0 for success). + """ + from pprint import pformat + + assert config.cache is not None + + tw = TerminalWriter() + tw.line("cachedir: " + str(config.cache._cachedir)) + if not config.cache._cachedir.is_dir(): + tw.line("cache is empty") + return 0 + + glob = config.option.cacheshow[0] + if glob is None: + glob = "*" + + dummy = object() + basedir = config.cache._cachedir + vdir = basedir / Cache._CACHE_PREFIX_VALUES + tw.sep("-", f"cache values for {glob!r}") + for valpath in sorted(x for x in vdir.rglob(glob) if x.is_file()): + key = str(valpath.relative_to(vdir)) + val = config.cache.get(key, dummy) + if val is dummy: + tw.line(f"{key} contains unreadable content, will be ignored") + else: + tw.line(f"{key} contains:") + for line in pformat(val).splitlines(): + tw.line(" " + line) + + ddir = basedir / Cache._CACHE_PREFIX_DIRS + if ddir.is_dir(): + contents = sorted(ddir.rglob(glob)) + tw.sep("-", f"cache directories for {glob!r}") + for p in contents: + # if p.is_dir(): + # print("%s/" % p.relative_to(basedir)) + if p.is_file(): + key = str(p.relative_to(basedir)) + tw.line(f"{key} is a file of length {p.stat().st_size}") + return 0 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/capture.py b/Backend/venv/lib/python3.12/site-packages/_pytest/capture.py new file mode 100644 index 00000000..6d98676b --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/capture.py @@ -0,0 +1,1144 @@ +# mypy: allow-untyped-defs +"""Per-test stdout/stderr capturing mechanism.""" + +from __future__ import annotations + +import abc +import collections +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import Iterator +import contextlib +import io +from io import UnsupportedOperation +import os +import sys +from tempfile import TemporaryFile +from types import TracebackType +from typing import Any +from typing import AnyStr +from typing import BinaryIO +from typing import cast +from typing import Final +from typing import final +from typing import Generic +from typing import Literal +from typing import NamedTuple +from typing import TextIO +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from typing_extensions import Self + +from _pytest.config import Config +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import fixture +from _pytest.fixtures import SubRequest +from _pytest.nodes import Collector +from _pytest.nodes import File +from _pytest.nodes import Item +from _pytest.reports import CollectReport + + +_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"] + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("general") + group.addoption( + "--capture", + action="store", + default="fd", + metavar="method", + choices=["fd", "sys", "no", "tee-sys"], + help="Per-test capturing method: one of fd|sys|no|tee-sys", + ) + group._addoption( # private to use reserved lower-case short option + "-s", + action="store_const", + const="no", + dest="capture", + help="Shortcut for --capture=no", + ) + + +def _colorama_workaround() -> None: + """Ensure colorama is imported so that it attaches to the correct stdio + handles on Windows. + + colorama uses the terminal on import time. So if something does the + first import of colorama while I/O capture is active, colorama will + fail in various ways. + """ + if sys.platform.startswith("win32"): + try: + import colorama # noqa: F401 + except ImportError: + pass + + +def _readline_workaround() -> None: + """Ensure readline is imported early so it attaches to the correct stdio handles. + + This isn't a problem with the default GNU readline implementation, but in + some configurations, Python uses libedit instead (on macOS, and for prebuilt + binaries such as used by uv). + + In theory this is only needed if readline.backend == "libedit", but the + workaround consists of importing readline here, so we already worked around + the issue by the time we could check if we need to. + """ + try: + import readline # noqa: F401 + except ImportError: + pass + + +def _windowsconsoleio_workaround(stream: TextIO) -> None: + """Workaround for Windows Unicode console handling. + + Python 3.6 implemented Unicode console handling for Windows. This works + by reading/writing to the raw console handle using + ``{Read,Write}ConsoleW``. + + The problem is that we are going to ``dup2`` over the stdio file + descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the + handles used by Python to write to the console. Though there is still some + weirdness and the console handle seems to only be closed randomly and not + on the first call to ``CloseHandle``, or maybe it gets reopened with the + same handle value when we suspend capturing. + + The workaround in this case will reopen stdio with a different fd which + also means a different handle by replicating the logic in + "Py_lifecycle.c:initstdio/create_stdio". + + :param stream: + In practice ``sys.stdout`` or ``sys.stderr``, but given + here as parameter for unittesting purposes. + + See https://github.com/pytest-dev/py/issues/103. + """ + if not sys.platform.startswith("win32") or hasattr(sys, "pypy_version_info"): + return + + # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666). + if not hasattr(stream, "buffer"): # type: ignore[unreachable,unused-ignore] + return + + raw_stdout = stream.buffer.raw if hasattr(stream.buffer, "raw") else stream.buffer + + if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined,unused-ignore] + return + + def _reopen_stdio(f, mode): + if not hasattr(stream.buffer, "raw") and mode[0] == "w": + buffering = 0 + else: + buffering = -1 + + return io.TextIOWrapper( + open(os.dup(f.fileno()), mode, buffering), + f.encoding, + f.errors, + f.newlines, + f.line_buffering, + ) + + sys.stdin = _reopen_stdio(sys.stdin, "rb") + sys.stdout = _reopen_stdio(sys.stdout, "wb") + sys.stderr = _reopen_stdio(sys.stderr, "wb") + + +@hookimpl(wrapper=True) +def pytest_load_initial_conftests(early_config: Config) -> Generator[None]: + ns = early_config.known_args_namespace + if ns.capture == "fd": + _windowsconsoleio_workaround(sys.stdout) + _colorama_workaround() + _readline_workaround() + pluginmanager = early_config.pluginmanager + capman = CaptureManager(ns.capture) + pluginmanager.register(capman, "capturemanager") + + # Make sure that capturemanager is properly reset at final shutdown. + early_config.add_cleanup(capman.stop_global_capturing) + + # Finally trigger conftest loading but while capturing (issue #93). + capman.start_global_capturing() + try: + try: + yield + finally: + capman.suspend_global_capture() + except BaseException: + out, err = capman.read_global_capture() + sys.stdout.write(out) + sys.stderr.write(err) + raise + + +# IO Helpers. + + +class EncodedFile(io.TextIOWrapper): + __slots__ = () + + @property + def name(self) -> str: + # Ensure that file.name is a string. Workaround for a Python bug + # fixed in >=3.7.4: https://bugs.python.org/issue36015 + return repr(self.buffer) + + @property + def mode(self) -> str: + # TextIOWrapper doesn't expose a mode, but at least some of our + # tests check it. + assert hasattr(self.buffer, "mode") + return cast(str, self.buffer.mode.replace("b", "")) + + +class CaptureIO(io.TextIOWrapper): + def __init__(self) -> None: + super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True) + + def getvalue(self) -> str: + assert isinstance(self.buffer, io.BytesIO) + return self.buffer.getvalue().decode("UTF-8") + + +class TeeCaptureIO(CaptureIO): + def __init__(self, other: TextIO) -> None: + self._other = other + super().__init__() + + def write(self, s: str) -> int: + super().write(s) + return self._other.write(s) + + +class DontReadFromInput(TextIO): + @property + def encoding(self) -> str: + assert sys.__stdin__ is not None + return sys.__stdin__.encoding + + def read(self, size: int = -1) -> str: + raise OSError( + "pytest: reading from stdin while output is captured! Consider using `-s`." + ) + + readline = read + + def __next__(self) -> str: + return self.readline() + + def readlines(self, hint: int | None = -1) -> list[str]: + raise OSError( + "pytest: reading from stdin while output is captured! Consider using `-s`." + ) + + def __iter__(self) -> Iterator[str]: + return self + + def fileno(self) -> int: + raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()") + + def flush(self) -> None: + raise UnsupportedOperation("redirected stdin is pseudofile, has no flush()") + + def isatty(self) -> bool: + return False + + def close(self) -> None: + pass + + def readable(self) -> bool: + return False + + def seek(self, offset: int, whence: int = 0) -> int: + raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)") + + def seekable(self) -> bool: + return False + + def tell(self) -> int: + raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()") + + def truncate(self, size: int | None = None) -> int: + raise UnsupportedOperation("cannot truncate stdin") + + def write(self, data: str) -> int: + raise UnsupportedOperation("cannot write to stdin") + + def writelines(self, lines: Iterable[str]) -> None: + raise UnsupportedOperation("Cannot write to stdin") + + def writable(self) -> bool: + return False + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + type: type[BaseException] | None, + value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + pass + + @property + def buffer(self) -> BinaryIO: + # The str/bytes doesn't actually matter in this type, so OK to fake. + return self # type: ignore[return-value] + + +# Capture classes. + + +class CaptureBase(abc.ABC, Generic[AnyStr]): + EMPTY_BUFFER: AnyStr + + @abc.abstractmethod + def __init__(self, fd: int) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def start(self) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def done(self) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def suspend(self) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def resume(self) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def writeorg(self, data: AnyStr) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def snap(self) -> AnyStr: + raise NotImplementedError() + + +patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"} + + +class NoCapture(CaptureBase[str]): + EMPTY_BUFFER = "" + + def __init__(self, fd: int) -> None: + pass + + def start(self) -> None: + pass + + def done(self) -> None: + pass + + def suspend(self) -> None: + pass + + def resume(self) -> None: + pass + + def snap(self) -> str: + return "" + + def writeorg(self, data: str) -> None: + pass + + +class SysCaptureBase(CaptureBase[AnyStr]): + def __init__( + self, fd: int, tmpfile: TextIO | None = None, *, tee: bool = False + ) -> None: + name = patchsysdict[fd] + self._old: TextIO = getattr(sys, name) + self.name = name + if tmpfile is None: + if name == "stdin": + tmpfile = DontReadFromInput() + else: + tmpfile = CaptureIO() if not tee else TeeCaptureIO(self._old) + self.tmpfile = tmpfile + self._state = "initialized" + + def repr(self, class_name: str) -> str: + return "<{} {} _old={} _state={!r} tmpfile={!r}>".format( + class_name, + self.name, + (hasattr(self, "_old") and repr(self._old)) or "", + self._state, + self.tmpfile, + ) + + def __repr__(self) -> str: + return "<{} {} _old={} _state={!r} tmpfile={!r}>".format( + self.__class__.__name__, + self.name, + (hasattr(self, "_old") and repr(self._old)) or "", + self._state, + self.tmpfile, + ) + + def _assert_state(self, op: str, states: tuple[str, ...]) -> None: + assert self._state in states, ( + "cannot {} in state {!r}: expected one of {}".format( + op, self._state, ", ".join(states) + ) + ) + + def start(self) -> None: + self._assert_state("start", ("initialized",)) + setattr(sys, self.name, self.tmpfile) + self._state = "started" + + def done(self) -> None: + self._assert_state("done", ("initialized", "started", "suspended", "done")) + if self._state == "done": + return + setattr(sys, self.name, self._old) + del self._old + self.tmpfile.close() + self._state = "done" + + def suspend(self) -> None: + self._assert_state("suspend", ("started", "suspended")) + setattr(sys, self.name, self._old) + self._state = "suspended" + + def resume(self) -> None: + self._assert_state("resume", ("started", "suspended")) + if self._state == "started": + return + setattr(sys, self.name, self.tmpfile) + self._state = "started" + + +class SysCaptureBinary(SysCaptureBase[bytes]): + EMPTY_BUFFER = b"" + + def snap(self) -> bytes: + self._assert_state("snap", ("started", "suspended")) + self.tmpfile.seek(0) + res = self.tmpfile.buffer.read() + self.tmpfile.seek(0) + self.tmpfile.truncate() + return res + + def writeorg(self, data: bytes) -> None: + self._assert_state("writeorg", ("started", "suspended")) + self._old.flush() + self._old.buffer.write(data) + self._old.buffer.flush() + + +class SysCapture(SysCaptureBase[str]): + EMPTY_BUFFER = "" + + def snap(self) -> str: + self._assert_state("snap", ("started", "suspended")) + assert isinstance(self.tmpfile, CaptureIO) + res = self.tmpfile.getvalue() + self.tmpfile.seek(0) + self.tmpfile.truncate() + return res + + def writeorg(self, data: str) -> None: + self._assert_state("writeorg", ("started", "suspended")) + self._old.write(data) + self._old.flush() + + +class FDCaptureBase(CaptureBase[AnyStr]): + def __init__(self, targetfd: int) -> None: + self.targetfd = targetfd + + try: + os.fstat(targetfd) + except OSError: + # FD capturing is conceptually simple -- create a temporary file, + # redirect the FD to it, redirect back when done. But when the + # target FD is invalid it throws a wrench into this lovely scheme. + # + # Tests themselves shouldn't care if the FD is valid, FD capturing + # should work regardless of external circumstances. So falling back + # to just sys capturing is not a good option. + # + # Further complications are the need to support suspend() and the + # possibility of FD reuse (e.g. the tmpfile getting the very same + # target FD). The following approach is robust, I believe. + self.targetfd_invalid: int | None = os.open(os.devnull, os.O_RDWR) + os.dup2(self.targetfd_invalid, targetfd) + else: + self.targetfd_invalid = None + self.targetfd_save = os.dup(targetfd) + + if targetfd == 0: + self.tmpfile = open(os.devnull, encoding="utf-8") + self.syscapture: CaptureBase[str] = SysCapture(targetfd) + else: + self.tmpfile = EncodedFile( + TemporaryFile(buffering=0), + encoding="utf-8", + errors="replace", + newline="", + write_through=True, + ) + if targetfd in patchsysdict: + self.syscapture = SysCapture(targetfd, self.tmpfile) + else: + self.syscapture = NoCapture(targetfd) + + self._state = "initialized" + + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__} {self.targetfd} oldfd={self.targetfd_save} " + f"_state={self._state!r} tmpfile={self.tmpfile!r}>" + ) + + def _assert_state(self, op: str, states: tuple[str, ...]) -> None: + assert self._state in states, ( + "cannot {} in state {!r}: expected one of {}".format( + op, self._state, ", ".join(states) + ) + ) + + def start(self) -> None: + """Start capturing on targetfd using memorized tmpfile.""" + self._assert_state("start", ("initialized",)) + os.dup2(self.tmpfile.fileno(), self.targetfd) + self.syscapture.start() + self._state = "started" + + def done(self) -> None: + """Stop capturing, restore streams, return original capture file, + seeked to position zero.""" + self._assert_state("done", ("initialized", "started", "suspended", "done")) + if self._state == "done": + return + os.dup2(self.targetfd_save, self.targetfd) + os.close(self.targetfd_save) + if self.targetfd_invalid is not None: + if self.targetfd_invalid != self.targetfd: + os.close(self.targetfd) + os.close(self.targetfd_invalid) + self.syscapture.done() + self.tmpfile.close() + self._state = "done" + + def suspend(self) -> None: + self._assert_state("suspend", ("started", "suspended")) + if self._state == "suspended": + return + self.syscapture.suspend() + os.dup2(self.targetfd_save, self.targetfd) + self._state = "suspended" + + def resume(self) -> None: + self._assert_state("resume", ("started", "suspended")) + if self._state == "started": + return + self.syscapture.resume() + os.dup2(self.tmpfile.fileno(), self.targetfd) + self._state = "started" + + +class FDCaptureBinary(FDCaptureBase[bytes]): + """Capture IO to/from a given OS-level file descriptor. + + snap() produces `bytes`. + """ + + EMPTY_BUFFER = b"" + + def snap(self) -> bytes: + self._assert_state("snap", ("started", "suspended")) + self.tmpfile.seek(0) + res = self.tmpfile.buffer.read() + self.tmpfile.seek(0) + self.tmpfile.truncate() + return res # type: ignore[return-value] + + def writeorg(self, data: bytes) -> None: + """Write to original file descriptor.""" + self._assert_state("writeorg", ("started", "suspended")) + os.write(self.targetfd_save, data) + + +class FDCapture(FDCaptureBase[str]): + """Capture IO to/from a given OS-level file descriptor. + + snap() produces text. + """ + + EMPTY_BUFFER = "" + + def snap(self) -> str: + self._assert_state("snap", ("started", "suspended")) + self.tmpfile.seek(0) + res = self.tmpfile.read() + self.tmpfile.seek(0) + self.tmpfile.truncate() + return res + + def writeorg(self, data: str) -> None: + """Write to original file descriptor.""" + self._assert_state("writeorg", ("started", "suspended")) + # XXX use encoding of original stream + os.write(self.targetfd_save, data.encode("utf-8")) + + +# MultiCapture + + +# Generic NamedTuple only supported since Python 3.11. +if sys.version_info >= (3, 11) or TYPE_CHECKING: + + @final + class CaptureResult(NamedTuple, Generic[AnyStr]): + """The result of :method:`caplog.readouterr() `.""" + + out: AnyStr + err: AnyStr + +else: + + class CaptureResult( + collections.namedtuple("CaptureResult", ["out", "err"]), # noqa: PYI024 + Generic[AnyStr], + ): + """The result of :method:`caplog.readouterr() `.""" + + __slots__ = () + + +class MultiCapture(Generic[AnyStr]): + _state = None + _in_suspended = False + + def __init__( + self, + in_: CaptureBase[AnyStr] | None, + out: CaptureBase[AnyStr] | None, + err: CaptureBase[AnyStr] | None, + ) -> None: + self.in_: CaptureBase[AnyStr] | None = in_ + self.out: CaptureBase[AnyStr] | None = out + self.err: CaptureBase[AnyStr] | None = err + + def __repr__(self) -> str: + return ( + f"" + ) + + def start_capturing(self) -> None: + self._state = "started" + if self.in_: + self.in_.start() + if self.out: + self.out.start() + if self.err: + self.err.start() + + def pop_outerr_to_orig(self) -> tuple[AnyStr, AnyStr]: + """Pop current snapshot out/err capture and flush to orig streams.""" + out, err = self.readouterr() + if out: + assert self.out is not None + self.out.writeorg(out) + if err: + assert self.err is not None + self.err.writeorg(err) + return out, err + + def suspend_capturing(self, in_: bool = False) -> None: + self._state = "suspended" + if self.out: + self.out.suspend() + if self.err: + self.err.suspend() + if in_ and self.in_: + self.in_.suspend() + self._in_suspended = True + + def resume_capturing(self) -> None: + self._state = "started" + if self.out: + self.out.resume() + if self.err: + self.err.resume() + if self._in_suspended: + assert self.in_ is not None + self.in_.resume() + self._in_suspended = False + + def stop_capturing(self) -> None: + """Stop capturing and reset capturing streams.""" + if self._state == "stopped": + raise ValueError("was already stopped") + self._state = "stopped" + if self.out: + self.out.done() + if self.err: + self.err.done() + if self.in_: + self.in_.done() + + def is_started(self) -> bool: + """Whether actively capturing -- not suspended or stopped.""" + return self._state == "started" + + def readouterr(self) -> CaptureResult[AnyStr]: + out = self.out.snap() if self.out else "" + err = self.err.snap() if self.err else "" + # TODO: This type error is real, need to fix. + return CaptureResult(out, err) # type: ignore[arg-type] + + +def _get_multicapture(method: _CaptureMethod) -> MultiCapture[str]: + if method == "fd": + return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2)) + elif method == "sys": + return MultiCapture(in_=SysCapture(0), out=SysCapture(1), err=SysCapture(2)) + elif method == "no": + return MultiCapture(in_=None, out=None, err=None) + elif method == "tee-sys": + return MultiCapture( + in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True) + ) + raise ValueError(f"unknown capturing method: {method!r}") + + +# CaptureManager and CaptureFixture + + +class CaptureManager: + """The capture plugin. + + Manages that the appropriate capture method is enabled/disabled during + collection and each test phase (setup, call, teardown). After each of + those points, the captured output is obtained and attached to the + collection/runtest report. + + There are two levels of capture: + + * global: enabled by default and can be suppressed by the ``-s`` + option. This is always enabled/disabled during collection and each test + phase. + + * fixture: when a test function or one of its fixture depend on the + ``capsys`` or ``capfd`` fixtures. In this case special handling is + needed to ensure the fixtures take precedence over the global capture. + """ + + def __init__(self, method: _CaptureMethod) -> None: + self._method: Final = method + self._global_capturing: MultiCapture[str] | None = None + self._capture_fixture: CaptureFixture[Any] | None = None + + def __repr__(self) -> str: + return ( + f"" + ) + + def is_capturing(self) -> str | bool: + if self.is_globally_capturing(): + return "global" + if self._capture_fixture: + return f"fixture {self._capture_fixture.request.fixturename}" + return False + + # Global capturing control + + def is_globally_capturing(self) -> bool: + return self._method != "no" + + def start_global_capturing(self) -> None: + assert self._global_capturing is None + self._global_capturing = _get_multicapture(self._method) + self._global_capturing.start_capturing() + + def stop_global_capturing(self) -> None: + if self._global_capturing is not None: + self._global_capturing.pop_outerr_to_orig() + self._global_capturing.stop_capturing() + self._global_capturing = None + + def resume_global_capture(self) -> None: + # During teardown of the python process, and on rare occasions, capture + # attributes can be `None` while trying to resume global capture. + if self._global_capturing is not None: + self._global_capturing.resume_capturing() + + def suspend_global_capture(self, in_: bool = False) -> None: + if self._global_capturing is not None: + self._global_capturing.suspend_capturing(in_=in_) + + def suspend(self, in_: bool = False) -> None: + # Need to undo local capsys-et-al if it exists before disabling global capture. + self.suspend_fixture() + self.suspend_global_capture(in_) + + def resume(self) -> None: + self.resume_global_capture() + self.resume_fixture() + + def read_global_capture(self) -> CaptureResult[str]: + assert self._global_capturing is not None + return self._global_capturing.readouterr() + + # Fixture Control + + def set_fixture(self, capture_fixture: CaptureFixture[Any]) -> None: + if self._capture_fixture: + current_fixture = self._capture_fixture.request.fixturename + requested_fixture = capture_fixture.request.fixturename + capture_fixture.request.raiseerror( + f"cannot use {requested_fixture} and {current_fixture} at the same time" + ) + self._capture_fixture = capture_fixture + + def unset_fixture(self) -> None: + self._capture_fixture = None + + def activate_fixture(self) -> None: + """If the current item is using ``capsys`` or ``capfd``, activate + them so they take precedence over the global capture.""" + if self._capture_fixture: + self._capture_fixture._start() + + def deactivate_fixture(self) -> None: + """Deactivate the ``capsys`` or ``capfd`` fixture of this item, if any.""" + if self._capture_fixture: + self._capture_fixture.close() + + def suspend_fixture(self) -> None: + if self._capture_fixture: + self._capture_fixture._suspend() + + def resume_fixture(self) -> None: + if self._capture_fixture: + self._capture_fixture._resume() + + # Helper context managers + + @contextlib.contextmanager + def global_and_fixture_disabled(self) -> Generator[None]: + """Context manager to temporarily disable global and current fixture capturing.""" + do_fixture = self._capture_fixture and self._capture_fixture._is_started() + if do_fixture: + self.suspend_fixture() + do_global = self._global_capturing and self._global_capturing.is_started() + if do_global: + self.suspend_global_capture() + try: + yield + finally: + if do_global: + self.resume_global_capture() + if do_fixture: + self.resume_fixture() + + @contextlib.contextmanager + def item_capture(self, when: str, item: Item) -> Generator[None]: + self.resume_global_capture() + self.activate_fixture() + try: + yield + finally: + self.deactivate_fixture() + self.suspend_global_capture(in_=False) + + out, err = self.read_global_capture() + item.add_report_section(when, "stdout", out) + item.add_report_section(when, "stderr", err) + + # Hooks + + @hookimpl(wrapper=True) + def pytest_make_collect_report( + self, collector: Collector + ) -> Generator[None, CollectReport, CollectReport]: + if isinstance(collector, File): + self.resume_global_capture() + try: + rep = yield + finally: + self.suspend_global_capture() + out, err = self.read_global_capture() + if out: + rep.sections.append(("Captured stdout", out)) + if err: + rep.sections.append(("Captured stderr", err)) + else: + rep = yield + return rep + + @hookimpl(wrapper=True) + def pytest_runtest_setup(self, item: Item) -> Generator[None]: + with self.item_capture("setup", item): + return (yield) + + @hookimpl(wrapper=True) + def pytest_runtest_call(self, item: Item) -> Generator[None]: + with self.item_capture("call", item): + return (yield) + + @hookimpl(wrapper=True) + def pytest_runtest_teardown(self, item: Item) -> Generator[None]: + with self.item_capture("teardown", item): + return (yield) + + @hookimpl(tryfirst=True) + def pytest_keyboard_interrupt(self) -> None: + self.stop_global_capturing() + + @hookimpl(tryfirst=True) + def pytest_internalerror(self) -> None: + self.stop_global_capturing() + + +class CaptureFixture(Generic[AnyStr]): + """Object returned by the :fixture:`capsys`, :fixture:`capsysbinary`, + :fixture:`capfd` and :fixture:`capfdbinary` fixtures.""" + + def __init__( + self, + captureclass: type[CaptureBase[AnyStr]], + request: SubRequest, + *, + config: dict[str, Any] | None = None, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + self.captureclass: type[CaptureBase[AnyStr]] = captureclass + self.request = request + self._config = config if config else {} + self._capture: MultiCapture[AnyStr] | None = None + self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER + self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER + + def _start(self) -> None: + if self._capture is None: + self._capture = MultiCapture( + in_=None, + out=self.captureclass(1, **self._config), + err=self.captureclass(2, **self._config), + ) + self._capture.start_capturing() + + def close(self) -> None: + if self._capture is not None: + out, err = self._capture.pop_outerr_to_orig() + self._captured_out += out + self._captured_err += err + self._capture.stop_capturing() + self._capture = None + + def readouterr(self) -> CaptureResult[AnyStr]: + """Read and return the captured output so far, resetting the internal + buffer. + + :returns: + The captured content as a namedtuple with ``out`` and ``err`` + string attributes. + """ + captured_out, captured_err = self._captured_out, self._captured_err + if self._capture is not None: + out, err = self._capture.readouterr() + captured_out += out + captured_err += err + self._captured_out = self.captureclass.EMPTY_BUFFER + self._captured_err = self.captureclass.EMPTY_BUFFER + return CaptureResult(captured_out, captured_err) + + def _suspend(self) -> None: + """Suspend this fixture's own capturing temporarily.""" + if self._capture is not None: + self._capture.suspend_capturing() + + def _resume(self) -> None: + """Resume this fixture's own capturing temporarily.""" + if self._capture is not None: + self._capture.resume_capturing() + + def _is_started(self) -> bool: + """Whether actively capturing -- not disabled or closed.""" + if self._capture is not None: + return self._capture.is_started() + return False + + @contextlib.contextmanager + def disabled(self) -> Generator[None]: + """Temporarily disable capturing while inside the ``with`` block.""" + capmanager: CaptureManager = self.request.config.pluginmanager.getplugin( + "capturemanager" + ) + with capmanager.global_and_fixture_disabled(): + yield + + +# The fixtures. + + +@fixture +def capsys(request: SubRequest) -> Generator[CaptureFixture[str]]: + r"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. + + The captured output is made available via ``capsys.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``text`` objects. + + Returns an instance of :class:`CaptureFixture[str] `. + + Example: + + .. code-block:: python + + def test_output(capsys): + print("hello") + captured = capsys.readouterr() + assert captured.out == "hello\n" + """ + capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture(SysCapture, request, _ispytest=True) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() + + +@fixture +def capteesys(request: SubRequest) -> Generator[CaptureFixture[str]]: + r"""Enable simultaneous text capturing and pass-through of writes + to ``sys.stdout`` and ``sys.stderr`` as defined by ``--capture=``. + + + The captured output is made available via ``capteesys.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``text`` objects. + + The output is also passed-through, allowing it to be "live-printed", + reported, or both as defined by ``--capture=``. + + Returns an instance of :class:`CaptureFixture[str] `. + + Example: + + .. code-block:: python + + def test_output(capteesys): + print("hello") + captured = capteesys.readouterr() + assert captured.out == "hello\n" + """ + capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture( + SysCapture, request, config=dict(tee=True), _ispytest=True + ) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() + + +@fixture +def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]: + r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. + + The captured output is made available via ``capsysbinary.readouterr()`` + method calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``bytes`` objects. + + Returns an instance of :class:`CaptureFixture[bytes] `. + + Example: + + .. code-block:: python + + def test_output(capsysbinary): + print("hello") + captured = capsysbinary.readouterr() + assert captured.out == b"hello\n" + """ + capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture(SysCaptureBinary, request, _ispytest=True) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() + + +@fixture +def capfd(request: SubRequest) -> Generator[CaptureFixture[str]]: + r"""Enable text capturing of writes to file descriptors ``1`` and ``2``. + + The captured output is made available via ``capfd.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``text`` objects. + + Returns an instance of :class:`CaptureFixture[str] `. + + Example: + + .. code-block:: python + + def test_system_echo(capfd): + os.system('echo "hello"') + captured = capfd.readouterr() + assert captured.out == "hello\n" + """ + capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture(FDCapture, request, _ispytest=True) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() + + +@fixture +def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]: + r"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``. + + The captured output is made available via ``capfd.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``byte`` objects. + + Returns an instance of :class:`CaptureFixture[bytes] `. + + Example: + + .. code-block:: python + + def test_system_echo(capfdbinary): + os.system('echo "hello"') + captured = capfdbinary.readouterr() + assert captured.out == b"hello\n" + + """ + capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture(FDCaptureBinary, request, _ispytest=True) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/compat.py b/Backend/venv/lib/python3.12/site-packages/_pytest/compat.py new file mode 100644 index 00000000..72c3d091 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/compat.py @@ -0,0 +1,314 @@ +# mypy: allow-untyped-defs +"""Python version compatibility code and random general utilities.""" + +from __future__ import annotations + +from collections.abc import Callable +import enum +import functools +import inspect +from inspect import Parameter +from inspect import Signature +import os +from pathlib import Path +import sys +from typing import Any +from typing import Final +from typing import NoReturn + +import py + + +if sys.version_info >= (3, 14): + from annotationlib import Format + + +#: constant to prepare valuing pylib path replacements/lazy proxies later on +# intended for removal in pytest 8.0 or 9.0 + +# fmt: off +# intentional space to create a fake difference for the verification +LEGACY_PATH = py.path. local +# fmt: on + + +def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH: + """Internal wrapper to prepare lazy proxies for legacy_path instances""" + return LEGACY_PATH(path) + + +# fmt: off +# Singleton type for NOTSET, as described in: +# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions +class NotSetType(enum.Enum): + token = 0 +NOTSET: Final = NotSetType.token +# fmt: on + + +def iscoroutinefunction(func: object) -> bool: + """Return True if func is a coroutine function (a function defined with async + def syntax, and doesn't contain yield), or a function decorated with + @asyncio.coroutine. + + Note: copied and modified from Python 3.5's builtin coroutines.py to avoid + importing asyncio directly, which in turns also initializes the "logging" + module as a side-effect (see issue #8). + """ + return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False) + + +def is_async_function(func: object) -> bool: + """Return True if the given function seems to be an async function or + an async generator.""" + return iscoroutinefunction(func) or inspect.isasyncgenfunction(func) + + +def signature(obj: Callable[..., Any]) -> Signature: + """Return signature without evaluating annotations.""" + if sys.version_info >= (3, 14): + return inspect.signature(obj, annotation_format=Format.STRING) + return inspect.signature(obj) + + +def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str: + function = get_real_func(function) + fn = Path(inspect.getfile(function)) + lineno = function.__code__.co_firstlineno + if curdir is not None: + try: + relfn = fn.relative_to(curdir) + except ValueError: + pass + else: + return f"{relfn}:{lineno + 1}" + return f"{fn}:{lineno + 1}" + + +def num_mock_patch_args(function) -> int: + """Return number of arguments used up by mock arguments (if any).""" + patchings = getattr(function, "patchings", None) + if not patchings: + return 0 + + mock_sentinel = getattr(sys.modules.get("mock"), "DEFAULT", object()) + ut_mock_sentinel = getattr(sys.modules.get("unittest.mock"), "DEFAULT", object()) + + return len( + [ + p + for p in patchings + if not p.attribute_name + and (p.new is mock_sentinel or p.new is ut_mock_sentinel) + ] + ) + + +def getfuncargnames( + function: Callable[..., object], + *, + name: str = "", + cls: type | None = None, +) -> tuple[str, ...]: + """Return the names of a function's mandatory arguments. + + Should return the names of all function arguments that: + * Aren't bound to an instance or type as in instance or class methods. + * Don't have default values. + * Aren't bound with functools.partial. + * Aren't replaced with mocks. + + The cls arguments indicate that the function should be treated as a bound + method even though it's not unless the function is a static method. + + The name parameter should be the original name in which the function was collected. + """ + # TODO(RonnyPfannschmidt): This function should be refactored when we + # revisit fixtures. The fixture mechanism should ask the node for + # the fixture names, and not try to obtain directly from the + # function object well after collection has occurred. + + # The parameters attribute of a Signature object contains an + # ordered mapping of parameter names to Parameter instances. This + # creates a tuple of the names of the parameters that don't have + # defaults. + try: + parameters = signature(function).parameters.values() + except (ValueError, TypeError) as e: + from _pytest.outcomes import fail + + fail( + f"Could not determine arguments of {function!r}: {e}", + pytrace=False, + ) + + arg_names = tuple( + p.name + for p in parameters + if ( + p.kind is Parameter.POSITIONAL_OR_KEYWORD + or p.kind is Parameter.KEYWORD_ONLY + ) + and p.default is Parameter.empty + ) + if not name: + name = function.__name__ + + # If this function should be treated as a bound method even though + # it's passed as an unbound method or function, and its first parameter + # wasn't defined as positional only, remove the first parameter name. + if not any(p.kind is Parameter.POSITIONAL_ONLY for p in parameters) and ( + # Not using `getattr` because we don't want to resolve the staticmethod. + # Not using `cls.__dict__` because we want to check the entire MRO. + cls + and not isinstance( + inspect.getattr_static(cls, name, default=None), staticmethod + ) + ): + arg_names = arg_names[1:] + # Remove any names that will be replaced with mocks. + if hasattr(function, "__wrapped__"): + arg_names = arg_names[num_mock_patch_args(function) :] + return arg_names + + +def get_default_arg_names(function: Callable[..., Any]) -> tuple[str, ...]: + # Note: this code intentionally mirrors the code at the beginning of + # getfuncargnames, to get the arguments which were excluded from its result + # because they had default values. + return tuple( + p.name + for p in signature(function).parameters.values() + if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY) + and p.default is not Parameter.empty + ) + + +_non_printable_ascii_translate_table = { + i: f"\\x{i:02x}" for i in range(128) if i not in range(32, 127) +} +_non_printable_ascii_translate_table.update( + {ord("\t"): "\\t", ord("\r"): "\\r", ord("\n"): "\\n"} +) + + +def ascii_escaped(val: bytes | str) -> str: + r"""If val is pure ASCII, return it as an str, otherwise, escape + bytes objects into a sequence of escaped bytes: + + b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6' + + and escapes strings into a sequence of escaped unicode ids, e.g.: + + r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944' + + Note: + The obvious "v.decode('unicode-escape')" will return + valid UTF-8 unicode if it finds them in bytes, but we + want to return escaped bytes for any byte, even if they match + a UTF-8 string. + """ + if isinstance(val, bytes): + ret = val.decode("ascii", "backslashreplace") + else: + ret = val.encode("unicode_escape").decode("ascii") + return ret.translate(_non_printable_ascii_translate_table) + + +def get_real_func(obj): + """Get the real function object of the (possibly) wrapped object by + :func:`functools.wraps`, or :func:`functools.partial`.""" + obj = inspect.unwrap(obj) + + if isinstance(obj, functools.partial): + obj = obj.func + return obj + + +def getimfunc(func): + try: + return func.__func__ + except AttributeError: + return func + + +def safe_getattr(object: Any, name: str, default: Any) -> Any: + """Like getattr but return default upon any Exception or any OutcomeException. + + Attribute access can potentially fail for 'evil' Python objects. + See issue #214. + It catches OutcomeException because of #2490 (issue #580), new outcomes + are derived from BaseException instead of Exception (for more details + check #2707). + """ + from _pytest.outcomes import TEST_OUTCOME + + try: + return getattr(object, name, default) + except TEST_OUTCOME: + return default + + +def safe_isclass(obj: object) -> bool: + """Ignore any exception via isinstance on Python 3.""" + try: + return inspect.isclass(obj) + except Exception: + return False + + +def get_user_id() -> int | None: + """Return the current process's real user id or None if it could not be + determined. + + :return: The user id or None if it could not be determined. + """ + # mypy follows the version and platform checking expectation of PEP 484: + # https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks + # Containment checks are too complex for mypy v1.5.0 and cause failure. + if sys.platform == "win32" or sys.platform == "emscripten": + # win32 does not have a getuid() function. + # Emscripten has a return 0 stub. + return None + else: + # On other platforms, a return value of -1 is assumed to indicate that + # the current process's real user id could not be determined. + ERROR = -1 + uid = os.getuid() + return uid if uid != ERROR else None + + +if sys.version_info >= (3, 11): + from typing import assert_never +else: + + def assert_never(value: NoReturn) -> NoReturn: + assert False, f"Unhandled value: {value} ({type(value).__name__})" + + +class CallableBool: + """ + A bool-like object that can also be called, returning its true/false value. + + Used for backwards compatibility in cases where something was supposed to be a method + but was implemented as a simple attribute by mistake (see `TerminalReporter.isatty`). + + Do not use in new code. + """ + + def __init__(self, value: bool) -> None: + self._value = value + + def __bool__(self) -> bool: + return self._value + + def __call__(self) -> bool: + return self._value + + +def running_on_ci() -> bool: + """Check if we're currently running on a CI system.""" + # Only enable CI mode if one of these env variables is defined and non-empty. + # Note: review `regendoc` tox env in case this list is changed. + env_vars = ["CI", "BUILD_NUMBER"] + return any(os.environ.get(var) for var in env_vars) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/config/__init__.py b/Backend/venv/lib/python3.12/site-packages/_pytest/config/__init__.py new file mode 100644 index 00000000..9b2afe3e --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/config/__init__.py @@ -0,0 +1,2166 @@ +# mypy: allow-untyped-defs +"""Command line options, config-file and conftest.py processing.""" + +from __future__ import annotations + +import argparse +import builtins +import collections.abc +from collections.abc import Callable +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Mapping +from collections.abc import Sequence +import contextlib +import copy +import dataclasses +import enum +from functools import lru_cache +import glob +import importlib.metadata +import inspect +import os +import pathlib +import re +import shlex +import sys +from textwrap import dedent +import types +from types import FunctionType +from typing import Any +from typing import cast +from typing import Final +from typing import final +from typing import IO +from typing import TextIO +from typing import TYPE_CHECKING +import warnings + +import pluggy +from pluggy import HookimplMarker +from pluggy import HookimplOpts +from pluggy import HookspecMarker +from pluggy import HookspecOpts +from pluggy import PluginManager + +from .compat import PathAwareHookProxy +from .exceptions import PrintHelp as PrintHelp +from .exceptions import UsageError as UsageError +from .findpaths import determine_setup +from _pytest import __version__ +import _pytest._code +from _pytest._code import ExceptionInfo +from _pytest._code import filter_traceback +from _pytest._code.code import TracebackStyle +from _pytest._io import TerminalWriter +from _pytest.compat import assert_never +from _pytest.config.argparsing import Argument +from _pytest.config.argparsing import FILE_OR_DIR +from _pytest.config.argparsing import Parser +import _pytest.deprecated +import _pytest.hookspec +from _pytest.outcomes import fail +from _pytest.outcomes import Skipped +from _pytest.pathlib import absolutepath +from _pytest.pathlib import bestrelpath +from _pytest.pathlib import import_path +from _pytest.pathlib import ImportMode +from _pytest.pathlib import resolve_package_path +from _pytest.pathlib import safe_exists +from _pytest.stash import Stash +from _pytest.warning_types import PytestConfigWarning +from _pytest.warning_types import warn_explicit_for + + +if TYPE_CHECKING: + from _pytest.assertion.rewrite import AssertionRewritingHook + from _pytest.cacheprovider import Cache + from _pytest.terminal import TerminalReporter + +_PluggyPlugin = object +"""A type to represent plugin objects. + +Plugins can be any namespace, so we can't narrow it down much, but we use an +alias to make the intent clear. + +Ideally this type would be provided by pluggy itself. +""" + + +hookimpl = HookimplMarker("pytest") +hookspec = HookspecMarker("pytest") + + +@final +class ExitCode(enum.IntEnum): + """Encodes the valid exit codes by pytest. + + Currently users and plugins may supply other exit codes as well. + + .. versionadded:: 5.0 + """ + + #: Tests passed. + OK = 0 + #: Tests failed. + TESTS_FAILED = 1 + #: pytest was interrupted. + INTERRUPTED = 2 + #: An internal error got in the way. + INTERNAL_ERROR = 3 + #: pytest was misused. + USAGE_ERROR = 4 + #: pytest couldn't find tests. + NO_TESTS_COLLECTED = 5 + + __module__ = "pytest" + + +class ConftestImportFailure(Exception): + def __init__( + self, + path: pathlib.Path, + *, + cause: Exception, + ) -> None: + self.path = path + self.cause = cause + + def __str__(self) -> str: + return f"{type(self.cause).__name__}: {self.cause} (from {self.path})" + + +def filter_traceback_for_conftest_import_failure( + entry: _pytest._code.TracebackEntry, +) -> bool: + """Filter tracebacks entries which point to pytest internals or importlib. + + Make a special case for importlib because we use it to import test modules and conftest files + in _pytest.pathlib.import_path. + """ + return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep) + + +def print_conftest_import_error(e: ConftestImportFailure, file: TextIO) -> None: + exc_info = ExceptionInfo.from_exception(e.cause) + tw = TerminalWriter(file) + tw.line(f"ImportError while loading conftest '{e.path}'.", red=True) + exc_info.traceback = exc_info.traceback.filter( + filter_traceback_for_conftest_import_failure + ) + exc_repr = ( + exc_info.getrepr(style="short", chain=False) + if exc_info.traceback + else exc_info.exconly() + ) + formatted_tb = str(exc_repr) + for line in formatted_tb.splitlines(): + tw.line(line.rstrip(), red=True) + + +def print_usage_error(e: UsageError, file: TextIO) -> None: + tw = TerminalWriter(file) + for msg in e.args: + tw.line(f"ERROR: {msg}\n", red=True) + + +def main( + args: list[str] | os.PathLike[str] | None = None, + plugins: Sequence[str | _PluggyPlugin] | None = None, +) -> int | ExitCode: + """Perform an in-process test run. + + :param args: + List of command line arguments. If `None` or not given, defaults to reading + arguments directly from the process command line (:data:`sys.argv`). + :param plugins: List of plugin objects to be auto-registered during initialization. + + :returns: An exit code. + """ + # Handle a single `--version` argument early to avoid starting up the entire pytest infrastructure. + new_args = sys.argv[1:] if args is None else args + if isinstance(new_args, Sequence) and new_args.count("--version") == 1: + sys.stdout.write(f"pytest {__version__}\n") + return ExitCode.OK + + old_pytest_version = os.environ.get("PYTEST_VERSION") + try: + os.environ["PYTEST_VERSION"] = __version__ + try: + config = _prepareconfig(new_args, plugins) + except ConftestImportFailure as e: + print_conftest_import_error(e, file=sys.stderr) + return ExitCode.USAGE_ERROR + + try: + ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config) + try: + return ExitCode(ret) + except ValueError: + return ret + finally: + config._ensure_unconfigure() + except UsageError as e: + print_usage_error(e, file=sys.stderr) + return ExitCode.USAGE_ERROR + finally: + if old_pytest_version is None: + os.environ.pop("PYTEST_VERSION", None) + else: + os.environ["PYTEST_VERSION"] = old_pytest_version + + +def console_main() -> int: + """The CLI entry point of pytest. + + This function is not meant for programmable use; use `main()` instead. + """ + # https://docs.python.org/3/library/signal.html#note-on-sigpipe + try: + code = main() + sys.stdout.flush() + return code + except BrokenPipeError: + # Python flushes standard streams on exit; redirect remaining output + # to devnull to avoid another BrokenPipeError at shutdown + devnull = os.open(os.devnull, os.O_WRONLY) + os.dup2(devnull, sys.stdout.fileno()) + return 1 # Python exits with error code 1 on EPIPE + + +class cmdline: # compatibility namespace + main = staticmethod(main) + + +def filename_arg(path: str, optname: str) -> str: + """Argparse type validator for filename arguments. + + :path: Path of filename. + :optname: Name of the option. + """ + if os.path.isdir(path): + raise UsageError(f"{optname} must be a filename, given: {path}") + return path + + +def directory_arg(path: str, optname: str) -> str: + """Argparse type validator for directory arguments. + + :path: Path of directory. + :optname: Name of the option. + """ + if not os.path.isdir(path): + raise UsageError(f"{optname} must be a directory, given: {path}") + return path + + +# Plugins that cannot be disabled via "-p no:X" currently. +essential_plugins = ( + "mark", + "main", + "runner", + "fixtures", + "helpconfig", # Provides -p. +) + +default_plugins = ( + *essential_plugins, + "python", + "terminal", + "debugging", + "unittest", + "capture", + "skipping", + "legacypath", + "tmpdir", + "monkeypatch", + "recwarn", + "pastebin", + "assertion", + "junitxml", + "doctest", + "cacheprovider", + "setuponly", + "setupplan", + "stepwise", + "unraisableexception", + "threadexception", + "warnings", + "logging", + "reports", + "faulthandler", + "subtests", +) + +builtin_plugins = { + *default_plugins, + "pytester", + "pytester_assertions", +} + + +def get_config( + args: Iterable[str] | None = None, + plugins: Sequence[str | _PluggyPlugin] | None = None, +) -> Config: + # Subsequent calls to main will create a fresh instance. + pluginmanager = PytestPluginManager() + invocation_params = Config.InvocationParams( + args=args or (), + plugins=plugins, + dir=pathlib.Path.cwd(), + ) + config = Config(pluginmanager, invocation_params=invocation_params) + + if invocation_params.args: + # Handle any "-p no:plugin" args. + pluginmanager.consider_preparse(invocation_params.args, exclude_only=True) + + for spec in default_plugins: + pluginmanager.import_plugin(spec) + + return config + + +def get_plugin_manager() -> PytestPluginManager: + """Obtain a new instance of the + :py:class:`pytest.PytestPluginManager`, with default plugins + already loaded. + + This function can be used by integration with other tools, like hooking + into pytest to run tests into an IDE. + """ + return get_config().pluginmanager + + +def _prepareconfig( + args: list[str] | os.PathLike[str], + plugins: Sequence[str | _PluggyPlugin] | None = None, +) -> Config: + if isinstance(args, os.PathLike): + args = [os.fspath(args)] + elif not isinstance(args, list): + msg = ( # type:ignore[unreachable] + "`args` parameter expected to be a list of strings, got: {!r} (type: {})" + ) + raise TypeError(msg.format(args, type(args))) + + initial_config = get_config(args, plugins) + pluginmanager = initial_config.pluginmanager + try: + if plugins: + for plugin in plugins: + if isinstance(plugin, str): + pluginmanager.consider_pluginarg(plugin) + else: + pluginmanager.register(plugin) + config: Config = pluginmanager.hook.pytest_cmdline_parse( + pluginmanager=pluginmanager, args=args + ) + return config + except BaseException: + initial_config._ensure_unconfigure() + raise + + +def _get_directory(path: pathlib.Path) -> pathlib.Path: + """Get the directory of a path - itself if already a directory.""" + if path.is_file(): + return path.parent + else: + return path + + +def _get_legacy_hook_marks( + method: Any, + hook_type: str, + opt_names: tuple[str, ...], +) -> dict[str, bool]: + if TYPE_CHECKING: + # abuse typeguard from importlib to avoid massive method type union that's lacking an alias + assert inspect.isroutine(method) + known_marks: set[str] = {m.name for m in getattr(method, "pytestmark", [])} + must_warn: list[str] = [] + opts: dict[str, bool] = {} + for opt_name in opt_names: + opt_attr = getattr(method, opt_name, AttributeError) + if opt_attr is not AttributeError: + must_warn.append(f"{opt_name}={opt_attr}") + opts[opt_name] = True + elif opt_name in known_marks: + must_warn.append(f"{opt_name}=True") + opts[opt_name] = True + else: + opts[opt_name] = False + if must_warn: + hook_opts = ", ".join(must_warn) + message = _pytest.deprecated.HOOK_LEGACY_MARKING.format( + type=hook_type, + fullname=method.__qualname__, + hook_opts=hook_opts, + ) + warn_explicit_for(cast(FunctionType, method), message) + return opts + + +@final +class PytestPluginManager(PluginManager): + """A :py:class:`pluggy.PluginManager ` with + additional pytest-specific functionality: + + * Loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and + ``pytest_plugins`` global variables found in plugins being loaded. + * ``conftest.py`` loading during start-up. + """ + + def __init__(self) -> None: + from _pytest.assertion import DummyRewriteHook + from _pytest.assertion import RewriteHook + + super().__init__("pytest") + + # -- State related to local conftest plugins. + # All loaded conftest modules. + self._conftest_plugins: set[types.ModuleType] = set() + # All conftest modules applicable for a directory. + # This includes the directory's own conftest modules as well + # as those of its parent directories. + self._dirpath2confmods: dict[pathlib.Path, list[types.ModuleType]] = {} + # Cutoff directory above which conftests are no longer discovered. + self._confcutdir: pathlib.Path | None = None + # If set, conftest loading is skipped. + self._noconftest = False + + # _getconftestmodules()'s call to _get_directory() causes a stat + # storm when it's called potentially thousands of times in a test + # session (#9478), often with the same path, so cache it. + self._get_directory = lru_cache(256)(_get_directory) + + # plugins that were explicitly skipped with pytest.skip + # list of (module name, skip reason) + # previously we would issue a warning when a plugin was skipped, but + # since we refactored warnings as first citizens of Config, they are + # just stored here to be used later. + self.skipped_plugins: list[tuple[str, str]] = [] + + self.add_hookspecs(_pytest.hookspec) + self.register(self) + if os.environ.get("PYTEST_DEBUG"): + err: IO[str] = sys.stderr + encoding: str = getattr(err, "encoding", "utf8") + try: + err = open( + os.dup(err.fileno()), + mode=err.mode, + buffering=1, + encoding=encoding, + ) + except Exception: + pass + self.trace.root.setwriter(err.write) + self.enable_tracing() + + # Config._consider_importhook will set a real object if required. + self.rewrite_hook: RewriteHook = DummyRewriteHook() + # Used to know when we are importing conftests after the pytest_configure stage. + self._configured = False + + def parse_hookimpl_opts( + self, plugin: _PluggyPlugin, name: str + ) -> HookimplOpts | None: + """:meta private:""" + # pytest hooks are always prefixed with "pytest_", + # so we avoid accessing possibly non-readable attributes + # (see issue #1073). + if not name.startswith("pytest_"): + return None + # Ignore names which cannot be hooks. + if name == "pytest_plugins": + return None + + opts = super().parse_hookimpl_opts(plugin, name) + if opts is not None: + return opts + + method = getattr(plugin, name) + # Consider only actual functions for hooks (#3775). + if not inspect.isroutine(method): + return None + # Collect unmarked hooks as long as they have the `pytest_' prefix. + legacy = _get_legacy_hook_marks( + method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper") + ) + return cast(HookimplOpts, legacy) + + def parse_hookspec_opts(self, module_or_class, name: str) -> HookspecOpts | None: + """:meta private:""" + opts = super().parse_hookspec_opts(module_or_class, name) + if opts is None: + method = getattr(module_or_class, name) + if name.startswith("pytest_"): + legacy = _get_legacy_hook_marks( + method, "spec", ("firstresult", "historic") + ) + opts = cast(HookspecOpts, legacy) + return opts + + def register(self, plugin: _PluggyPlugin, name: str | None = None) -> str | None: + if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS: + warnings.warn( + PytestConfigWarning( + "{} plugin has been merged into the core, " + "please remove it from your requirements.".format( + name.replace("_", "-") + ) + ) + ) + return None + plugin_name = super().register(plugin, name) + if plugin_name is not None: + self.hook.pytest_plugin_registered.call_historic( + kwargs=dict( + plugin=plugin, + plugin_name=plugin_name, + manager=self, + ) + ) + + if isinstance(plugin, types.ModuleType): + self.consider_module(plugin) + return plugin_name + + def getplugin(self, name: str): + # Support deprecated naming because plugins (xdist e.g.) use it. + plugin: _PluggyPlugin | None = self.get_plugin(name) + return plugin + + def hasplugin(self, name: str) -> bool: + """Return whether a plugin with the given name is registered.""" + return bool(self.get_plugin(name)) + + def pytest_configure(self, config: Config) -> None: + """:meta private:""" + # XXX now that the pluginmanager exposes hookimpl(tryfirst...) + # we should remove tryfirst/trylast as markers. + config.addinivalue_line( + "markers", + "tryfirst: mark a hook implementation function such that the " + "plugin machinery will try to call it first/as early as possible. " + "DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.", + ) + config.addinivalue_line( + "markers", + "trylast: mark a hook implementation function such that the " + "plugin machinery will try to call it last/as late as possible. " + "DEPRECATED, use @pytest.hookimpl(trylast=True) instead.", + ) + self._configured = True + + # + # Internal API for local conftest plugin handling. + # + def _set_initial_conftests( + self, + args: Sequence[str | pathlib.Path], + pyargs: bool, + noconftest: bool, + rootpath: pathlib.Path, + confcutdir: pathlib.Path | None, + invocation_dir: pathlib.Path, + importmode: ImportMode | str, + *, + consider_namespace_packages: bool, + ) -> None: + """Load initial conftest files given a preparsed "namespace". + + As conftest files may add their own command line options which have + arguments ('--my-opt somepath') we might get some false positives. + All builtin and 3rd party plugins will have been loaded, however, so + common options will not confuse our logic here. + """ + self._confcutdir = ( + absolutepath(invocation_dir / confcutdir) if confcutdir else None + ) + self._noconftest = noconftest + self._using_pyargs = pyargs + foundanchor = False + for initial_path in args: + path = str(initial_path) + # remove node-id syntax + i = path.find("::") + if i != -1: + path = path[:i] + anchor = absolutepath(invocation_dir / path) + + # Ensure we do not break if what appears to be an anchor + # is in fact a very long option (#10169, #11394). + if safe_exists(anchor): + self._try_load_conftest( + anchor, + importmode, + rootpath, + consider_namespace_packages=consider_namespace_packages, + ) + foundanchor = True + if not foundanchor: + self._try_load_conftest( + invocation_dir, + importmode, + rootpath, + consider_namespace_packages=consider_namespace_packages, + ) + + def _is_in_confcutdir(self, path: pathlib.Path) -> bool: + """Whether to consider the given path to load conftests from.""" + if self._confcutdir is None: + return True + # The semantics here are literally: + # Do not load a conftest if it is found upwards from confcut dir. + # But this is *not* the same as: + # Load only conftests from confcutdir or below. + # At first glance they might seem the same thing, however we do support use cases where + # we want to load conftests that are not found in confcutdir or below, but are found + # in completely different directory hierarchies like packages installed + # in out-of-source trees. + # (see #9767 for a regression where the logic was inverted). + return path not in self._confcutdir.parents + + def _try_load_conftest( + self, + anchor: pathlib.Path, + importmode: str | ImportMode, + rootpath: pathlib.Path, + *, + consider_namespace_packages: bool, + ) -> None: + self._loadconftestmodules( + anchor, + importmode, + rootpath, + consider_namespace_packages=consider_namespace_packages, + ) + # let's also consider test* subdirs + if anchor.is_dir(): + for x in anchor.glob("test*"): + if x.is_dir(): + self._loadconftestmodules( + x, + importmode, + rootpath, + consider_namespace_packages=consider_namespace_packages, + ) + + def _loadconftestmodules( + self, + path: pathlib.Path, + importmode: str | ImportMode, + rootpath: pathlib.Path, + *, + consider_namespace_packages: bool, + ) -> None: + if self._noconftest: + return + + directory = self._get_directory(path) + + # Optimization: avoid repeated searches in the same directory. + # Assumes always called with same importmode and rootpath. + if directory in self._dirpath2confmods: + return + + clist = [] + for parent in reversed((directory, *directory.parents)): + if self._is_in_confcutdir(parent): + conftestpath = parent / "conftest.py" + if conftestpath.is_file(): + mod = self._importconftest( + conftestpath, + importmode, + rootpath, + consider_namespace_packages=consider_namespace_packages, + ) + clist.append(mod) + self._dirpath2confmods[directory] = clist + + def _getconftestmodules(self, path: pathlib.Path) -> Sequence[types.ModuleType]: + directory = self._get_directory(path) + return self._dirpath2confmods.get(directory, ()) + + def _rget_with_confmod( + self, + name: str, + path: pathlib.Path, + ) -> tuple[types.ModuleType, Any]: + modules = self._getconftestmodules(path) + for mod in reversed(modules): + try: + return mod, getattr(mod, name) + except AttributeError: + continue + raise KeyError(name) + + def _importconftest( + self, + conftestpath: pathlib.Path, + importmode: str | ImportMode, + rootpath: pathlib.Path, + *, + consider_namespace_packages: bool, + ) -> types.ModuleType: + conftestpath_plugin_name = str(conftestpath) + existing = self.get_plugin(conftestpath_plugin_name) + if existing is not None: + return cast(types.ModuleType, existing) + + # conftest.py files there are not in a Python package all have module + # name "conftest", and thus conflict with each other. Clear the existing + # before loading the new one, otherwise the existing one will be + # returned from the module cache. + pkgpath = resolve_package_path(conftestpath) + if pkgpath is None: + try: + del sys.modules[conftestpath.stem] + except KeyError: + pass + + try: + mod = import_path( + conftestpath, + mode=importmode, + root=rootpath, + consider_namespace_packages=consider_namespace_packages, + ) + except Exception as e: + assert e.__traceback__ is not None + raise ConftestImportFailure(conftestpath, cause=e) from e + + self._check_non_top_pytest_plugins(mod, conftestpath) + + self._conftest_plugins.add(mod) + dirpath = conftestpath.parent + if dirpath in self._dirpath2confmods: + for path, mods in self._dirpath2confmods.items(): + if dirpath in path.parents or path == dirpath: + if mod in mods: + raise AssertionError( + f"While trying to load conftest path {conftestpath!s}, " + f"found that the module {mod} is already loaded with path {mod.__file__}. " + "This is not supposed to happen. Please report this issue to pytest." + ) + mods.append(mod) + self.trace(f"loading conftestmodule {mod!r}") + self.consider_conftest(mod, registration_name=conftestpath_plugin_name) + return mod + + def _check_non_top_pytest_plugins( + self, + mod: types.ModuleType, + conftestpath: pathlib.Path, + ) -> None: + if ( + hasattr(mod, "pytest_plugins") + and self._configured + and not self._using_pyargs + ): + msg = ( + "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported:\n" + "It affects the entire test suite instead of just below the conftest as expected.\n" + " {}\n" + "Please move it to a top level conftest file at the rootdir:\n" + " {}\n" + "For more information, visit:\n" + " https://docs.pytest.org/en/stable/deprecations.html#pytest-plugins-in-non-top-level-conftest-files" + ) + fail(msg.format(conftestpath, self._confcutdir), pytrace=False) + + # + # API for bootstrapping plugin loading + # + # + + def consider_preparse( + self, args: Sequence[str], *, exclude_only: bool = False + ) -> None: + """:meta private:""" + i = 0 + n = len(args) + while i < n: + opt = args[i] + i += 1 + if isinstance(opt, str): + if opt == "-p": + try: + parg = args[i] + except IndexError: + return + i += 1 + elif opt.startswith("-p"): + parg = opt[2:] + else: + continue + parg = parg.strip() + if exclude_only and not parg.startswith("no:"): + continue + self.consider_pluginarg(parg) + + def consider_pluginarg(self, arg: str) -> None: + """:meta private:""" + if arg.startswith("no:"): + name = arg[3:] + if name in essential_plugins: + raise UsageError(f"plugin {name} cannot be disabled") + + # PR #4304: remove stepwise if cacheprovider is blocked. + if name == "cacheprovider": + self.set_blocked("stepwise") + self.set_blocked("pytest_stepwise") + + self.set_blocked(name) + if not name.startswith("pytest_"): + self.set_blocked("pytest_" + name) + else: + name = arg + # Unblock the plugin. + self.unblock(name) + if not name.startswith("pytest_"): + self.unblock("pytest_" + name) + self.import_plugin(arg, consider_entry_points=True) + + def consider_conftest( + self, conftestmodule: types.ModuleType, registration_name: str + ) -> None: + """:meta private:""" + self.register(conftestmodule, name=registration_name) + + def consider_env(self) -> None: + """:meta private:""" + self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS")) + + def consider_module(self, mod: types.ModuleType) -> None: + """:meta private:""" + self._import_plugin_specs(getattr(mod, "pytest_plugins", [])) + + def _import_plugin_specs( + self, spec: None | types.ModuleType | str | Sequence[str] + ) -> None: + plugins = _get_plugin_specs_as_list(spec) + for import_spec in plugins: + self.import_plugin(import_spec) + + def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None: + """Import a plugin with ``modname``. + + If ``consider_entry_points`` is True, entry point names are also + considered to find a plugin. + """ + # Most often modname refers to builtin modules, e.g. "pytester", + # "terminal" or "capture". Those plugins are registered under their + # basename for historic purposes but must be imported with the + # _pytest prefix. + assert isinstance(modname, str), ( + f"module name as text required, got {modname!r}" + ) + if self.is_blocked(modname) or self.get_plugin(modname) is not None: + return + + importspec = "_pytest." + modname if modname in builtin_plugins else modname + self.rewrite_hook.mark_rewrite(importspec) + + if consider_entry_points: + loaded = self.load_setuptools_entrypoints("pytest11", name=modname) + if loaded: + return + + try: + __import__(importspec) + except ImportError as e: + raise ImportError( + f'Error importing plugin "{modname}": {e.args[0]}' + ).with_traceback(e.__traceback__) from e + + except Skipped as e: + self.skipped_plugins.append((modname, e.msg or "")) + else: + mod = sys.modules[importspec] + self.register(mod, modname) + + +def _get_plugin_specs_as_list( + specs: None | types.ModuleType | str | Sequence[str], +) -> list[str]: + """Parse a plugins specification into a list of plugin names.""" + # None means empty. + if specs is None: + return [] + # Workaround for #3899 - a submodule which happens to be called "pytest_plugins". + if isinstance(specs, types.ModuleType): + return [] + # Comma-separated list. + if isinstance(specs, str): + return specs.split(",") if specs else [] + # Direct specification. + if isinstance(specs, collections.abc.Sequence): + return list(specs) + raise UsageError( + f"Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: {specs!r}" + ) + + +class Notset: + def __repr__(self): + return "" + + +notset = Notset() + + +def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]: + """Given an iterable of file names in a source distribution, return the "names" that should + be marked for assertion rewrite. + + For example the package "pytest_mock/__init__.py" should be added as "pytest_mock" in + the assertion rewrite mechanism. + + This function has to deal with dist-info based distributions and egg based distributions + (which are still very much in use for "editable" installs). + + Here are the file names as seen in a dist-info based distribution: + + pytest_mock/__init__.py + pytest_mock/_version.py + pytest_mock/plugin.py + pytest_mock.egg-info/PKG-INFO + + Here are the file names as seen in an egg based distribution: + + src/pytest_mock/__init__.py + src/pytest_mock/_version.py + src/pytest_mock/plugin.py + src/pytest_mock.egg-info/PKG-INFO + LICENSE + setup.py + + We have to take in account those two distribution flavors in order to determine which + names should be considered for assertion rewriting. + + More information: + https://github.com/pytest-dev/pytest-mock/issues/167 + """ + package_files = list(package_files) + seen_some = False + for fn in package_files: + is_simple_module = "/" not in fn and fn.endswith(".py") + is_package = fn.count("/") == 1 and fn.endswith("__init__.py") + if is_simple_module: + module_name, _ = os.path.splitext(fn) + # we ignore "setup.py" at the root of the distribution + # as well as editable installation finder modules made by setuptools + if module_name != "setup" and not module_name.startswith("__editable__"): + seen_some = True + yield module_name + elif is_package: + package_name = os.path.dirname(fn) + seen_some = True + yield package_name + + if not seen_some: + # At this point we did not find any packages or modules suitable for assertion + # rewriting, so we try again by stripping the first path component (to account for + # "src" based source trees for example). + # This approach lets us have the common case continue to be fast, as egg-distributions + # are rarer. + new_package_files = [] + for fn in package_files: + parts = fn.split("/") + new_fn = "/".join(parts[1:]) + if new_fn: + new_package_files.append(new_fn) + if new_package_files: + yield from _iter_rewritable_modules(new_package_files) + + +@final +class Config: + """Access to configuration values, pluginmanager and plugin hooks. + + :param PytestPluginManager pluginmanager: + A pytest PluginManager. + + :param InvocationParams invocation_params: + Object containing parameters regarding the :func:`pytest.main` + invocation. + """ + + @final + @dataclasses.dataclass(frozen=True) + class InvocationParams: + """Holds parameters passed during :func:`pytest.main`. + + The object attributes are read-only. + + .. versionadded:: 5.1 + + .. note:: + + Note that the environment variable ``PYTEST_ADDOPTS`` and the ``addopts`` + configuration option are handled by pytest, not being included in the ``args`` attribute. + + Plugins accessing ``InvocationParams`` must be aware of that. + """ + + args: tuple[str, ...] + """The command-line arguments as passed to :func:`pytest.main`.""" + plugins: Sequence[str | _PluggyPlugin] | None + """Extra plugins, might be `None`.""" + dir: pathlib.Path + """The directory from which :func:`pytest.main` was invoked.""" + + def __init__( + self, + *, + args: Iterable[str], + plugins: Sequence[str | _PluggyPlugin] | None, + dir: pathlib.Path, + ) -> None: + object.__setattr__(self, "args", tuple(args)) + object.__setattr__(self, "plugins", plugins) + object.__setattr__(self, "dir", dir) + + class ArgsSource(enum.Enum): + """Indicates the source of the test arguments. + + .. versionadded:: 7.2 + """ + + #: Command line arguments. + ARGS = enum.auto() + #: Invocation directory. + INVOCATION_DIR = enum.auto() + INCOVATION_DIR = INVOCATION_DIR # backwards compatibility alias + #: 'testpaths' configuration value. + TESTPATHS = enum.auto() + + # Set by cacheprovider plugin. + cache: Cache + + def __init__( + self, + pluginmanager: PytestPluginManager, + *, + invocation_params: InvocationParams | None = None, + ) -> None: + if invocation_params is None: + invocation_params = self.InvocationParams( + args=(), plugins=None, dir=pathlib.Path.cwd() + ) + + self.option = argparse.Namespace() + """Access to command line option as attributes. + + :type: argparse.Namespace + """ + + self.invocation_params = invocation_params + """The parameters with which pytest was invoked. + + :type: InvocationParams + """ + + self._parser = Parser( + usage=f"%(prog)s [options] [{FILE_OR_DIR}] [{FILE_OR_DIR}] [...]", + processopt=self._processopt, + _ispytest=True, + ) + self.pluginmanager = pluginmanager + """The plugin manager handles plugin registration and hook invocation. + + :type: PytestPluginManager + """ + + self.stash = Stash() + """A place where plugins can store information on the config for their + own use. + + :type: Stash + """ + # Deprecated alias. Was never public. Can be removed in a few releases. + self._store = self.stash + + self.trace = self.pluginmanager.trace.root.get("config") + self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment] + self._inicache: dict[str, Any] = {} + self._opt2dest: dict[str, str] = {} + self._cleanup_stack = contextlib.ExitStack() + self.pluginmanager.register(self, "pytestconfig") + self._configured = False + self.hook.pytest_addoption.call_historic( + kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager) + ) + self.args_source = Config.ArgsSource.ARGS + self.args: list[str] = [] + + @property + def rootpath(self) -> pathlib.Path: + """The path to the :ref:`rootdir `. + + .. versionadded:: 6.1 + """ + return self._rootpath + + @property + def inipath(self) -> pathlib.Path | None: + """The path to the :ref:`configfile `. + + .. versionadded:: 6.1 + """ + return self._inipath + + def add_cleanup(self, func: Callable[[], None]) -> None: + """Add a function to be called when the config object gets out of + use (usually coinciding with pytest_unconfigure). + """ + self._cleanup_stack.callback(func) + + def _do_configure(self) -> None: + assert not self._configured + self._configured = True + self.hook.pytest_configure.call_historic(kwargs=dict(config=self)) + + def _ensure_unconfigure(self) -> None: + try: + if self._configured: + self._configured = False + try: + self.hook.pytest_unconfigure(config=self) + finally: + self.hook.pytest_configure._call_history = [] + finally: + try: + self._cleanup_stack.close() + finally: + self._cleanup_stack = contextlib.ExitStack() + + def get_terminal_writer(self) -> TerminalWriter: + terminalreporter: TerminalReporter | None = self.pluginmanager.get_plugin( + "terminalreporter" + ) + assert terminalreporter is not None + return terminalreporter._tw + + def pytest_cmdline_parse( + self, pluginmanager: PytestPluginManager, args: list[str] + ) -> Config: + try: + self.parse(args) + except UsageError: + # Handle `--version --version` and `--help` here in a minimal fashion. + # This gets done via helpconfig normally, but its + # pytest_cmdline_main is not called in case of errors. + if getattr(self.option, "version", False) or "--version" in args: + from _pytest.helpconfig import show_version_verbose + + # Note that `--version` (single argument) is handled early by `Config.main()`, so the only + # way we are reaching this point is via `--version --version`. + show_version_verbose(self) + elif ( + getattr(self.option, "help", False) or "--help" in args or "-h" in args + ): + self._parser.optparser.print_help() + sys.stdout.write( + "\nNOTE: displaying only minimal help due to UsageError.\n\n" + ) + + raise + + return self + + def notify_exception( + self, + excinfo: ExceptionInfo[BaseException], + option: argparse.Namespace | None = None, + ) -> None: + if option and getattr(option, "fulltrace", False): + style: TracebackStyle = "long" + else: + style = "native" + excrepr = excinfo.getrepr( + funcargs=True, showlocals=getattr(option, "showlocals", False), style=style + ) + res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo) + if not any(res): + for line in str(excrepr).split("\n"): + sys.stderr.write(f"INTERNALERROR> {line}\n") + sys.stderr.flush() + + def cwd_relative_nodeid(self, nodeid: str) -> str: + # nodeid's are relative to the rootpath, compute relative to cwd. + if self.invocation_params.dir != self.rootpath: + base_path_part, *nodeid_part = nodeid.split("::") + # Only process path part + fullpath = self.rootpath / base_path_part + relative_path = bestrelpath(self.invocation_params.dir, fullpath) + + nodeid = "::".join([relative_path, *nodeid_part]) + return nodeid + + @classmethod + def fromdictargs(cls, option_dict: Mapping[str, Any], args: list[str]) -> Config: + """Constructor usable for subprocesses.""" + config = get_config(args) + config.option.__dict__.update(option_dict) + config.parse(args, addopts=False) + for x in config.option.plugins: + config.pluginmanager.consider_pluginarg(x) + return config + + def _processopt(self, opt: Argument) -> None: + for name in opt._short_opts + opt._long_opts: + self._opt2dest[name] = opt.dest + + if hasattr(opt, "default"): + if not hasattr(self.option, opt.dest): + setattr(self.option, opt.dest, opt.default) + + @hookimpl(trylast=True) + def pytest_load_initial_conftests(self, early_config: Config) -> None: + # We haven't fully parsed the command line arguments yet, so + # early_config.args it not set yet. But we need it for + # discovering the initial conftests. So "pre-run" the logic here. + # It will be done for real in `parse()`. + args, _args_source = early_config._decide_args( + args=early_config.known_args_namespace.file_or_dir, + pyargs=early_config.known_args_namespace.pyargs, + testpaths=early_config.getini("testpaths"), + invocation_dir=early_config.invocation_params.dir, + rootpath=early_config.rootpath, + warn=False, + ) + self.pluginmanager._set_initial_conftests( + args=args, + pyargs=early_config.known_args_namespace.pyargs, + noconftest=early_config.known_args_namespace.noconftest, + rootpath=early_config.rootpath, + confcutdir=early_config.known_args_namespace.confcutdir, + invocation_dir=early_config.invocation_params.dir, + importmode=early_config.known_args_namespace.importmode, + consider_namespace_packages=early_config.getini( + "consider_namespace_packages" + ), + ) + + def _consider_importhook(self) -> None: + """Install the PEP 302 import hook if using assertion rewriting. + + Needs to parse the --assert= option from the commandline + and find all the installed plugins to mark them for rewriting + by the importhook. + """ + mode = getattr(self.known_args_namespace, "assertmode", "plain") + + disable_autoload = getattr( + self.known_args_namespace, "disable_plugin_autoload", False + ) or bool(os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD")) + if mode == "rewrite": + import _pytest.assertion + + try: + hook = _pytest.assertion.install_importhook(self) + except SystemError: + mode = "plain" + else: + self._mark_plugins_for_rewrite(hook, disable_autoload) + self._warn_about_missing_assertion(mode) + + def _mark_plugins_for_rewrite( + self, hook: AssertionRewritingHook, disable_autoload: bool + ) -> None: + """Given an importhook, mark for rewrite any top-level + modules or packages in the distribution package for + all pytest plugins.""" + self.pluginmanager.rewrite_hook = hook + + if disable_autoload: + # We don't autoload from distribution package entry points, + # no need to continue. + return + + package_files = ( + str(file) + for dist in importlib.metadata.distributions() + if any(ep.group == "pytest11" for ep in dist.entry_points) + for file in dist.files or [] + ) + + for name in _iter_rewritable_modules(package_files): + hook.mark_rewrite(name) + + def _configure_python_path(self) -> None: + # `pythonpath = a b` will set `sys.path` to `[a, b, x, y, z, ...]` + for path in reversed(self.getini("pythonpath")): + sys.path.insert(0, str(path)) + self.add_cleanup(self._unconfigure_python_path) + + def _unconfigure_python_path(self) -> None: + for path in self.getini("pythonpath"): + path_str = str(path) + if path_str in sys.path: + sys.path.remove(path_str) + + def _validate_args(self, args: list[str], via: str) -> list[str]: + """Validate known args.""" + self._parser.extra_info["config source"] = via + try: + self._parser.parse_known_and_unknown_args( + args, namespace=copy.copy(self.option) + ) + finally: + self._parser.extra_info.pop("config source", None) + + return args + + def _decide_args( + self, + *, + args: list[str], + pyargs: bool, + testpaths: list[str], + invocation_dir: pathlib.Path, + rootpath: pathlib.Path, + warn: bool, + ) -> tuple[list[str], ArgsSource]: + """Decide the args (initial paths/nodeids) to use given the relevant inputs. + + :param warn: Whether can issue warnings. + + :returns: The args and the args source. Guaranteed to be non-empty. + """ + if args: + source = Config.ArgsSource.ARGS + result = args + else: + if invocation_dir == rootpath: + source = Config.ArgsSource.TESTPATHS + if pyargs: + result = testpaths + else: + result = [] + for path in testpaths: + result.extend(sorted(glob.iglob(path, recursive=True))) + if testpaths and not result: + if warn: + warning_text = ( + "No files were found in testpaths; " + "consider removing or adjusting your testpaths configuration. " + "Searching recursively from the current directory instead." + ) + self.issue_config_time_warning( + PytestConfigWarning(warning_text), stacklevel=3 + ) + else: + result = [] + if not result: + source = Config.ArgsSource.INVOCATION_DIR + result = [str(invocation_dir)] + return result, source + + @hookimpl(wrapper=True) + def pytest_collection(self) -> Generator[None, object, object]: + # Validate invalid configuration keys after collection is done so we + # take in account options added by late-loading conftest files. + try: + return (yield) + finally: + self._validate_config_options() + + def _checkversion(self) -> None: + import pytest + + minver_ini_value = self.inicfg.get("minversion", None) + minver = minver_ini_value.value if minver_ini_value is not None else None + if minver: + # Imported lazily to improve start-up time. + from packaging.version import Version + + if not isinstance(minver, str): + raise pytest.UsageError( + f"{self.inipath}: 'minversion' must be a single value" + ) + + if Version(minver) > Version(pytest.__version__): + raise pytest.UsageError( + f"{self.inipath}: 'minversion' requires pytest-{minver}, actual pytest-{pytest.__version__}'" + ) + + def _validate_config_options(self) -> None: + for key in sorted(self._get_unknown_ini_keys()): + self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") + + def _validate_plugins(self) -> None: + required_plugins = sorted(self.getini("required_plugins")) + if not required_plugins: + return + + # Imported lazily to improve start-up time. + from packaging.requirements import InvalidRequirement + from packaging.requirements import Requirement + from packaging.version import Version + + plugin_info = self.pluginmanager.list_plugin_distinfo() + plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info} + + missing_plugins = [] + for required_plugin in required_plugins: + try: + req = Requirement(required_plugin) + except InvalidRequirement: + missing_plugins.append(required_plugin) + continue + + if req.name not in plugin_dist_info: + missing_plugins.append(required_plugin) + elif not req.specifier.contains( + Version(plugin_dist_info[req.name]), prereleases=True + ): + missing_plugins.append(required_plugin) + + if missing_plugins: + raise UsageError( + "Missing required plugins: {}".format(", ".join(missing_plugins)), + ) + + def _warn_or_fail_if_strict(self, message: str) -> None: + strict_config = self.getini("strict_config") + if strict_config is None: + strict_config = self.getini("strict") + if strict_config: + raise UsageError(message) + + self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3) + + def _get_unknown_ini_keys(self) -> set[str]: + known_keys = self._parser._inidict.keys() | self._parser._ini_aliases.keys() + return self.inicfg.keys() - known_keys + + def parse(self, args: list[str], addopts: bool = True) -> None: + # Parse given cmdline arguments into this config object. + assert self.args == [], ( + "can only parse cmdline args at most once per Config object" + ) + + self.hook.pytest_addhooks.call_historic( + kwargs=dict(pluginmanager=self.pluginmanager) + ) + + if addopts: + env_addopts = os.environ.get("PYTEST_ADDOPTS", "") + if len(env_addopts): + args[:] = ( + self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS") + + args + ) + + ns = self._parser.parse_known_args(args, namespace=copy.copy(self.option)) + rootpath, inipath, inicfg, ignored_config_files = determine_setup( + inifile=ns.inifilename, + override_ini=ns.override_ini, + args=ns.file_or_dir, + rootdir_cmd_arg=ns.rootdir or None, + invocation_dir=self.invocation_params.dir, + ) + self._rootpath = rootpath + self._inipath = inipath + self._ignored_config_files = ignored_config_files + self.inicfg = inicfg + self._parser.extra_info["rootdir"] = str(self.rootpath) + self._parser.extra_info["inifile"] = str(self.inipath) + + self._parser.addini("addopts", "Extra command line options", "args") + self._parser.addini("minversion", "Minimally required pytest version") + self._parser.addini( + "pythonpath", type="paths", help="Add paths to sys.path", default=[] + ) + self._parser.addini( + "required_plugins", + "Plugins that must be present for pytest to run", + type="args", + default=[], + ) + + if addopts: + args[:] = ( + self._validate_args(self.getini("addopts"), "via addopts config") + args + ) + + self.known_args_namespace = self._parser.parse_known_args( + args, namespace=copy.copy(self.option) + ) + self._checkversion() + self._consider_importhook() + self._configure_python_path() + self.pluginmanager.consider_preparse(args, exclude_only=False) + if ( + not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD") + and not self.known_args_namespace.disable_plugin_autoload + ): + # Autoloading from distribution package entry point has + # not been disabled. + self.pluginmanager.load_setuptools_entrypoints("pytest11") + # Otherwise only plugins explicitly specified in PYTEST_PLUGINS + # are going to be loaded. + self.pluginmanager.consider_env() + + self._parser.parse_known_args(args, namespace=self.known_args_namespace) + + self._validate_plugins() + self._warn_about_skipped_plugins() + + if self.known_args_namespace.confcutdir is None: + if self.inipath is not None: + confcutdir = str(self.inipath.parent) + else: + confcutdir = str(self.rootpath) + self.known_args_namespace.confcutdir = confcutdir + try: + self.hook.pytest_load_initial_conftests( + early_config=self, args=args, parser=self._parser + ) + except ConftestImportFailure as e: + if self.known_args_namespace.help or self.known_args_namespace.version: + # we don't want to prevent --help/--version to work + # so just let it pass and print a warning at the end + self.issue_config_time_warning( + PytestConfigWarning(f"could not load initial conftests: {e.path}"), + stacklevel=2, + ) + else: + raise + + try: + self._parser.parse(args, namespace=self.option) + except PrintHelp: + return + + self.args, self.args_source = self._decide_args( + args=getattr(self.option, FILE_OR_DIR), + pyargs=self.option.pyargs, + testpaths=self.getini("testpaths"), + invocation_dir=self.invocation_params.dir, + rootpath=self.rootpath, + warn=True, + ) + + def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None: + """Issue and handle a warning during the "configure" stage. + + During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item`` + function because it is not possible to have hook wrappers around ``pytest_configure``. + + This function is mainly intended for plugins that need to issue warnings during + ``pytest_configure`` (or similar stages). + + :param warning: The warning instance. + :param stacklevel: stacklevel forwarded to warnings.warn. + """ + if self.pluginmanager.is_blocked("warnings"): + return + + cmdline_filters = self.known_args_namespace.pythonwarnings or [] + config_filters = self.getini("filterwarnings") + + with warnings.catch_warnings(record=True) as records: + warnings.simplefilter("always", type(warning)) + apply_warning_filters(config_filters, cmdline_filters) + warnings.warn(warning, stacklevel=stacklevel) + + if records: + frame = sys._getframe(stacklevel - 1) + location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name + self.hook.pytest_warning_recorded.call_historic( + kwargs=dict( + warning_message=records[0], + when="config", + nodeid="", + location=location, + ) + ) + + def addinivalue_line(self, name: str, line: str) -> None: + """Add a line to a configuration option. The option must have been + declared but might not yet be set in which case the line becomes + the first line in its value.""" + x = self.getini(name) + assert isinstance(x, list) + x.append(line) # modifies the cached list inline + + def getini(self, name: str) -> Any: + """Return configuration value the an :ref:`configuration file `. + + If a configuration value is not defined in a + :ref:`configuration file `, then the ``default`` value + provided while registering the configuration through + :func:`parser.addini ` will be returned. + Please note that you can even provide ``None`` as a valid + default value. + + If ``default`` is not provided while registering using + :func:`parser.addini `, then a default value + based on the ``type`` parameter passed to + :func:`parser.addini ` will be returned. + The default values based on ``type`` are: + ``paths``, ``pathlist``, ``args`` and ``linelist`` : empty list ``[]`` + ``bool`` : ``False`` + ``string`` : empty string ``""`` + ``int`` : ``0`` + ``float`` : ``0.0`` + + If neither the ``default`` nor the ``type`` parameter is passed + while registering the configuration through + :func:`parser.addini `, then the configuration + is treated as a string and a default empty string '' is returned. + + If the specified name hasn't been registered through a prior + :func:`parser.addini ` call (usually from a + plugin), a ValueError is raised. + """ + canonical_name = self._parser._ini_aliases.get(name, name) + try: + return self._inicache[canonical_name] + except KeyError: + pass + self._inicache[canonical_name] = val = self._getini(canonical_name) + return val + + # Meant for easy monkeypatching by legacypath plugin. + # Can be inlined back (with no cover removed) once legacypath is gone. + def _getini_unknown_type(self, name: str, type: str, value: object): + msg = ( + f"Option {name} has unknown configuration type {type} with value {value!r}" + ) + raise ValueError(msg) # pragma: no cover + + def _getini(self, name: str): + # If this is an alias, resolve to canonical name. + canonical_name = self._parser._ini_aliases.get(name, name) + + try: + _description, type, default = self._parser._inidict[canonical_name] + except KeyError as e: + raise ValueError(f"unknown configuration value: {name!r}") from e + + # Collect all possible values (canonical name + aliases) from inicfg. + # Each candidate is (ConfigValue, is_canonical). + candidates = [] + if canonical_name in self.inicfg: + candidates.append((self.inicfg[canonical_name], True)) + for alias, target in self._parser._ini_aliases.items(): + if target == canonical_name and alias in self.inicfg: + candidates.append((self.inicfg[alias], False)) + + if not candidates: + return default + + # Pick the best candidate based on precedence: + # 1. CLI override takes precedence over file, then + # 2. Canonical name takes precedence over alias. + selected = max(candidates, key=lambda x: (x[0].origin == "override", x[1]))[0] + value = selected.value + mode = selected.mode + + if mode == "ini": + # In ini mode, values are always str | list[str]. + assert isinstance(value, (str, list)) + return self._getini_ini(name, canonical_name, type, value, default) + elif mode == "toml": + return self._getini_toml(name, canonical_name, type, value, default) + else: + assert_never(mode) + + def _getini_ini( + self, + name: str, + canonical_name: str, + type: str, + value: str | list[str], + default: Any, + ): + """Handle config values read in INI mode. + + In INI mode, values are stored as str or list[str] only, and coerced + from string based on the registered type. + """ + # Note: some coercions are only required if we are reading from .ini + # files, because the file format doesn't contain type information, but + # when reading from toml (in ini mode) we will get either str or list of + # str values (see load_config_dict_from_file). For example: + # + # ini: + # a_line_list = "tests acceptance" + # + # in this case, we need to split the string to obtain a list of strings. + # + # toml (ini mode): + # a_line_list = ["tests", "acceptance"] + # + # in this case, we already have a list ready to use. + if type == "paths": + dp = ( + self.inipath.parent + if self.inipath is not None + else self.invocation_params.dir + ) + input_values = shlex.split(value) if isinstance(value, str) else value + return [dp / x for x in input_values] + elif type == "args": + return shlex.split(value) if isinstance(value, str) else value + elif type == "linelist": + if isinstance(value, str): + return [t for t in map(lambda x: x.strip(), value.split("\n")) if t] + else: + return value + elif type == "bool": + return _strtobool(str(value).strip()) + elif type == "string": + return value + elif type == "int": + if not isinstance(value, str): + raise TypeError( + f"Expected an int string for option {name} of type integer, but got: {value!r}" + ) from None + return int(value) + elif type == "float": + if not isinstance(value, str): + raise TypeError( + f"Expected a float string for option {name} of type float, but got: {value!r}" + ) from None + return float(value) + else: + return self._getini_unknown_type(name, type, value) + + def _getini_toml( + self, + name: str, + canonical_name: str, + type: str, + value: object, + default: Any, + ): + """Handle TOML config values with strict type validation and no coercion. + + In TOML mode, values already have native types from TOML parsing. + We validate types match expectations exactly, including list items. + """ + value_type = builtins.type(value).__name__ + if type == "paths": + # Expect a list of strings. + if not isinstance(value, list): + raise TypeError( + f"{self.inipath}: config option '{name}' expects a list for type 'paths', " + f"got {value_type}: {value!r}" + ) + for i, item in enumerate(value): + if not isinstance(item, str): + item_type = builtins.type(item).__name__ + raise TypeError( + f"{self.inipath}: config option '{name}' expects a list of strings, " + f"but item at index {i} is {item_type}: {item!r}" + ) + dp = ( + self.inipath.parent + if self.inipath is not None + else self.invocation_params.dir + ) + return [dp / x for x in value] + elif type in {"args", "linelist"}: + # Expect a list of strings. + if not isinstance(value, list): + raise TypeError( + f"{self.inipath}: config option '{name}' expects a list for type '{type}', " + f"got {value_type}: {value!r}" + ) + for i, item in enumerate(value): + if not isinstance(item, str): + item_type = builtins.type(item).__name__ + raise TypeError( + f"{self.inipath}: config option '{name}' expects a list of strings, " + f"but item at index {i} is {item_type}: {item!r}" + ) + return list(value) + elif type == "bool": + # Expect a boolean. + if not isinstance(value, bool): + raise TypeError( + f"{self.inipath}: config option '{name}' expects a bool, " + f"got {value_type}: {value!r}" + ) + return value + elif type == "int": + # Expect an integer (but not bool, which is a subclass of int). + if not isinstance(value, int) or isinstance(value, bool): + raise TypeError( + f"{self.inipath}: config option '{name}' expects an int, " + f"got {value_type}: {value!r}" + ) + return value + elif type == "float": + # Expect a float or integer only. + if not isinstance(value, (float, int)) or isinstance(value, bool): + raise TypeError( + f"{self.inipath}: config option '{name}' expects a float, " + f"got {value_type}: {value!r}" + ) + return value + elif type == "string": + # Expect a string. + if not isinstance(value, str): + raise TypeError( + f"{self.inipath}: config option '{name}' expects a string, " + f"got {value_type}: {value!r}" + ) + return value + else: + return self._getini_unknown_type(name, type, value) + + def _getconftest_pathlist( + self, name: str, path: pathlib.Path + ) -> list[pathlib.Path] | None: + try: + mod, relroots = self.pluginmanager._rget_with_confmod(name, path) + except KeyError: + return None + assert mod.__file__ is not None + modpath = pathlib.Path(mod.__file__).parent + values: list[pathlib.Path] = [] + for relroot in relroots: + if isinstance(relroot, os.PathLike): + relroot = pathlib.Path(relroot) + else: + relroot = relroot.replace("/", os.sep) + relroot = absolutepath(modpath / relroot) + values.append(relroot) + return values + + def getoption(self, name: str, default: Any = notset, skip: bool = False): + """Return command line option value. + + :param name: Name of the option. You may also specify + the literal ``--OPT`` option instead of the "dest" option name. + :param default: Fallback value if no option of that name is **declared** via :hook:`pytest_addoption`. + Note this parameter will be ignored when the option is **declared** even if the option's value is ``None``. + :param skip: If ``True``, raise :func:`pytest.skip` if option is undeclared or has a ``None`` value. + Note that even if ``True``, if a default was specified it will be returned instead of a skip. + """ + name = self._opt2dest.get(name, name) + try: + val = getattr(self.option, name) + if val is None and skip: + raise AttributeError(name) + return val + except AttributeError as e: + if default is not notset: + return default + if skip: + import pytest + + pytest.skip(f"no {name!r} option found") + raise ValueError(f"no option named {name!r}") from e + + def getvalue(self, name: str, path=None): + """Deprecated, use getoption() instead.""" + return self.getoption(name) + + def getvalueorskip(self, name: str, path=None): + """Deprecated, use getoption(skip=True) instead.""" + return self.getoption(name, skip=True) + + #: Verbosity type for failed assertions (see :confval:`verbosity_assertions`). + VERBOSITY_ASSERTIONS: Final = "assertions" + #: Verbosity type for test case execution (see :confval:`verbosity_test_cases`). + VERBOSITY_TEST_CASES: Final = "test_cases" + #: Verbosity type for failed subtests (see :confval:`verbosity_subtests`). + VERBOSITY_SUBTESTS: Final = "subtests" + + _VERBOSITY_INI_DEFAULT: Final = "auto" + + def get_verbosity(self, verbosity_type: str | None = None) -> int: + r"""Retrieve the verbosity level for a fine-grained verbosity type. + + :param verbosity_type: Verbosity type to get level for. If a level is + configured for the given type, that value will be returned. If the + given type is not a known verbosity type, the global verbosity + level will be returned. If the given type is None (default), the + global verbosity level will be returned. + + To configure a level for a fine-grained verbosity type, the + configuration file should have a setting for the configuration name + and a numeric value for the verbosity level. A special value of "auto" + can be used to explicitly use the global verbosity level. + + Example: + + .. tab:: toml + + .. code-block:: toml + + [tool.pytest] + verbosity_assertions = 2 + + .. tab:: ini + + .. code-block:: ini + + [pytest] + verbosity_assertions = 2 + + .. code-block:: console + + pytest -v + + .. code-block:: python + + print(config.get_verbosity()) # 1 + print(config.get_verbosity(Config.VERBOSITY_ASSERTIONS)) # 2 + """ + global_level = self.getoption("verbose", default=0) + assert isinstance(global_level, int) + if verbosity_type is None: + return global_level + + ini_name = Config._verbosity_ini_name(verbosity_type) + if ini_name not in self._parser._inidict: + return global_level + + level = self.getini(ini_name) + if level == Config._VERBOSITY_INI_DEFAULT: + return global_level + + return int(level) + + @staticmethod + def _verbosity_ini_name(verbosity_type: str) -> str: + return f"verbosity_{verbosity_type}" + + @staticmethod + def _add_verbosity_ini(parser: Parser, verbosity_type: str, help: str) -> None: + """Add a output verbosity configuration option for the given output type. + + :param parser: Parser for command line arguments and config-file values. + :param verbosity_type: Fine-grained verbosity category. + :param help: Description of the output this type controls. + + The value should be retrieved via a call to + :py:func:`config.get_verbosity(type) `. + """ + parser.addini( + Config._verbosity_ini_name(verbosity_type), + help=help, + type="string", + default=Config._VERBOSITY_INI_DEFAULT, + ) + + def _warn_about_missing_assertion(self, mode: str) -> None: + if not _assertion_supported(): + if mode == "plain": + warning_text = ( + "ASSERTIONS ARE NOT EXECUTED" + " and FAILING TESTS WILL PASS. Are you" + " using python -O?" + ) + else: + warning_text = ( + "assertions not in test modules or" + " plugins will be ignored" + " because assert statements are not executed " + "by the underlying Python interpreter " + "(are you using python -O?)\n" + ) + self.issue_config_time_warning( + PytestConfigWarning(warning_text), + stacklevel=3, + ) + + def _warn_about_skipped_plugins(self) -> None: + for module_name, msg in self.pluginmanager.skipped_plugins: + self.issue_config_time_warning( + PytestConfigWarning(f"skipped plugin {module_name!r}: {msg}"), + stacklevel=2, + ) + + +def _assertion_supported() -> bool: + try: + assert False + except AssertionError: + return True + else: + return False # type: ignore[unreachable] + + +def create_terminal_writer( + config: Config, file: TextIO | None = None +) -> TerminalWriter: + """Create a TerminalWriter instance configured according to the options + in the config object. + + Every code which requires a TerminalWriter object and has access to a + config object should use this function. + """ + tw = TerminalWriter(file=file) + + if config.option.color == "yes": + tw.hasmarkup = True + elif config.option.color == "no": + tw.hasmarkup = False + + if config.option.code_highlight == "yes": + tw.code_highlight = True + elif config.option.code_highlight == "no": + tw.code_highlight = False + + return tw + + +def _strtobool(val: str) -> bool: + """Convert a string representation of truth to True or False. + + True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values + are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if + 'val' is anything else. + + .. note:: Copied from distutils.util. + """ + val = val.lower() + if val in ("y", "yes", "t", "true", "on", "1"): + return True + elif val in ("n", "no", "f", "false", "off", "0"): + return False + else: + raise ValueError(f"invalid truth value {val!r}") + + +@lru_cache(maxsize=50) +def parse_warning_filter( + arg: str, *, escape: bool +) -> tuple[warnings._ActionKind, str, type[Warning], str, int]: + """Parse a warnings filter string. + + This is copied from warnings._setoption with the following changes: + + * Does not apply the filter. + * Escaping is optional. + * Raises UsageError so we get nice error messages on failure. + """ + __tracebackhide__ = True + error_template = dedent( + f"""\ + while parsing the following warning configuration: + + {arg} + + This error occurred: + + {{error}} + """ + ) + + parts = arg.split(":") + if len(parts) > 5: + doc_url = ( + "https://docs.python.org/3/library/warnings.html#describing-warning-filters" + ) + error = dedent( + f"""\ + Too many fields ({len(parts)}), expected at most 5 separated by colons: + + action:message:category:module:line + + For more information please consult: {doc_url} + """ + ) + raise UsageError(error_template.format(error=error)) + + while len(parts) < 5: + parts.append("") + action_, message, category_, module, lineno_ = (s.strip() for s in parts) + try: + action: warnings._ActionKind = warnings._getaction(action_) # type: ignore[attr-defined] + except warnings._OptionError as e: + raise UsageError(error_template.format(error=str(e))) from None + try: + category: type[Warning] = _resolve_warning_category(category_) + except ImportError: + raise + except Exception: + exc_info = ExceptionInfo.from_current() + exception_text = exc_info.getrepr(style="native") + raise UsageError(error_template.format(error=exception_text)) from None + if message and escape: + message = re.escape(message) + if module and escape: + module = re.escape(module) + r"\Z" + if lineno_: + try: + lineno = int(lineno_) + if lineno < 0: + raise ValueError("number is negative") + except ValueError as e: + raise UsageError( + error_template.format(error=f"invalid lineno {lineno_!r}: {e}") + ) from None + else: + lineno = 0 + try: + re.compile(message) + re.compile(module) + except re.error as e: + raise UsageError( + error_template.format(error=f"Invalid regex {e.pattern!r}: {e}") + ) from None + return action, message, category, module, lineno + + +def _resolve_warning_category(category: str) -> type[Warning]: + """ + Copied from warnings._getcategory, but changed so it lets exceptions (specially ImportErrors) + propagate so we can get access to their tracebacks (#9218). + """ + __tracebackhide__ = True + if not category: + return Warning + + if "." not in category: + import builtins as m + + klass = category + else: + module, _, klass = category.rpartition(".") + m = __import__(module, None, None, [klass]) + cat = getattr(m, klass) + if not issubclass(cat, Warning): + raise UsageError(f"{cat} is not a Warning subclass") + return cast(type[Warning], cat) + + +def apply_warning_filters( + config_filters: Iterable[str], cmdline_filters: Iterable[str] +) -> None: + """Applies pytest-configured filters to the warnings module""" + # Filters should have this precedence: cmdline options, config. + # Filters should be applied in the inverse order of precedence. + for arg in config_filters: + try: + warnings.filterwarnings(*parse_warning_filter(arg, escape=False)) + except ImportError as e: + warnings.warn( + f"Failed to import filter module '{e.name}': {arg}", PytestConfigWarning + ) + continue + + for arg in cmdline_filters: + try: + warnings.filterwarnings(*parse_warning_filter(arg, escape=True)) + except ImportError as e: + warnings.warn( + f"Failed to import filter module '{e.name}': {arg}", PytestConfigWarning + ) + continue diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b8e739c4d39512a149b88b38401b243db5d6d34e GIT binary patch literal 89859 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%0D^LVl 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 E0sO6uE&u=k literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/compat.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/compat.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd6ba647203f02774077e7dd4773c032c3971cb3 GIT binary patch literal 3592 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/findpaths.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/findpaths.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d216d1a8fab6f36f5e9db7e32a06f3b6a4d918da GIT binary patch literal 13270 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 MA2)H#F4~d*0(Ge!bpQYW literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/config/argparsing.py b/Backend/venv/lib/python3.12/site-packages/_pytest/config/argparsing.py new file mode 100644 index 00000000..8216ad8b --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/config/argparsing.py @@ -0,0 +1,578 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import argparse +from collections.abc import Callable +from collections.abc import Mapping +from collections.abc import Sequence +import os +import sys +from typing import Any +from typing import final +from typing import Literal +from typing import NoReturn + +from .exceptions import UsageError +import _pytest._io +from _pytest.deprecated import check_ispytest + + +FILE_OR_DIR = "file_or_dir" + + +class NotSet: + def __repr__(self) -> str: + return "" + + +NOT_SET = NotSet() + + +@final +class Parser: + """Parser for command line arguments and config-file values. + + :ivar extra_info: Dict of generic param -> value to display in case + there's an error processing the command line arguments. + """ + + def __init__( + self, + usage: str | None = None, + processopt: Callable[[Argument], None] | None = None, + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + + from _pytest._argcomplete import filescompleter + + self._processopt = processopt + self.extra_info: dict[str, Any] = {} + self.optparser = PytestArgumentParser(self, usage, self.extra_info) + anonymous_arggroup = self.optparser.add_argument_group("Custom options") + self._anonymous = OptionGroup( + anonymous_arggroup, "_anonymous", self, _ispytest=True + ) + self._groups = [self._anonymous] + file_or_dir_arg = self.optparser.add_argument(FILE_OR_DIR, nargs="*") + file_or_dir_arg.completer = filescompleter # type: ignore + + self._inidict: dict[str, tuple[str, str, Any]] = {} + # Maps alias -> canonical name. + self._ini_aliases: dict[str, str] = {} + + @property + def prog(self) -> str: + return self.optparser.prog + + @prog.setter + def prog(self, value: str) -> None: + self.optparser.prog = value + + def processoption(self, option: Argument) -> None: + if self._processopt: + if option.dest: + self._processopt(option) + + def getgroup( + self, name: str, description: str = "", after: str | None = None + ) -> OptionGroup: + """Get (or create) a named option Group. + + :param name: Name of the option group. + :param description: Long description for --help output. + :param after: Name of another group, used for ordering --help output. + :returns: The option group. + + The returned group object has an ``addoption`` method with the same + signature as :func:`parser.addoption ` but + will be shown in the respective group in the output of + ``pytest --help``. + """ + for group in self._groups: + if group.name == name: + return group + + arggroup = self.optparser.add_argument_group(description or name) + group = OptionGroup(arggroup, name, self, _ispytest=True) + i = 0 + for i, grp in enumerate(self._groups): + if grp.name == after: + break + self._groups.insert(i + 1, group) + # argparse doesn't provide a way to control `--help` order, so must + # access its internals ☹. + self.optparser._action_groups.insert(i + 1, self.optparser._action_groups.pop()) + return group + + def addoption(self, *opts: str, **attrs: Any) -> None: + """Register a command line option. + + :param opts: + Option names, can be short or long options. + :param attrs: + Same attributes as the argparse library's :meth:`add_argument() + ` function accepts. + + After command line parsing, options are available on the pytest config + object via ``config.option.NAME`` where ``NAME`` is usually set + by passing a ``dest`` attribute, for example + ``addoption("--long", dest="NAME", ...)``. + """ + self._anonymous.addoption(*opts, **attrs) + + def parse( + self, + args: Sequence[str | os.PathLike[str]], + namespace: argparse.Namespace | None = None, + ) -> argparse.Namespace: + """Parse the arguments. + + Unlike ``parse_known_args`` and ``parse_known_and_unknown_args``, + raises PrintHelp on `--help` and UsageError on unknown flags + + :meta private: + """ + from _pytest._argcomplete import try_argcomplete + + try_argcomplete(self.optparser) + strargs = [os.fspath(x) for x in args] + if namespace is None: + namespace = argparse.Namespace() + try: + namespace._raise_print_help = True + return self.optparser.parse_intermixed_args(strargs, namespace=namespace) + finally: + del namespace._raise_print_help + + def parse_known_args( + self, + args: Sequence[str | os.PathLike[str]], + namespace: argparse.Namespace | None = None, + ) -> argparse.Namespace: + """Parse the known arguments at this point. + + :returns: An argparse namespace object. + """ + return self.parse_known_and_unknown_args(args, namespace=namespace)[0] + + def parse_known_and_unknown_args( + self, + args: Sequence[str | os.PathLike[str]], + namespace: argparse.Namespace | None = None, + ) -> tuple[argparse.Namespace, list[str]]: + """Parse the known arguments at this point, and also return the + remaining unknown flag arguments. + + :returns: + A tuple containing an argparse namespace object for the known + arguments, and a list of unknown flag arguments. + """ + strargs = [os.fspath(x) for x in args] + if sys.version_info < (3, 12, 8) or (3, 13) <= sys.version_info < (3, 13, 1): + # Older argparse have a bugged parse_known_intermixed_args. + namespace, unknown = self.optparser.parse_known_args(strargs, namespace) + assert namespace is not None + file_or_dir = getattr(namespace, FILE_OR_DIR) + unknown_flags: list[str] = [] + for arg in unknown: + (unknown_flags if arg.startswith("-") else file_or_dir).append(arg) + return namespace, unknown_flags + else: + return self.optparser.parse_known_intermixed_args(strargs, namespace) + + def addini( + self, + name: str, + help: str, + type: Literal[ + "string", "paths", "pathlist", "args", "linelist", "bool", "int", "float" + ] + | None = None, + default: Any = NOT_SET, + *, + aliases: Sequence[str] = (), + ) -> None: + """Register a configuration file option. + + :param name: + Name of the configuration. + :param type: + Type of the configuration. Can be: + + * ``string``: a string + * ``bool``: a boolean + * ``args``: a list of strings, separated as in a shell + * ``linelist``: a list of strings, separated by line breaks + * ``paths``: a list of :class:`pathlib.Path`, separated as in a shell + * ``pathlist``: a list of ``py.path``, separated as in a shell + * ``int``: an integer + * ``float``: a floating-point number + + .. versionadded:: 8.4 + + The ``float`` and ``int`` types. + + For ``paths`` and ``pathlist`` types, they are considered relative to the config-file. + In case the execution is happening without a config-file defined, + they will be considered relative to the current working directory (for example with ``--override-ini``). + + .. versionadded:: 7.0 + The ``paths`` variable type. + + .. versionadded:: 8.1 + Use the current working directory to resolve ``paths`` and ``pathlist`` in the absence of a config-file. + + Defaults to ``string`` if ``None`` or not passed. + :param default: + Default value if no config-file option exists but is queried. + :param aliases: + Additional names by which this option can be referenced. + Aliases resolve to the canonical name. + + .. versionadded:: 9.0 + The ``aliases`` parameter. + + The value of configuration keys can be retrieved via a call to + :py:func:`config.getini(name) `. + """ + assert type in ( + None, + "string", + "paths", + "pathlist", + "args", + "linelist", + "bool", + "int", + "float", + ) + if type is None: + type = "string" + if default is NOT_SET: + default = get_ini_default_for_type(type) + + self._inidict[name] = (help, type, default) + + for alias in aliases: + if alias in self._inidict: + raise ValueError( + f"alias {alias!r} conflicts with existing configuration option" + ) + if (already := self._ini_aliases.get(alias)) is not None: + raise ValueError(f"{alias!r} is already an alias of {already!r}") + self._ini_aliases[alias] = name + + +def get_ini_default_for_type( + type: Literal[ + "string", "paths", "pathlist", "args", "linelist", "bool", "int", "float" + ], +) -> Any: + """ + Used by addini to get the default value for a given config option type, when + default is not supplied. + """ + if type in ("paths", "pathlist", "args", "linelist"): + return [] + elif type == "bool": + return False + elif type == "int": + return 0 + elif type == "float": + return 0.0 + else: + return "" + + +class ArgumentError(Exception): + """Raised if an Argument instance is created with invalid or + inconsistent arguments.""" + + def __init__(self, msg: str, option: Argument | str) -> None: + self.msg = msg + self.option_id = str(option) + + def __str__(self) -> str: + if self.option_id: + return f"option {self.option_id}: {self.msg}" + else: + return self.msg + + +class Argument: + """Class that mimics the necessary behaviour of optparse.Option. + + It's currently a least effort implementation and ignoring choices + and integer prefixes. + + https://docs.python.org/3/library/optparse.html#optparse-standard-option-types + """ + + def __init__(self, *names: str, **attrs: Any) -> None: + """Store params in private vars for use in add_argument.""" + self._attrs = attrs + self._short_opts: list[str] = [] + self._long_opts: list[str] = [] + try: + self.type = attrs["type"] + except KeyError: + pass + try: + # Attribute existence is tested in Config._processopt. + self.default = attrs["default"] + except KeyError: + pass + self._set_opt_strings(names) + dest: str | None = attrs.get("dest") + if dest: + self.dest = dest + elif self._long_opts: + self.dest = self._long_opts[0][2:].replace("-", "_") + else: + try: + self.dest = self._short_opts[0][1:] + except IndexError as e: + self.dest = "???" # Needed for the error repr. + raise ArgumentError("need a long or short option", self) from e + + def names(self) -> list[str]: + return self._short_opts + self._long_opts + + def attrs(self) -> Mapping[str, Any]: + # Update any attributes set by processopt. + for attr in ("default", "dest", "help", self.dest): + try: + self._attrs[attr] = getattr(self, attr) + except AttributeError: + pass + return self._attrs + + def _set_opt_strings(self, opts: Sequence[str]) -> None: + """Directly from optparse. + + Might not be necessary as this is passed to argparse later on. + """ + for opt in opts: + if len(opt) < 2: + raise ArgumentError( + f"invalid option string {opt!r}: " + "must be at least two characters long", + self, + ) + elif len(opt) == 2: + if not (opt[0] == "-" and opt[1] != "-"): + raise ArgumentError( + f"invalid short option string {opt!r}: " + "must be of the form -x, (x any non-dash char)", + self, + ) + self._short_opts.append(opt) + else: + if not (opt[0:2] == "--" and opt[2] != "-"): + raise ArgumentError( + f"invalid long option string {opt!r}: " + "must start with --, followed by non-dash", + self, + ) + self._long_opts.append(opt) + + def __repr__(self) -> str: + args: list[str] = [] + if self._short_opts: + args += ["_short_opts: " + repr(self._short_opts)] + if self._long_opts: + args += ["_long_opts: " + repr(self._long_opts)] + args += ["dest: " + repr(self.dest)] + if hasattr(self, "type"): + args += ["type: " + repr(self.type)] + if hasattr(self, "default"): + args += ["default: " + repr(self.default)] + return "Argument({})".format(", ".join(args)) + + +class OptionGroup: + """A group of options shown in its own section.""" + + def __init__( + self, + arggroup: argparse._ArgumentGroup, + name: str, + parser: Parser | None, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + self._arggroup = arggroup + self.name = name + self.options: list[Argument] = [] + self.parser = parser + + def addoption(self, *opts: str, **attrs: Any) -> None: + """Add an option to this group. + + If a shortened version of a long option is specified, it will + be suppressed in the help. ``addoption('--twowords', '--two-words')`` + results in help showing ``--two-words`` only, but ``--twowords`` gets + accepted **and** the automatic destination is in ``args.twowords``. + + :param opts: + Option names, can be short or long options. + :param attrs: + Same attributes as the argparse library's :meth:`add_argument() + ` function accepts. + """ + conflict = set(opts).intersection( + name for opt in self.options for name in opt.names() + ) + if conflict: + raise ValueError(f"option names {conflict} already added") + option = Argument(*opts, **attrs) + self._addoption_instance(option, shortupper=False) + + def _addoption(self, *opts: str, **attrs: Any) -> None: + option = Argument(*opts, **attrs) + self._addoption_instance(option, shortupper=True) + + def _addoption_instance(self, option: Argument, shortupper: bool = False) -> None: + if not shortupper: + for opt in option._short_opts: + if opt[0] == "-" and opt[1].islower(): + raise ValueError("lowercase shortoptions reserved") + + if self.parser: + self.parser.processoption(option) + + self._arggroup.add_argument(*option.names(), **option.attrs()) + self.options.append(option) + + +class PytestArgumentParser(argparse.ArgumentParser): + def __init__( + self, + parser: Parser, + usage: str | None, + extra_info: dict[str, str], + ) -> None: + self._parser = parser + super().__init__( + usage=usage, + add_help=False, + formatter_class=DropShorterLongHelpFormatter, + allow_abbrev=False, + fromfile_prefix_chars="@", + ) + # extra_info is a dict of (param -> value) to display if there's + # an usage error to provide more contextual information to the user. + self.extra_info = extra_info + + def error(self, message: str) -> NoReturn: + """Transform argparse error message into UsageError.""" + msg = f"{self.prog}: error: {message}" + if self.extra_info: + msg += "\n" + "\n".join( + f" {k}: {v}" for k, v in sorted(self.extra_info.items()) + ) + raise UsageError(self.format_usage() + msg) + + +class DropShorterLongHelpFormatter(argparse.HelpFormatter): + """Shorten help for long options that differ only in extra hyphens. + + - Collapse **long** options that are the same except for extra hyphens. + - Shortcut if there are only two options and one of them is a short one. + - Cache result on the action object as this is called at least 2 times. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + # Use more accurate terminal width. + if "width" not in kwargs: + kwargs["width"] = _pytest._io.get_terminal_width() + super().__init__(*args, **kwargs) + + def _format_action_invocation(self, action: argparse.Action) -> str: + orgstr = super()._format_action_invocation(action) + if orgstr and orgstr[0] != "-": # only optional arguments + return orgstr + res: str | None = getattr(action, "_formatted_action_invocation", None) + if res: + return res + options = orgstr.split(", ") + if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2): + # a shortcut for '-h, --help' or '--abc', '-a' + action._formatted_action_invocation = orgstr # type: ignore + return orgstr + return_list = [] + short_long: dict[str, str] = {} + for option in options: + if len(option) == 2 or option[2] == " ": + continue + if not option.startswith("--"): + raise ArgumentError( + f'long optional argument without "--": [{option}]', option + ) + xxoption = option[2:] + shortened = xxoption.replace("-", "") + if shortened not in short_long or len(short_long[shortened]) < len( + xxoption + ): + short_long[shortened] = xxoption + # now short_long has been filled out to the longest with dashes + # **and** we keep the right option ordering from add_argument + for option in options: + if len(option) == 2 or option[2] == " ": + return_list.append(option) + if option[2:] == short_long.get(option.replace("-", "")): + return_list.append(option.replace(" ", "=", 1)) + formatted_action_invocation = ", ".join(return_list) + action._formatted_action_invocation = formatted_action_invocation # type: ignore + return formatted_action_invocation + + def _split_lines(self, text, width): + """Wrap lines after splitting on original newlines. + + This allows to have explicit line breaks in the help text. + """ + import textwrap + + lines = [] + for line in text.splitlines(): + lines.extend(textwrap.wrap(line.strip(), width)) + return lines + + +class OverrideIniAction(argparse.Action): + """Custom argparse action that makes a CLI flag equivalent to overriding an + option, in addition to behaving like `store_true`. + + This can simplify things since code only needs to inspect the config option + and not consider the CLI flag. + """ + + def __init__( + self, + option_strings: Sequence[str], + dest: str, + nargs: int | str | None = None, + *args, + ini_option: str, + ini_value: str, + **kwargs, + ) -> None: + super().__init__(option_strings, dest, 0, *args, **kwargs) + self.ini_option = ini_option + self.ini_value = ini_value + + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + *args, + **kwargs, + ) -> None: + setattr(namespace, self.dest, True) + current_overrides = getattr(namespace, "override_ini", None) + if current_overrides is None: + current_overrides = [] + current_overrides.append(f"{self.ini_option}={self.ini_value}") + setattr(namespace, "override_ini", current_overrides) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/config/compat.py b/Backend/venv/lib/python3.12/site-packages/_pytest/config/compat.py new file mode 100644 index 00000000..21eab4c7 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/config/compat.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from collections.abc import Mapping +import functools +from pathlib import Path +from typing import Any +import warnings + +import pluggy + +from ..compat import LEGACY_PATH +from ..compat import legacy_path +from ..deprecated import HOOK_LEGACY_PATH_ARG + + +# hookname: (Path, LEGACY_PATH) +imply_paths_hooks: Mapping[str, tuple[str, str]] = { + "pytest_ignore_collect": ("collection_path", "path"), + "pytest_collect_file": ("file_path", "path"), + "pytest_pycollect_makemodule": ("module_path", "path"), + "pytest_report_header": ("start_path", "startdir"), + "pytest_report_collectionfinish": ("start_path", "startdir"), +} + + +def _check_path(path: Path, fspath: LEGACY_PATH) -> None: + if Path(fspath) != path: + raise ValueError( + f"Path({fspath!r}) != {path!r}\n" + "if both path and fspath are given they need to be equal" + ) + + +class PathAwareHookProxy: + """ + this helper wraps around hook callers + until pluggy supports fixingcalls, this one will do + + it currently doesn't return full hook caller proxies for fixed hooks, + this may have to be changed later depending on bugs + """ + + def __init__(self, hook_relay: pluggy.HookRelay) -> None: + self._hook_relay = hook_relay + + def __dir__(self) -> list[str]: + return dir(self._hook_relay) + + def __getattr__(self, key: str) -> pluggy.HookCaller: + hook: pluggy.HookCaller = getattr(self._hook_relay, key) + if key not in imply_paths_hooks: + self.__dict__[key] = hook + return hook + else: + path_var, fspath_var = imply_paths_hooks[key] + + @functools.wraps(hook) + def fixed_hook(**kw: Any) -> Any: + path_value: Path | None = kw.pop(path_var, None) + fspath_value: LEGACY_PATH | None = kw.pop(fspath_var, None) + if fspath_value is not None: + warnings.warn( + HOOK_LEGACY_PATH_ARG.format( + pylib_path_arg=fspath_var, pathlib_path_arg=path_var + ), + stacklevel=2, + ) + if path_value is not None: + if fspath_value is not None: + _check_path(path_value, fspath_value) + else: + fspath_value = legacy_path(path_value) + else: + assert fspath_value is not None + path_value = Path(fspath_value) + + kw[path_var] = path_value + kw[fspath_var] = fspath_value + return hook(**kw) + + fixed_hook.name = hook.name # type: ignore[attr-defined] + fixed_hook.spec = hook.spec # type: ignore[attr-defined] + fixed_hook.__name__ = key + self.__dict__[key] = fixed_hook + return fixed_hook # type: ignore[return-value] diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/config/exceptions.py b/Backend/venv/lib/python3.12/site-packages/_pytest/config/exceptions.py new file mode 100644 index 00000000..d84a9ea6 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/config/exceptions.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from typing import final + + +@final +class UsageError(Exception): + """Error in pytest usage or invocation.""" + + __module__ = "pytest" + + +class PrintHelp(Exception): + """Raised when pytest should print its help to skip the rest of the + argument parsing and validation.""" diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/config/findpaths.py b/Backend/venv/lib/python3.12/site-packages/_pytest/config/findpaths.py new file mode 100644 index 00000000..3c628a09 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/config/findpaths.py @@ -0,0 +1,350 @@ +from __future__ import annotations + +from collections.abc import Iterable +from collections.abc import Sequence +from dataclasses import dataclass +from dataclasses import KW_ONLY +import os +from pathlib import Path +import sys +from typing import Literal +from typing import TypeAlias + +import iniconfig + +from .exceptions import UsageError +from _pytest.outcomes import fail +from _pytest.pathlib import absolutepath +from _pytest.pathlib import commonpath +from _pytest.pathlib import safe_exists + + +@dataclass(frozen=True) +class ConfigValue: + """Represents a configuration value with its origin and parsing mode. + + This allows tracking whether a value came from a configuration file + or from a CLI override (--override-ini), which is important for + determining precedence when dealing with ini option aliases. + + The mode tracks the parsing mode/data model used for the value: + - "ini": from INI files or [tool.pytest.ini_options], where the only + supported value types are `str` or `list[str]`. + - "toml": from TOML files (not in INI mode), where native TOML types + are preserved. + """ + + value: object + _: KW_ONLY + origin: Literal["file", "override"] + mode: Literal["ini", "toml"] + + +ConfigDict: TypeAlias = dict[str, ConfigValue] + + +def _parse_ini_config(path: Path) -> iniconfig.IniConfig: + """Parse the given generic '.ini' file using legacy IniConfig parser, returning + the parsed object. + + Raise UsageError if the file cannot be parsed. + """ + try: + return iniconfig.IniConfig(str(path)) + except iniconfig.ParseError as exc: + raise UsageError(str(exc)) from exc + + +def load_config_dict_from_file( + filepath: Path, +) -> ConfigDict | None: + """Load pytest configuration from the given file path, if supported. + + Return None if the file does not contain valid pytest configuration. + """ + # Configuration from ini files are obtained from the [pytest] section, if present. + if filepath.suffix == ".ini": + iniconfig = _parse_ini_config(filepath) + + if "pytest" in iniconfig: + return { + k: ConfigValue(v, origin="file", mode="ini") + for k, v in iniconfig["pytest"].items() + } + else: + # "pytest.ini" files are always the source of configuration, even if empty. + if filepath.name in {"pytest.ini", ".pytest.ini"}: + return {} + + # '.cfg' files are considered if they contain a "[tool:pytest]" section. + elif filepath.suffix == ".cfg": + iniconfig = _parse_ini_config(filepath) + + if "tool:pytest" in iniconfig.sections: + return { + k: ConfigValue(v, origin="file", mode="ini") + for k, v in iniconfig["tool:pytest"].items() + } + elif "pytest" in iniconfig.sections: + # If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that + # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086). + fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False) + + # '.toml' files are considered if they contain a [tool.pytest] table (toml mode) + # or [tool.pytest.ini_options] table (ini mode) for pyproject.toml, + # or [pytest] table (toml mode) for pytest.toml/.pytest.toml. + elif filepath.suffix == ".toml": + if sys.version_info >= (3, 11): + import tomllib + else: + import tomli as tomllib + + toml_text = filepath.read_text(encoding="utf-8") + try: + config = tomllib.loads(toml_text) + except tomllib.TOMLDecodeError as exc: + raise UsageError(f"{filepath}: {exc}") from exc + + # pytest.toml and .pytest.toml use [pytest] table directly. + if filepath.name in ("pytest.toml", ".pytest.toml"): + pytest_config = config.get("pytest", {}) + if pytest_config: + # TOML mode - preserve native TOML types. + return { + k: ConfigValue(v, origin="file", mode="toml") + for k, v in pytest_config.items() + } + # "pytest.toml" files are always the source of configuration, even if empty. + return {} + + # pyproject.toml uses [tool.pytest] or [tool.pytest.ini_options]. + else: + tool_pytest = config.get("tool", {}).get("pytest", {}) + + # Check for toml mode config: [tool.pytest] with content outside of ini_options. + toml_config = {k: v for k, v in tool_pytest.items() if k != "ini_options"} + # Check for ini mode config: [tool.pytest.ini_options]. + ini_config = tool_pytest.get("ini_options", None) + + if toml_config and ini_config: + raise UsageError( + f"{filepath}: Cannot use both [tool.pytest] (native TOML types) and " + "[tool.pytest.ini_options] (string-based INI format) simultaneously. " + "Please use [tool.pytest] with native TOML types (recommended) " + "or [tool.pytest.ini_options] for backwards compatibility." + ) + + if toml_config: + # TOML mode - preserve native TOML types. + return { + k: ConfigValue(v, origin="file", mode="toml") + for k, v in toml_config.items() + } + + elif ini_config is not None: + # INI mode - TOML supports richer data types than INI files, but we need to + # convert all scalar values to str for compatibility with the INI system. + def make_scalar(v: object) -> str | list[str]: + return v if isinstance(v, list) else str(v) + + return { + k: ConfigValue(make_scalar(v), origin="file", mode="ini") + for k, v in ini_config.items() + } + + return None + + +def locate_config( + invocation_dir: Path, + args: Iterable[Path], +) -> tuple[Path | None, Path | None, ConfigDict, Sequence[str]]: + """Search in the list of arguments for a valid ini-file for pytest, + and return a tuple of (rootdir, inifile, cfg-dict, ignored-config-files), where + ignored-config-files is a list of config basenames found that contain + pytest configuration but were ignored.""" + config_names = [ + "pytest.toml", + ".pytest.toml", + "pytest.ini", + ".pytest.ini", + "pyproject.toml", + "tox.ini", + "setup.cfg", + ] + args = [x for x in args if not str(x).startswith("-")] + if not args: + args = [invocation_dir] + found_pyproject_toml: Path | None = None + ignored_config_files: list[str] = [] + + for arg in args: + argpath = absolutepath(arg) + for base in (argpath, *argpath.parents): + for config_name in config_names: + p = base / config_name + if p.is_file(): + if p.name == "pyproject.toml" and found_pyproject_toml is None: + found_pyproject_toml = p + ini_config = load_config_dict_from_file(p) + if ini_config is not None: + index = config_names.index(config_name) + for remainder in config_names[index + 1 :]: + p2 = base / remainder + if ( + p2.is_file() + and load_config_dict_from_file(p2) is not None + ): + ignored_config_files.append(remainder) + return base, p, ini_config, ignored_config_files + if found_pyproject_toml is not None: + return found_pyproject_toml.parent, found_pyproject_toml, {}, [] + return None, None, {}, [] + + +def get_common_ancestor( + invocation_dir: Path, + paths: Iterable[Path], +) -> Path: + common_ancestor: Path | None = None + for path in paths: + if not path.exists(): + continue + if common_ancestor is None: + common_ancestor = path + else: + if common_ancestor in path.parents or path == common_ancestor: + continue + elif path in common_ancestor.parents: + common_ancestor = path + else: + shared = commonpath(path, common_ancestor) + if shared is not None: + common_ancestor = shared + if common_ancestor is None: + common_ancestor = invocation_dir + elif common_ancestor.is_file(): + common_ancestor = common_ancestor.parent + return common_ancestor + + +def get_dirs_from_args(args: Iterable[str]) -> list[Path]: + def is_option(x: str) -> bool: + return x.startswith("-") + + def get_file_part_from_node_id(x: str) -> str: + return x.split("::")[0] + + def get_dir_from_path(path: Path) -> Path: + if path.is_dir(): + return path + return path.parent + + # These look like paths but may not exist + possible_paths = ( + absolutepath(get_file_part_from_node_id(arg)) + for arg in args + if not is_option(arg) + ) + + return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)] + + +def parse_override_ini(override_ini: Sequence[str] | None) -> ConfigDict: + """Parse the -o/--override-ini command line arguments and return the overrides. + + :raises UsageError: + If one of the values is malformed. + """ + overrides = {} + # override_ini is a list of "ini=value" options. + # Always use the last item if multiple values are set for same ini-name, + # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2. + for ini_config in override_ini or (): + try: + key, user_ini_value = ini_config.split("=", 1) + except ValueError as e: + raise UsageError( + f"-o/--override-ini expects option=value style (got: {ini_config!r})." + ) from e + else: + overrides[key] = ConfigValue(user_ini_value, origin="override", mode="ini") + return overrides + + +CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead." + + +def determine_setup( + *, + inifile: str | None, + override_ini: Sequence[str] | None, + args: Sequence[str], + rootdir_cmd_arg: str | None, + invocation_dir: Path, +) -> tuple[Path, Path | None, ConfigDict, Sequence[str]]: + """Determine the rootdir, inifile and ini configuration values from the + command line arguments. + + :param inifile: + The `--inifile` command line argument, if given. + :param override_ini: + The -o/--override-ini command line arguments, if given. + :param args: + The free command line arguments. + :param rootdir_cmd_arg: + The `--rootdir` command line argument, if given. + :param invocation_dir: + The working directory when pytest was invoked. + + :raises UsageError: + """ + rootdir = None + dirs = get_dirs_from_args(args) + ignored_config_files: Sequence[str] = [] + + if inifile: + inipath_ = absolutepath(inifile) + inipath: Path | None = inipath_ + inicfg = load_config_dict_from_file(inipath_) or {} + if rootdir_cmd_arg is None: + rootdir = inipath_.parent + else: + ancestor = get_common_ancestor(invocation_dir, dirs) + rootdir, inipath, inicfg, ignored_config_files = locate_config( + invocation_dir, [ancestor] + ) + if rootdir is None and rootdir_cmd_arg is None: + for possible_rootdir in (ancestor, *ancestor.parents): + if (possible_rootdir / "setup.py").is_file(): + rootdir = possible_rootdir + break + else: + if dirs != [ancestor]: + rootdir, inipath, inicfg, _ = locate_config(invocation_dir, dirs) + if rootdir is None: + rootdir = get_common_ancestor( + invocation_dir, [invocation_dir, ancestor] + ) + if is_fs_root(rootdir): + rootdir = ancestor + if rootdir_cmd_arg: + rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg)) + if not rootdir.is_dir(): + raise UsageError( + f"Directory '{rootdir}' not found. Check your '--rootdir' option." + ) + + ini_overrides = parse_override_ini(override_ini) + inicfg.update(ini_overrides) + + assert rootdir is not None + return rootdir, inipath, inicfg, ignored_config_files + + +def is_fs_root(p: Path) -> bool: + r""" + Return True if the given path is pointing to the root of the + file system ("/" on Unix and "C:\\" on Windows for example). + """ + return os.path.splitdrive(str(p))[1] == os.sep diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/debugging.py b/Backend/venv/lib/python3.12/site-packages/_pytest/debugging.py new file mode 100644 index 00000000..de1b2688 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/debugging.py @@ -0,0 +1,407 @@ +# mypy: allow-untyped-defs +# ruff: noqa: T100 +"""Interactive debugging with PDB, the Python Debugger.""" + +from __future__ import annotations + +import argparse +from collections.abc import Callable +from collections.abc import Generator +import functools +import sys +import types +from typing import Any +import unittest + +from _pytest import outcomes +from _pytest._code import ExceptionInfo +from _pytest.capture import CaptureManager +from _pytest.config import Config +from _pytest.config import ConftestImportFailure +from _pytest.config import hookimpl +from _pytest.config import PytestPluginManager +from _pytest.config.argparsing import Parser +from _pytest.config.exceptions import UsageError +from _pytest.nodes import Node +from _pytest.reports import BaseReport +from _pytest.runner import CallInfo + + +def _validate_usepdb_cls(value: str) -> tuple[str, str]: + """Validate syntax of --pdbcls option.""" + try: + modname, classname = value.split(":") + except ValueError as e: + raise argparse.ArgumentTypeError( + f"{value!r} is not in the format 'modname:classname'" + ) from e + return (modname, classname) + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("general") + group.addoption( + "--pdb", + dest="usepdb", + action="store_true", + help="Start the interactive Python debugger on errors or KeyboardInterrupt", + ) + group.addoption( + "--pdbcls", + dest="usepdb_cls", + metavar="modulename:classname", + type=_validate_usepdb_cls, + help="Specify a custom interactive Python debugger for use with --pdb." + "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb", + ) + group.addoption( + "--trace", + dest="trace", + action="store_true", + help="Immediately break when running each test", + ) + + +def pytest_configure(config: Config) -> None: + import pdb + + if config.getvalue("trace"): + config.pluginmanager.register(PdbTrace(), "pdbtrace") + if config.getvalue("usepdb"): + config.pluginmanager.register(PdbInvoke(), "pdbinvoke") + + pytestPDB._saved.append( + (pdb.set_trace, pytestPDB._pluginmanager, pytestPDB._config) + ) + pdb.set_trace = pytestPDB.set_trace + pytestPDB._pluginmanager = config.pluginmanager + pytestPDB._config = config + + # NOTE: not using pytest_unconfigure, since it might get called although + # pytest_configure was not (if another plugin raises UsageError). + def fin() -> None: + ( + pdb.set_trace, + pytestPDB._pluginmanager, + pytestPDB._config, + ) = pytestPDB._saved.pop() + + config.add_cleanup(fin) + + +class pytestPDB: + """Pseudo PDB that defers to the real pdb.""" + + _pluginmanager: PytestPluginManager | None = None + _config: Config | None = None + _saved: list[ + tuple[Callable[..., None], PytestPluginManager | None, Config | None] + ] = [] + _recursive_debug = 0 + _wrapped_pdb_cls: tuple[type[Any], type[Any]] | None = None + + @classmethod + def _is_capturing(cls, capman: CaptureManager | None) -> str | bool: + if capman: + return capman.is_capturing() + return False + + @classmethod + def _import_pdb_cls(cls, capman: CaptureManager | None): + if not cls._config: + import pdb + + # Happens when using pytest.set_trace outside of a test. + return pdb.Pdb + + usepdb_cls = cls._config.getvalue("usepdb_cls") + + if cls._wrapped_pdb_cls and cls._wrapped_pdb_cls[0] == usepdb_cls: + return cls._wrapped_pdb_cls[1] + + if usepdb_cls: + modname, classname = usepdb_cls + + try: + __import__(modname) + mod = sys.modules[modname] + + # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp). + parts = classname.split(".") + pdb_cls = getattr(mod, parts[0]) + for part in parts[1:]: + pdb_cls = getattr(pdb_cls, part) + except Exception as exc: + value = ":".join((modname, classname)) + raise UsageError( + f"--pdbcls: could not import {value!r}: {exc}" + ) from exc + else: + import pdb + + pdb_cls = pdb.Pdb + + wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman) + cls._wrapped_pdb_cls = (usepdb_cls, wrapped_cls) + return wrapped_cls + + @classmethod + def _get_pdb_wrapper_class(cls, pdb_cls, capman: CaptureManager | None): + import _pytest.config + + class PytestPdbWrapper(pdb_cls): + _pytest_capman = capman + _continued = False + + def do_debug(self, arg): + cls._recursive_debug += 1 + ret = super().do_debug(arg) + cls._recursive_debug -= 1 + return ret + + if hasattr(pdb_cls, "do_debug"): + do_debug.__doc__ = pdb_cls.do_debug.__doc__ + + def do_continue(self, arg): + ret = super().do_continue(arg) + if cls._recursive_debug == 0: + assert cls._config is not None + tw = _pytest.config.create_terminal_writer(cls._config) + tw.line() + + capman = self._pytest_capman + capturing = pytestPDB._is_capturing(capman) + if capturing: + if capturing == "global": + tw.sep(">", "PDB continue (IO-capturing resumed)") + else: + tw.sep( + ">", + f"PDB continue (IO-capturing resumed for {capturing})", + ) + assert capman is not None + capman.resume() + else: + tw.sep(">", "PDB continue") + assert cls._pluginmanager is not None + cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self) + self._continued = True + return ret + + if hasattr(pdb_cls, "do_continue"): + do_continue.__doc__ = pdb_cls.do_continue.__doc__ + + do_c = do_cont = do_continue + + def do_quit(self, arg): + # Raise Exit outcome when quit command is used in pdb. + # + # This is a bit of a hack - it would be better if BdbQuit + # could be handled, but this would require to wrap the + # whole pytest run, and adjust the report etc. + ret = super().do_quit(arg) + + if cls._recursive_debug == 0: + outcomes.exit("Quitting debugger") + + return ret + + if hasattr(pdb_cls, "do_quit"): + do_quit.__doc__ = pdb_cls.do_quit.__doc__ + + do_q = do_quit + do_exit = do_quit + + def setup(self, f, tb): + """Suspend on setup(). + + Needed after do_continue resumed, and entering another + breakpoint again. + """ + ret = super().setup(f, tb) + if not ret and self._continued: + # pdb.setup() returns True if the command wants to exit + # from the interaction: do not suspend capturing then. + if self._pytest_capman: + self._pytest_capman.suspend_global_capture(in_=True) + return ret + + def get_stack(self, f, t): + stack, i = super().get_stack(f, t) + if f is None: + # Find last non-hidden frame. + i = max(0, len(stack) - 1) + while i and stack[i][0].f_locals.get("__tracebackhide__", False): + i -= 1 + return stack, i + + return PytestPdbWrapper + + @classmethod + def _init_pdb(cls, method, *args, **kwargs): + """Initialize PDB debugging, dropping any IO capturing.""" + import _pytest.config + + if cls._pluginmanager is None: + capman: CaptureManager | None = None + else: + capman = cls._pluginmanager.getplugin("capturemanager") + if capman: + capman.suspend(in_=True) + + if cls._config: + tw = _pytest.config.create_terminal_writer(cls._config) + tw.line() + + if cls._recursive_debug == 0: + # Handle header similar to pdb.set_trace in py37+. + header = kwargs.pop("header", None) + if header is not None: + tw.sep(">", header) + else: + capturing = cls._is_capturing(capman) + if capturing == "global": + tw.sep(">", f"PDB {method} (IO-capturing turned off)") + elif capturing: + tw.sep( + ">", + f"PDB {method} (IO-capturing turned off for {capturing})", + ) + else: + tw.sep(">", f"PDB {method}") + + _pdb = cls._import_pdb_cls(capman)(**kwargs) + + if cls._pluginmanager: + cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb) + return _pdb + + @classmethod + def set_trace(cls, *args, **kwargs) -> None: + """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing.""" + frame = sys._getframe().f_back + _pdb = cls._init_pdb("set_trace", *args, **kwargs) + _pdb.set_trace(frame) + + +class PdbInvoke: + def pytest_exception_interact( + self, node: Node, call: CallInfo[Any], report: BaseReport + ) -> None: + capman = node.config.pluginmanager.getplugin("capturemanager") + if capman: + capman.suspend_global_capture(in_=True) + out, err = capman.read_global_capture() + sys.stdout.write(out) + sys.stdout.write(err) + assert call.excinfo is not None + + if not isinstance(call.excinfo.value, unittest.SkipTest): + _enter_pdb(node, call.excinfo, report) + + def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None: + exc_or_tb = _postmortem_exc_or_tb(excinfo) + post_mortem(exc_or_tb) + + +class PdbTrace: + @hookimpl(wrapper=True) + def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, object, object]: + wrap_pytest_function_for_tracing(pyfuncitem) + return (yield) + + +def wrap_pytest_function_for_tracing(pyfuncitem) -> None: + """Change the Python function object of the given Function item by a + wrapper which actually enters pdb before calling the python function + itself, effectively leaving the user in the pdb prompt in the first + statement of the function.""" + _pdb = pytestPDB._init_pdb("runcall") + testfunction = pyfuncitem.obj + + # we can't just return `partial(pdb.runcall, testfunction)` because (on + # python < 3.7.4) runcall's first param is `func`, which means we'd get + # an exception if one of the kwargs to testfunction was called `func`. + @functools.wraps(testfunction) + def wrapper(*args, **kwargs) -> None: + func = functools.partial(testfunction, *args, **kwargs) + _pdb.runcall(func) + + pyfuncitem.obj = wrapper + + +def maybe_wrap_pytest_function_for_tracing(pyfuncitem) -> None: + """Wrap the given pytestfunct item for tracing support if --trace was given in + the command line.""" + if pyfuncitem.config.getvalue("trace"): + wrap_pytest_function_for_tracing(pyfuncitem) + + +def _enter_pdb( + node: Node, excinfo: ExceptionInfo[BaseException], rep: BaseReport +) -> BaseReport: + # XXX we reuse the TerminalReporter's terminalwriter + # because this seems to avoid some encoding related troubles + # for not completely clear reasons. + tw = node.config.pluginmanager.getplugin("terminalreporter")._tw + tw.line() + + showcapture = node.config.option.showcapture + + for sectionname, content in ( + ("stdout", rep.capstdout), + ("stderr", rep.capstderr), + ("log", rep.caplog), + ): + if showcapture in (sectionname, "all") and content: + tw.sep(">", "captured " + sectionname) + if content[-1:] == "\n": + content = content[:-1] + tw.line(content) + + tw.sep(">", "traceback") + rep.toterminal(tw) + tw.sep(">", "entering PDB") + tb_or_exc = _postmortem_exc_or_tb(excinfo) + rep._pdbshown = True # type: ignore[attr-defined] + post_mortem(tb_or_exc) + return rep + + +def _postmortem_exc_or_tb( + excinfo: ExceptionInfo[BaseException], +) -> types.TracebackType | BaseException: + from doctest import UnexpectedException + + get_exc = sys.version_info >= (3, 13) + if isinstance(excinfo.value, UnexpectedException): + # A doctest.UnexpectedException is not useful for post_mortem. + # Use the underlying exception instead: + underlying_exc = excinfo.value + if get_exc: + return underlying_exc.exc_info[1] + + return underlying_exc.exc_info[2] + elif isinstance(excinfo.value, ConftestImportFailure): + # A config.ConftestImportFailure is not useful for post_mortem. + # Use the underlying exception instead: + cause = excinfo.value.cause + if get_exc: + return cause + + assert cause.__traceback__ is not None + return cause.__traceback__ + else: + assert excinfo._excinfo is not None + if get_exc: + return excinfo._excinfo[1] + + return excinfo._excinfo[2] + + +def post_mortem(tb_or_exc: types.TracebackType | BaseException) -> None: + p = pytestPDB._init_pdb("post_mortem") + p.reset() + p.interaction(None, tb_or_exc) + if p.quitting: + outcomes.exit("Quitting debugger") diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/deprecated.py b/Backend/venv/lib/python3.12/site-packages/_pytest/deprecated.py new file mode 100644 index 00000000..cb5d2e93 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/deprecated.py @@ -0,0 +1,99 @@ +"""Deprecation messages and bits of code used elsewhere in the codebase that +is planned to be removed in the next pytest release. + +Keeping it in a central location makes it easy to track what is deprecated and should +be removed when the time comes. + +All constants defined in this module should be either instances of +:class:`PytestWarning`, or :class:`UnformattedWarning` +in case of warnings which need to format their messages. +""" + +from __future__ import annotations + +from warnings import warn + +from _pytest.warning_types import PytestDeprecationWarning +from _pytest.warning_types import PytestRemovedIn9Warning +from _pytest.warning_types import PytestRemovedIn10Warning +from _pytest.warning_types import UnformattedWarning + + +# set of plugins which have been integrated into the core; we use this list to ignore +# them during registration to avoid conflicts +DEPRECATED_EXTERNAL_PLUGINS = { + "pytest_catchlog", + "pytest_capturelog", + "pytest_faulthandler", + "pytest_subtests", +} + + +# This could have been removed pytest 8, but it's harmless and common, so no rush to remove. +YIELD_FIXTURE = PytestDeprecationWarning( + "@pytest.yield_fixture is deprecated.\n" + "Use @pytest.fixture instead; they are the same." +) + +# This deprecation is never really meant to be removed. +PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.") + + +HOOK_LEGACY_PATH_ARG = UnformattedWarning( + PytestRemovedIn9Warning, + "The ({pylib_path_arg}: py.path.local) argument is deprecated, please use ({pathlib_path_arg}: pathlib.Path)\n" + "see https://docs.pytest.org/en/latest/deprecations.html" + "#py-path-local-arguments-for-hooks-replaced-with-pathlib-path", +) + +NODE_CTOR_FSPATH_ARG = UnformattedWarning( + PytestRemovedIn9Warning, + "The (fspath: py.path.local) argument to {node_type_name} is deprecated. " + "Please use the (path: pathlib.Path) argument instead.\n" + "See https://docs.pytest.org/en/latest/deprecations.html" + "#fspath-argument-for-node-constructors-replaced-with-pathlib-path", +) + +HOOK_LEGACY_MARKING = UnformattedWarning( + PytestDeprecationWarning, + "The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n" + "Please use the pytest.hook{type}({hook_opts}) decorator instead\n" + " to configure the hooks.\n" + " See https://docs.pytest.org/en/latest/deprecations.html" + "#configuring-hook-specs-impls-using-markers", +) + +MARKED_FIXTURE = PytestRemovedIn9Warning( + "Marks applied to fixtures have no effect\n" + "See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function" +) + +MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES = PytestRemovedIn10Warning( + "monkeypatch.syspath_prepend() called with pkg_resources legacy namespace packages detected.\n" + "Legacy namespace packages (using pkg_resources.declare_namespace) are deprecated.\n" + "Please use native namespace packages (PEP 420) instead.\n" + "See https://docs.pytest.org/en/stable/deprecations.html#monkeypatch-fixup-namespace-packages" +) + +# You want to make some `__init__` or function "private". +# +# def my_private_function(some, args): +# ... +# +# Do this: +# +# def my_private_function(some, args, *, _ispytest: bool = False): +# check_ispytest(_ispytest) +# ... +# +# Change all internal/allowed calls to +# +# my_private_function(some, args, _ispytest=True) +# +# All other calls will get the default _ispytest=False and trigger +# the warning (possibly error in the future). + + +def check_ispytest(ispytest: bool) -> None: + if not ispytest: + warn(PRIVATE, stacklevel=3) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/doctest.py b/Backend/venv/lib/python3.12/site-packages/_pytest/doctest.py new file mode 100644 index 00000000..cd255f5e --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/doctest.py @@ -0,0 +1,736 @@ +# mypy: allow-untyped-defs +"""Discover and run doctests in modules and test files.""" + +from __future__ import annotations + +import bdb +from collections.abc import Callable +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import Sequence +from contextlib import contextmanager +import functools +import inspect +import os +from pathlib import Path +import platform +import re +import sys +import traceback +import types +from typing import Any +from typing import TYPE_CHECKING +import warnings + +from _pytest import outcomes +from _pytest._code.code import ExceptionInfo +from _pytest._code.code import ReprFileLocation +from _pytest._code.code import TerminalRepr +from _pytest._io import TerminalWriter +from _pytest.compat import safe_getattr +from _pytest.config import Config +from _pytest.config.argparsing import Parser +from _pytest.fixtures import fixture +from _pytest.fixtures import TopRequest +from _pytest.nodes import Collector +from _pytest.nodes import Item +from _pytest.outcomes import OutcomeException +from _pytest.outcomes import skip +from _pytest.pathlib import fnmatch_ex +from _pytest.python import Module +from _pytest.python_api import approx +from _pytest.warning_types import PytestWarning + + +if TYPE_CHECKING: + import doctest + + from typing_extensions import Self + +DOCTEST_REPORT_CHOICE_NONE = "none" +DOCTEST_REPORT_CHOICE_CDIFF = "cdiff" +DOCTEST_REPORT_CHOICE_NDIFF = "ndiff" +DOCTEST_REPORT_CHOICE_UDIFF = "udiff" +DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE = "only_first_failure" + +DOCTEST_REPORT_CHOICES = ( + DOCTEST_REPORT_CHOICE_NONE, + DOCTEST_REPORT_CHOICE_CDIFF, + DOCTEST_REPORT_CHOICE_NDIFF, + DOCTEST_REPORT_CHOICE_UDIFF, + DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE, +) + +# Lazy definition of runner class +RUNNER_CLASS = None +# Lazy definition of output checker class +CHECKER_CLASS: type[doctest.OutputChecker] | None = None + + +def pytest_addoption(parser: Parser) -> None: + parser.addini( + "doctest_optionflags", + "Option flags for doctests", + type="args", + default=["ELLIPSIS"], + ) + parser.addini( + "doctest_encoding", "Encoding used for doctest files", default="utf-8" + ) + group = parser.getgroup("collect") + group.addoption( + "--doctest-modules", + action="store_true", + default=False, + help="Run doctests in all .py modules", + dest="doctestmodules", + ) + group.addoption( + "--doctest-report", + type=str.lower, + default="udiff", + help="Choose another output format for diffs on doctest failure", + choices=DOCTEST_REPORT_CHOICES, + dest="doctestreport", + ) + group.addoption( + "--doctest-glob", + action="append", + default=[], + metavar="pat", + help="Doctests file matching pattern, default: test*.txt", + dest="doctestglob", + ) + group.addoption( + "--doctest-ignore-import-errors", + action="store_true", + default=False, + help="Ignore doctest collection errors", + dest="doctest_ignore_import_errors", + ) + group.addoption( + "--doctest-continue-on-failure", + action="store_true", + default=False, + help="For a given doctest, continue to run after the first failure", + dest="doctest_continue_on_failure", + ) + + +def pytest_unconfigure() -> None: + global RUNNER_CLASS + + RUNNER_CLASS = None + + +def pytest_collect_file( + file_path: Path, + parent: Collector, +) -> DoctestModule | DoctestTextfile | None: + config = parent.config + if file_path.suffix == ".py": + if config.option.doctestmodules and not any( + (_is_setup_py(file_path), _is_main_py(file_path)) + ): + return DoctestModule.from_parent(parent, path=file_path) + elif _is_doctest(config, file_path, parent): + return DoctestTextfile.from_parent(parent, path=file_path) + return None + + +def _is_setup_py(path: Path) -> bool: + if path.name != "setup.py": + return False + contents = path.read_bytes() + return b"setuptools" in contents or b"distutils" in contents + + +def _is_doctest(config: Config, path: Path, parent: Collector) -> bool: + if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path): + return True + globs = config.getoption("doctestglob") or ["test*.txt"] + return any(fnmatch_ex(glob, path) for glob in globs) + + +def _is_main_py(path: Path) -> bool: + return path.name == "__main__.py" + + +class ReprFailDoctest(TerminalRepr): + def __init__( + self, reprlocation_lines: Sequence[tuple[ReprFileLocation, Sequence[str]]] + ) -> None: + self.reprlocation_lines = reprlocation_lines + + def toterminal(self, tw: TerminalWriter) -> None: + for reprlocation, lines in self.reprlocation_lines: + for line in lines: + tw.line(line) + reprlocation.toterminal(tw) + + +class MultipleDoctestFailures(Exception): + def __init__(self, failures: Sequence[doctest.DocTestFailure]) -> None: + super().__init__() + self.failures = failures + + +def _init_runner_class() -> type[doctest.DocTestRunner]: + import doctest + + class PytestDoctestRunner(doctest.DebugRunner): + """Runner to collect failures. + + Note that the out variable in this case is a list instead of a + stdout-like object. + """ + + def __init__( + self, + checker: doctest.OutputChecker | None = None, + verbose: bool | None = None, + optionflags: int = 0, + continue_on_failure: bool = True, + ) -> None: + super().__init__(checker=checker, verbose=verbose, optionflags=optionflags) + self.continue_on_failure = continue_on_failure + + def report_failure( + self, + out, + test: doctest.DocTest, + example: doctest.Example, + got: str, + ) -> None: + failure = doctest.DocTestFailure(test, example, got) + if self.continue_on_failure: + out.append(failure) + else: + raise failure + + def report_unexpected_exception( + self, + out, + test: doctest.DocTest, + example: doctest.Example, + exc_info: tuple[type[BaseException], BaseException, types.TracebackType], + ) -> None: + if isinstance(exc_info[1], OutcomeException): + raise exc_info[1] + if isinstance(exc_info[1], bdb.BdbQuit): + outcomes.exit("Quitting debugger") + failure = doctest.UnexpectedException(test, example, exc_info) + if self.continue_on_failure: + out.append(failure) + else: + raise failure + + return PytestDoctestRunner + + +def _get_runner( + checker: doctest.OutputChecker | None = None, + verbose: bool | None = None, + optionflags: int = 0, + continue_on_failure: bool = True, +) -> doctest.DocTestRunner: + # We need this in order to do a lazy import on doctest + global RUNNER_CLASS + if RUNNER_CLASS is None: + RUNNER_CLASS = _init_runner_class() + # Type ignored because the continue_on_failure argument is only defined on + # PytestDoctestRunner, which is lazily defined so can't be used as a type. + return RUNNER_CLASS( # type: ignore + checker=checker, + verbose=verbose, + optionflags=optionflags, + continue_on_failure=continue_on_failure, + ) + + +class DoctestItem(Item): + def __init__( + self, + name: str, + parent: DoctestTextfile | DoctestModule, + runner: doctest.DocTestRunner, + dtest: doctest.DocTest, + ) -> None: + super().__init__(name, parent) + self.runner = runner + self.dtest = dtest + + # Stuff needed for fixture support. + self.obj = None + fm = self.session._fixturemanager + fixtureinfo = fm.getfixtureinfo(node=self, func=None, cls=None) + self._fixtureinfo = fixtureinfo + self.fixturenames = fixtureinfo.names_closure + self._initrequest() + + @classmethod + def from_parent( # type: ignore[override] + cls, + parent: DoctestTextfile | DoctestModule, + *, + name: str, + runner: doctest.DocTestRunner, + dtest: doctest.DocTest, + ) -> Self: + # incompatible signature due to imposed limits on subclass + """The public named constructor.""" + return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) + + def _initrequest(self) -> None: + self.funcargs: dict[str, object] = {} + self._request = TopRequest(self, _ispytest=True) # type: ignore[arg-type] + + def setup(self) -> None: + self._request._fillfixtures() + globs = dict(getfixture=self._request.getfixturevalue) + for name, value in self._request.getfixturevalue("doctest_namespace").items(): + globs[name] = value + self.dtest.globs.update(globs) + + def runtest(self) -> None: + _check_all_skipped(self.dtest) + self._disable_output_capturing_for_darwin() + failures: list[doctest.DocTestFailure] = [] + # Type ignored because we change the type of `out` from what + # doctest expects. + self.runner.run(self.dtest, out=failures) # type: ignore[arg-type] + if failures: + raise MultipleDoctestFailures(failures) + + def _disable_output_capturing_for_darwin(self) -> None: + """Disable output capturing. Otherwise, stdout is lost to doctest (#985).""" + if platform.system() != "Darwin": + return + capman = self.config.pluginmanager.getplugin("capturemanager") + if capman: + capman.suspend_global_capture(in_=True) + out, err = capman.read_global_capture() + sys.stdout.write(out) + sys.stderr.write(err) + + # TODO: Type ignored -- breaks Liskov Substitution. + def repr_failure( # type: ignore[override] + self, + excinfo: ExceptionInfo[BaseException], + ) -> str | TerminalRepr: + import doctest + + failures: ( + Sequence[doctest.DocTestFailure | doctest.UnexpectedException] | None + ) = None + if isinstance( + excinfo.value, doctest.DocTestFailure | doctest.UnexpectedException + ): + failures = [excinfo.value] + elif isinstance(excinfo.value, MultipleDoctestFailures): + failures = excinfo.value.failures + + if failures is None: + return super().repr_failure(excinfo) + + reprlocation_lines = [] + for failure in failures: + example = failure.example + test = failure.test + filename = test.filename + if test.lineno is None: + lineno = None + else: + lineno = test.lineno + example.lineno + 1 + message = type(failure).__name__ + # TODO: ReprFileLocation doesn't expect a None lineno. + reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type] + checker = _get_checker() + report_choice = _get_report_choice(self.config.getoption("doctestreport")) + if lineno is not None: + assert failure.test.docstring is not None + lines = failure.test.docstring.splitlines(False) + # add line numbers to the left of the error message + assert test.lineno is not None + lines = [ + f"{i + test.lineno + 1:03d} {x}" for (i, x) in enumerate(lines) + ] + # trim docstring error lines to 10 + lines = lines[max(example.lineno - 9, 0) : example.lineno + 1] + else: + lines = [ + "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example" + ] + indent = ">>>" + for line in example.source.splitlines(): + lines.append(f"??? {indent} {line}") + indent = "..." + if isinstance(failure, doctest.DocTestFailure): + lines += checker.output_difference( + example, failure.got, report_choice + ).split("\n") + else: + inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info) + lines += [f"UNEXPECTED EXCEPTION: {inner_excinfo.value!r}"] + lines += [ + x.strip("\n") for x in traceback.format_exception(*failure.exc_info) + ] + reprlocation_lines.append((reprlocation, lines)) + return ReprFailDoctest(reprlocation_lines) + + def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]: + return self.path, self.dtest.lineno, f"[doctest] {self.name}" + + +def _get_flag_lookup() -> dict[str, int]: + import doctest + + return dict( + DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1, + DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE, + NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE, + ELLIPSIS=doctest.ELLIPSIS, + IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL, + COMPARISON_FLAGS=doctest.COMPARISON_FLAGS, + ALLOW_UNICODE=_get_allow_unicode_flag(), + ALLOW_BYTES=_get_allow_bytes_flag(), + NUMBER=_get_number_flag(), + ) + + +def get_optionflags(config: Config) -> int: + optionflags_str = config.getini("doctest_optionflags") + flag_lookup_table = _get_flag_lookup() + flag_acc = 0 + for flag in optionflags_str: + flag_acc |= flag_lookup_table[flag] + return flag_acc + + +def _get_continue_on_failure(config: Config) -> bool: + continue_on_failure: bool = config.getvalue("doctest_continue_on_failure") + if continue_on_failure: + # We need to turn off this if we use pdb since we should stop at + # the first failure. + if config.getvalue("usepdb"): + continue_on_failure = False + return continue_on_failure + + +class DoctestTextfile(Module): + obj = None + + def collect(self) -> Iterable[DoctestItem]: + import doctest + + # Inspired by doctest.testfile; ideally we would use it directly, + # but it doesn't support passing a custom checker. + encoding = self.config.getini("doctest_encoding") + text = self.path.read_text(encoding) + filename = str(self.path) + name = self.path.name + globs = {"__name__": "__main__"} + + optionflags = get_optionflags(self.config) + + runner = _get_runner( + verbose=False, + optionflags=optionflags, + checker=_get_checker(), + continue_on_failure=_get_continue_on_failure(self.config), + ) + + parser = doctest.DocTestParser() + test = parser.get_doctest(text, globs, name, filename, 0) + if test.examples: + yield DoctestItem.from_parent( + self, name=test.name, runner=runner, dtest=test + ) + + +def _check_all_skipped(test: doctest.DocTest) -> None: + """Raise pytest.skip() if all examples in the given DocTest have the SKIP + option set.""" + import doctest + + all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples) + if all_skipped: + skip("all tests skipped by +SKIP option") + + +def _is_mocked(obj: object) -> bool: + """Return if an object is possibly a mock object by checking the + existence of a highly improbable attribute.""" + return ( + safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None) + is not None + ) + + +@contextmanager +def _patch_unwrap_mock_aware() -> Generator[None]: + """Context manager which replaces ``inspect.unwrap`` with a version + that's aware of mock objects and doesn't recurse into them.""" + real_unwrap = inspect.unwrap + + def _mock_aware_unwrap( + func: Callable[..., Any], *, stop: Callable[[Any], Any] | None = None + ) -> Any: + try: + if stop is None or stop is _is_mocked: + return real_unwrap(func, stop=_is_mocked) + _stop = stop + return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func)) + except Exception as e: + warnings.warn( + f"Got {e!r} when unwrapping {func!r}. This is usually caused " + "by a violation of Python's object protocol; see e.g. " + "https://github.com/pytest-dev/pytest/issues/5080", + PytestWarning, + ) + raise + + inspect.unwrap = _mock_aware_unwrap + try: + yield + finally: + inspect.unwrap = real_unwrap + + +class DoctestModule(Module): + def collect(self) -> Iterable[DoctestItem]: + import doctest + + class MockAwareDocTestFinder(doctest.DocTestFinder): + py_ver_info_minor = sys.version_info[:2] + is_find_lineno_broken = ( + py_ver_info_minor < (3, 11) + or (py_ver_info_minor == (3, 11) and sys.version_info.micro < 9) + or (py_ver_info_minor == (3, 12) and sys.version_info.micro < 3) + ) + if is_find_lineno_broken: + + def _find_lineno(self, obj, source_lines): + """On older Pythons, doctest code does not take into account + `@property`. https://github.com/python/cpython/issues/61648 + + Moreover, wrapped Doctests need to be unwrapped so the correct + line number is returned. #8796 + """ + if isinstance(obj, property): + obj = getattr(obj, "fget", obj) + + if hasattr(obj, "__wrapped__"): + # Get the main obj in case of it being wrapped + obj = inspect.unwrap(obj) + + # Type ignored because this is a private function. + return super()._find_lineno( # type:ignore[misc] + obj, + source_lines, + ) + + if sys.version_info < (3, 13): + + def _from_module(self, module, object): + """`cached_property` objects are never considered a part + of the 'current module'. As such they are skipped by doctest. + Here we override `_from_module` to check the underlying + function instead. https://github.com/python/cpython/issues/107995 + """ + if isinstance(object, functools.cached_property): + object = object.func + + # Type ignored because this is a private function. + return super()._from_module(module, object) # type: ignore[misc] + + try: + module = self.obj + except Collector.CollectError: + if self.config.getvalue("doctest_ignore_import_errors"): + skip(f"unable to import module {self.path!r}") + else: + raise + + # While doctests currently don't support fixtures directly, we still + # need to pick up autouse fixtures. + self.session._fixturemanager.parsefactories(self) + + # Uses internal doctest module parsing mechanism. + finder = MockAwareDocTestFinder() + optionflags = get_optionflags(self.config) + runner = _get_runner( + verbose=False, + optionflags=optionflags, + checker=_get_checker(), + continue_on_failure=_get_continue_on_failure(self.config), + ) + + for test in finder.find(module, module.__name__): + if test.examples: # skip empty doctests + yield DoctestItem.from_parent( + self, name=test.name, runner=runner, dtest=test + ) + + +def _init_checker_class() -> type[doctest.OutputChecker]: + import doctest + + class LiteralsOutputChecker(doctest.OutputChecker): + # Based on doctest_nose_plugin.py from the nltk project + # (https://github.com/nltk/nltk) and on the "numtest" doctest extension + # by Sebastien Boisgerault (https://github.com/boisgera/numtest). + + _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE) + _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE) + _number_re = re.compile( + r""" + (?P + (?P + (?P [+-]?\d*)\.(?P\d+) + | + (?P [+-]?\d+)\. + ) + (?: + [Ee] + (?P [+-]?\d+) + )? + | + (?P [+-]?\d+) + (?: + [Ee] + (?P [+-]?\d+) + ) + ) + """, + re.VERBOSE, + ) + + def check_output(self, want: str, got: str, optionflags: int) -> bool: + if super().check_output(want, got, optionflags): + return True + + allow_unicode = optionflags & _get_allow_unicode_flag() + allow_bytes = optionflags & _get_allow_bytes_flag() + allow_number = optionflags & _get_number_flag() + + if not allow_unicode and not allow_bytes and not allow_number: + return False + + def remove_prefixes(regex: re.Pattern[str], txt: str) -> str: + return re.sub(regex, r"\1\2", txt) + + if allow_unicode: + want = remove_prefixes(self._unicode_literal_re, want) + got = remove_prefixes(self._unicode_literal_re, got) + + if allow_bytes: + want = remove_prefixes(self._bytes_literal_re, want) + got = remove_prefixes(self._bytes_literal_re, got) + + if allow_number: + got = self._remove_unwanted_precision(want, got) + + return super().check_output(want, got, optionflags) + + def _remove_unwanted_precision(self, want: str, got: str) -> str: + wants = list(self._number_re.finditer(want)) + gots = list(self._number_re.finditer(got)) + if len(wants) != len(gots): + return got + offset = 0 + for w, g in zip(wants, gots, strict=True): + fraction: str | None = w.group("fraction") + exponent: str | None = w.group("exponent1") + if exponent is None: + exponent = w.group("exponent2") + precision = 0 if fraction is None else len(fraction) + if exponent is not None: + precision -= int(exponent) + if float(w.group()) == approx(float(g.group()), abs=10**-precision): + # They're close enough. Replace the text we actually + # got with the text we want, so that it will match when we + # check the string literally. + got = ( + got[: g.start() + offset] + w.group() + got[g.end() + offset :] + ) + offset += w.end() - w.start() - (g.end() - g.start()) + return got + + return LiteralsOutputChecker + + +def _get_checker() -> doctest.OutputChecker: + """Return a doctest.OutputChecker subclass that supports some + additional options: + + * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b'' + prefixes (respectively) in string literals. Useful when the same + doctest should run in Python 2 and Python 3. + + * NUMBER to ignore floating-point differences smaller than the + precision of the literal number in the doctest. + + An inner class is used to avoid importing "doctest" at the module + level. + """ + global CHECKER_CLASS + if CHECKER_CLASS is None: + CHECKER_CLASS = _init_checker_class() + return CHECKER_CLASS() + + +def _get_allow_unicode_flag() -> int: + """Register and return the ALLOW_UNICODE flag.""" + import doctest + + return doctest.register_optionflag("ALLOW_UNICODE") + + +def _get_allow_bytes_flag() -> int: + """Register and return the ALLOW_BYTES flag.""" + import doctest + + return doctest.register_optionflag("ALLOW_BYTES") + + +def _get_number_flag() -> int: + """Register and return the NUMBER flag.""" + import doctest + + return doctest.register_optionflag("NUMBER") + + +def _get_report_choice(key: str) -> int: + """Return the actual `doctest` module flag value. + + We want to do it as late as possible to avoid importing `doctest` and all + its dependencies when parsing options, as it adds overhead and breaks tests. + """ + import doctest + + return { + DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF, + DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF, + DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF, + DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE, + DOCTEST_REPORT_CHOICE_NONE: 0, + }[key] + + +@fixture(scope="session") +def doctest_namespace() -> dict[str, Any]: + """Fixture that returns a :py:class:`dict` that will be injected into the + namespace of doctests. + + Usually this fixture is used in conjunction with another ``autouse`` fixture: + + .. code-block:: python + + @pytest.fixture(autouse=True) + def add_np(doctest_namespace): + doctest_namespace["np"] = numpy + + For more details: :ref:`doctest_namespace`. + """ + return dict() diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/faulthandler.py b/Backend/venv/lib/python3.12/site-packages/_pytest/faulthandler.py new file mode 100644 index 00000000..080cf583 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/faulthandler.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +from collections.abc import Generator +import os +import sys + +from _pytest.config import Config +from _pytest.config.argparsing import Parser +from _pytest.nodes import Item +from _pytest.stash import StashKey +import pytest + + +fault_handler_original_stderr_fd_key = StashKey[int]() +fault_handler_stderr_fd_key = StashKey[int]() + + +def pytest_addoption(parser: Parser) -> None: + help_timeout = ( + "Dump the traceback of all threads if a test takes " + "more than TIMEOUT seconds to finish" + ) + help_exit_on_timeout = ( + "Exit the test process if a test takes more than " + "faulthandler_timeout seconds to finish" + ) + parser.addini("faulthandler_timeout", help_timeout, default=0.0) + parser.addini( + "faulthandler_exit_on_timeout", help_exit_on_timeout, type="bool", default=False + ) + + +def pytest_configure(config: Config) -> None: + import faulthandler + + # at teardown we want to restore the original faulthandler fileno + # but faulthandler has no api to return the original fileno + # so here we stash the stderr fileno to be used at teardown + # sys.stderr and sys.__stderr__ may be closed or patched during the session + # so we can't rely on their values being good at that point (#11572). + stderr_fileno = get_stderr_fileno() + if faulthandler.is_enabled(): + config.stash[fault_handler_original_stderr_fd_key] = stderr_fileno + config.stash[fault_handler_stderr_fd_key] = os.dup(stderr_fileno) + faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key]) + + +def pytest_unconfigure(config: Config) -> None: + import faulthandler + + faulthandler.disable() + # Close the dup file installed during pytest_configure. + if fault_handler_stderr_fd_key in config.stash: + os.close(config.stash[fault_handler_stderr_fd_key]) + del config.stash[fault_handler_stderr_fd_key] + # Re-enable the faulthandler if it was originally enabled. + if fault_handler_original_stderr_fd_key in config.stash: + faulthandler.enable(config.stash[fault_handler_original_stderr_fd_key]) + del config.stash[fault_handler_original_stderr_fd_key] + + +def get_stderr_fileno() -> int: + try: + fileno = sys.stderr.fileno() + # The Twisted Logger will return an invalid file descriptor since it is not backed + # by an FD. So, let's also forward this to the same code path as with pytest-xdist. + if fileno == -1: + raise AttributeError() + return fileno + except (AttributeError, ValueError): + # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file. + # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors + # This is potentially dangerous, but the best we can do. + assert sys.__stderr__ is not None + return sys.__stderr__.fileno() + + +def get_timeout_config_value(config: Config) -> float: + return float(config.getini("faulthandler_timeout") or 0.0) + + +def get_exit_on_timeout_config_value(config: Config) -> bool: + exit_on_timeout = config.getini("faulthandler_exit_on_timeout") + assert isinstance(exit_on_timeout, bool) + return exit_on_timeout + + +@pytest.hookimpl(wrapper=True, trylast=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: + timeout = get_timeout_config_value(item.config) + exit_on_timeout = get_exit_on_timeout_config_value(item.config) + if timeout > 0: + import faulthandler + + stderr = item.config.stash[fault_handler_stderr_fd_key] + faulthandler.dump_traceback_later(timeout, file=stderr, exit=exit_on_timeout) + try: + return (yield) + finally: + faulthandler.cancel_dump_traceback_later() + else: + return (yield) + + +@pytest.hookimpl(tryfirst=True) +def pytest_enter_pdb() -> None: + """Cancel any traceback dumping due to timeout before entering pdb.""" + import faulthandler + + faulthandler.cancel_dump_traceback_later() + + +@pytest.hookimpl(tryfirst=True) +def pytest_exception_interact() -> None: + """Cancel any traceback dumping due to an interactive exception being + raised.""" + import faulthandler + + faulthandler.cancel_dump_traceback_later() diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/fixtures.py b/Backend/venv/lib/python3.12/site-packages/_pytest/fixtures.py new file mode 100644 index 00000000..27846db1 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/fixtures.py @@ -0,0 +1,2047 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import abc +from collections import defaultdict +from collections import deque +from collections import OrderedDict +from collections.abc import Callable +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Mapping +from collections.abc import MutableMapping +from collections.abc import Sequence +from collections.abc import Set as AbstractSet +import dataclasses +import functools +import inspect +import os +from pathlib import Path +import sys +import types +from typing import Any +from typing import cast +from typing import Final +from typing import final +from typing import Generic +from typing import NoReturn +from typing import overload +from typing import TYPE_CHECKING +from typing import TypeVar +import warnings + +import _pytest +from _pytest import nodes +from _pytest._code import getfslineno +from _pytest._code import Source +from _pytest._code.code import FormattedExcinfo +from _pytest._code.code import TerminalRepr +from _pytest._io import TerminalWriter +from _pytest.compat import assert_never +from _pytest.compat import get_real_func +from _pytest.compat import getfuncargnames +from _pytest.compat import getimfunc +from _pytest.compat import getlocation +from _pytest.compat import NOTSET +from _pytest.compat import NotSetType +from _pytest.compat import safe_getattr +from _pytest.compat import safe_isclass +from _pytest.compat import signature +from _pytest.config import _PluggyPlugin +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.deprecated import MARKED_FIXTURE +from _pytest.deprecated import YIELD_FIXTURE +from _pytest.main import Session +from _pytest.mark import Mark +from _pytest.mark import ParameterSet +from _pytest.mark.structures import MarkDecorator +from _pytest.outcomes import fail +from _pytest.outcomes import skip +from _pytest.outcomes import TEST_OUTCOME +from _pytest.pathlib import absolutepath +from _pytest.pathlib import bestrelpath +from _pytest.scope import _ScopeName +from _pytest.scope import HIGH_SCOPES +from _pytest.scope import Scope +from _pytest.warning_types import PytestRemovedIn9Warning +from _pytest.warning_types import PytestWarning + + +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup + + +if TYPE_CHECKING: + from _pytest.python import CallSpec2 + from _pytest.python import Function + from _pytest.python import Metafunc + + +# The value of the fixture -- return/yield of the fixture function (type variable). +FixtureValue = TypeVar("FixtureValue", covariant=True) +# The type of the fixture function (type variable). +FixtureFunction = TypeVar("FixtureFunction", bound=Callable[..., object]) +# The type of a fixture function (type alias generic in fixture value). +_FixtureFunc = Callable[..., FixtureValue] | Callable[..., Generator[FixtureValue]] +# The type of FixtureDef.cached_result (type alias generic in fixture value). +_FixtureCachedResult = ( + tuple[ + # The result. + FixtureValue, + # Cache key. + object, + None, + ] + | tuple[ + None, + # Cache key. + object, + # The exception and the original traceback. + tuple[BaseException, types.TracebackType | None], + ] +) + + +def pytest_sessionstart(session: Session) -> None: + session._fixturemanager = FixtureManager(session) + + +def get_scope_package( + node: nodes.Item, + fixturedef: FixtureDef[object], +) -> nodes.Node | None: + from _pytest.python import Package + + for parent in node.iter_parents(): + if isinstance(parent, Package) and parent.nodeid == fixturedef.baseid: + return parent + return node.session + + +def get_scope_node(node: nodes.Node, scope: Scope) -> nodes.Node | None: + """Get the closest parent node (including self) which matches the given + scope. + + If there is no parent node for the scope (e.g. asking for class scope on a + Module, or on a Function when not defined in a class), returns None. + """ + import _pytest.python + + if scope is Scope.Function: + # Type ignored because this is actually safe, see: + # https://github.com/python/mypy/issues/4717 + return node.getparent(nodes.Item) # type: ignore[type-abstract] + elif scope is Scope.Class: + return node.getparent(_pytest.python.Class) + elif scope is Scope.Module: + return node.getparent(_pytest.python.Module) + elif scope is Scope.Package: + return node.getparent(_pytest.python.Package) + elif scope is Scope.Session: + return node.getparent(_pytest.main.Session) + else: + assert_never(scope) + + +# TODO: Try to use FixtureFunctionDefinition instead of the marker +def getfixturemarker(obj: object) -> FixtureFunctionMarker | None: + """Return fixturemarker or None if it doesn't exist""" + if isinstance(obj, FixtureFunctionDefinition): + return obj._fixture_function_marker + return None + + +# Algorithm for sorting on a per-parametrized resource setup basis. +# It is called for Session scope first and performs sorting +# down to the lower scopes such as to minimize number of "high scope" +# setups and teardowns. + + +@dataclasses.dataclass(frozen=True) +class ParamArgKey: + """A key for a high-scoped parameter used by an item. + + For use as a hashable key in `reorder_items`. The combination of fields + is meant to uniquely identify a particular "instance" of a param, + potentially shared by multiple items in a scope. + """ + + #: The param name. + argname: str + param_index: int + #: For scopes Package, Module, Class, the path to the file (directory in + #: Package's case) of the package/module/class where the item is defined. + scoped_item_path: Path | None + #: For Class scope, the class where the item is defined. + item_cls: type | None + + +_V = TypeVar("_V") +OrderedSet = dict[_V, None] + + +def get_param_argkeys(item: nodes.Item, scope: Scope) -> Iterator[ParamArgKey]: + """Return all ParamArgKeys for item matching the specified high scope.""" + assert scope is not Scope.Function + + try: + callspec: CallSpec2 = item.callspec # type: ignore[attr-defined] + except AttributeError: + return + + item_cls = None + if scope is Scope.Session: + scoped_item_path = None + elif scope is Scope.Package: + # Package key = module's directory. + scoped_item_path = item.path.parent + elif scope is Scope.Module: + scoped_item_path = item.path + elif scope is Scope.Class: + scoped_item_path = item.path + item_cls = item.cls # type: ignore[attr-defined] + else: + assert_never(scope) + + for argname in callspec.indices: + if callspec._arg2scope[argname] != scope: + continue + param_index = callspec.indices[argname] + yield ParamArgKey(argname, param_index, scoped_item_path, item_cls) + + +def reorder_items(items: Sequence[nodes.Item]) -> list[nodes.Item]: + argkeys_by_item: dict[Scope, dict[nodes.Item, OrderedSet[ParamArgKey]]] = {} + items_by_argkey: dict[Scope, dict[ParamArgKey, OrderedDict[nodes.Item, None]]] = {} + for scope in HIGH_SCOPES: + scoped_argkeys_by_item = argkeys_by_item[scope] = {} + scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(OrderedDict) + for item in items: + argkeys = dict.fromkeys(get_param_argkeys(item, scope)) + if argkeys: + scoped_argkeys_by_item[item] = argkeys + for argkey in argkeys: + scoped_items_by_argkey[argkey][item] = None + + items_set = dict.fromkeys(items) + return list( + reorder_items_atscope( + items_set, argkeys_by_item, items_by_argkey, Scope.Session + ) + ) + + +def reorder_items_atscope( + items: OrderedSet[nodes.Item], + argkeys_by_item: Mapping[Scope, Mapping[nodes.Item, OrderedSet[ParamArgKey]]], + items_by_argkey: Mapping[ + Scope, Mapping[ParamArgKey, OrderedDict[nodes.Item, None]] + ], + scope: Scope, +) -> OrderedSet[nodes.Item]: + if scope is Scope.Function or len(items) < 3: + return items + + scoped_items_by_argkey = items_by_argkey[scope] + scoped_argkeys_by_item = argkeys_by_item[scope] + + ignore: set[ParamArgKey] = set() + items_deque = deque(items) + items_done: OrderedSet[nodes.Item] = {} + while items_deque: + no_argkey_items: OrderedSet[nodes.Item] = {} + slicing_argkey = None + while items_deque: + item = items_deque.popleft() + if item in items_done or item in no_argkey_items: + continue + argkeys = dict.fromkeys( + k for k in scoped_argkeys_by_item.get(item, ()) if k not in ignore + ) + if not argkeys: + no_argkey_items[item] = None + else: + slicing_argkey, _ = argkeys.popitem() + # We don't have to remove relevant items from later in the + # deque because they'll just be ignored. + matching_items = [ + i for i in scoped_items_by_argkey[slicing_argkey] if i in items + ] + for i in reversed(matching_items): + items_deque.appendleft(i) + # Fix items_by_argkey order. + for other_scope in HIGH_SCOPES: + other_scoped_items_by_argkey = items_by_argkey[other_scope] + for argkey in argkeys_by_item[other_scope].get(i, ()): + argkey_dict = other_scoped_items_by_argkey[argkey] + if not hasattr(sys, "pypy_version_info"): + argkey_dict[i] = None + argkey_dict.move_to_end(i, last=False) + else: + # Work around a bug in PyPy: + # https://github.com/pypy/pypy/issues/5257 + # https://github.com/pytest-dev/pytest/issues/13312 + bkp = argkey_dict.copy() + argkey_dict.clear() + argkey_dict[i] = None + argkey_dict.update(bkp) + break + if no_argkey_items: + reordered_no_argkey_items = reorder_items_atscope( + no_argkey_items, argkeys_by_item, items_by_argkey, scope.next_lower() + ) + items_done.update(reordered_no_argkey_items) + if slicing_argkey is not None: + ignore.add(slicing_argkey) + return items_done + + +@dataclasses.dataclass(frozen=True) +class FuncFixtureInfo: + """Fixture-related information for a fixture-requesting item (e.g. test + function). + + This is used to examine the fixtures which an item requests statically + (known during collection). This includes autouse fixtures, fixtures + requested by the `usefixtures` marker, fixtures requested in the function + parameters, and the transitive closure of these. + + An item may also request fixtures dynamically (using `request.getfixturevalue`); + these are not reflected here. + """ + + __slots__ = ("argnames", "initialnames", "name2fixturedefs", "names_closure") + + # Fixture names that the item requests directly by function parameters. + argnames: tuple[str, ...] + # Fixture names that the item immediately requires. These include + # argnames + fixture names specified via usefixtures and via autouse=True in + # fixture definitions. + initialnames: tuple[str, ...] + # The transitive closure of the fixture names that the item requires. + # Note: can't include dynamic dependencies (`request.getfixturevalue` calls). + names_closure: list[str] + # A map from a fixture name in the transitive closure to the FixtureDefs + # matching the name which are applicable to this function. + # There may be multiple overriding fixtures with the same name. The + # sequence is ordered from furthest to closes to the function. + name2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] + + def prune_dependency_tree(self) -> None: + """Recompute names_closure from initialnames and name2fixturedefs. + + Can only reduce names_closure, which means that the new closure will + always be a subset of the old one. The order is preserved. + + This method is needed because direct parametrization may shadow some + of the fixtures that were included in the originally built dependency + tree. In this way the dependency tree can get pruned, and the closure + of argnames may get reduced. + """ + closure: set[str] = set() + working_set = set(self.initialnames) + while working_set: + argname = working_set.pop() + # Argname may be something not included in the original names_closure, + # in which case we ignore it. This currently happens with pseudo + # FixtureDefs which wrap 'get_direct_param_fixture_func(request)'. + # So they introduce the new dependency 'request' which might have + # been missing in the original tree (closure). + if argname not in closure and argname in self.names_closure: + closure.add(argname) + if argname in self.name2fixturedefs: + working_set.update(self.name2fixturedefs[argname][-1].argnames) + + self.names_closure[:] = sorted(closure, key=self.names_closure.index) + + +class FixtureRequest(abc.ABC): + """The type of the ``request`` fixture. + + A request object gives access to the requesting test context and has a + ``param`` attribute in case the fixture is parametrized. + """ + + def __init__( + self, + pyfuncitem: Function, + fixturename: str | None, + arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]], + fixture_defs: dict[str, FixtureDef[Any]], + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + #: Fixture for which this request is being performed. + self.fixturename: Final = fixturename + self._pyfuncitem: Final = pyfuncitem + # The FixtureDefs for each fixture name requested by this item. + # Starts from the statically-known fixturedefs resolved during + # collection. Dynamically requested fixtures (using + # `request.getfixturevalue("foo")`) are added dynamically. + self._arg2fixturedefs: Final = arg2fixturedefs + # The evaluated argnames so far, mapping to the FixtureDef they resolved + # to. + self._fixture_defs: Final = fixture_defs + # Notes on the type of `param`: + # -`request.param` is only defined in parametrized fixtures, and will raise + # AttributeError otherwise. Python typing has no notion of "undefined", so + # this cannot be reflected in the type. + # - Technically `param` is only (possibly) defined on SubRequest, not + # FixtureRequest, but the typing of that is still in flux so this cheats. + # - In the future we might consider using a generic for the param type, but + # for now just using Any. + self.param: Any + + @property + def _fixturemanager(self) -> FixtureManager: + return self._pyfuncitem.session._fixturemanager + + @property + @abc.abstractmethod + def _scope(self) -> Scope: + raise NotImplementedError() + + @property + def scope(self) -> _ScopeName: + """Scope string, one of "function", "class", "module", "package", "session".""" + return self._scope.value + + @abc.abstractmethod + def _check_scope( + self, + requested_fixturedef: FixtureDef[object], + requested_scope: Scope, + ) -> None: + raise NotImplementedError() + + @property + def fixturenames(self) -> list[str]: + """Names of all active fixtures in this request.""" + result = list(self._pyfuncitem.fixturenames) + result.extend(set(self._fixture_defs).difference(result)) + return result + + @property + @abc.abstractmethod + def node(self): + """Underlying collection node (depends on current request scope).""" + raise NotImplementedError() + + @property + def config(self) -> Config: + """The pytest config object associated with this request.""" + return self._pyfuncitem.config + + @property + def function(self): + """Test function object if the request has a per-function scope.""" + if self.scope != "function": + raise AttributeError( + f"function not available in {self.scope}-scoped context" + ) + return self._pyfuncitem.obj + + @property + def cls(self): + """Class (can be None) where the test function was collected.""" + if self.scope not in ("class", "function"): + raise AttributeError(f"cls not available in {self.scope}-scoped context") + clscol = self._pyfuncitem.getparent(_pytest.python.Class) + if clscol: + return clscol.obj + + @property + def instance(self): + """Instance (can be None) on which test function was collected.""" + if self.scope != "function": + return None + return getattr(self._pyfuncitem, "instance", None) + + @property + def module(self): + """Python module object where the test function was collected.""" + if self.scope not in ("function", "class", "module"): + raise AttributeError(f"module not available in {self.scope}-scoped context") + mod = self._pyfuncitem.getparent(_pytest.python.Module) + assert mod is not None + return mod.obj + + @property + def path(self) -> Path: + """Path where the test function was collected.""" + if self.scope not in ("function", "class", "module", "package"): + raise AttributeError(f"path not available in {self.scope}-scoped context") + return self._pyfuncitem.path + + @property + def keywords(self) -> MutableMapping[str, Any]: + """Keywords/markers dictionary for the underlying node.""" + node: nodes.Node = self.node + return node.keywords + + @property + def session(self) -> Session: + """Pytest session object.""" + return self._pyfuncitem.session + + @abc.abstractmethod + def addfinalizer(self, finalizer: Callable[[], object]) -> None: + """Add finalizer/teardown function to be called without arguments after + the last test within the requesting test context finished execution.""" + raise NotImplementedError() + + def applymarker(self, marker: str | MarkDecorator) -> None: + """Apply a marker to a single test function invocation. + + This method is useful if you don't want to have a keyword/marker + on all function invocations. + + :param marker: + An object created by a call to ``pytest.mark.NAME(...)``. + """ + self.node.add_marker(marker) + + def raiseerror(self, msg: str | None) -> NoReturn: + """Raise a FixtureLookupError exception. + + :param msg: + An optional custom error message. + """ + raise FixtureLookupError(None, self, msg) + + def getfixturevalue(self, argname: str) -> Any: + """Dynamically run a named fixture function. + + Declaring fixtures via function argument is recommended where possible. + But if you can only decide whether to use another fixture at test + setup time, you may use this function to retrieve it inside a fixture + or test function body. + + This method can be used during the test setup phase or the test run + phase, but during the test teardown phase a fixture's value may not + be available. + + :param argname: + The fixture name. + :raises pytest.FixtureLookupError: + If the given fixture could not be found. + """ + # Note that in addition to the use case described in the docstring, + # getfixturevalue() is also called by pytest itself during item and fixture + # setup to evaluate the fixtures that are requested statically + # (using function parameters, autouse, etc). + + fixturedef = self._get_active_fixturedef(argname) + assert fixturedef.cached_result is not None, ( + f'The fixture value for "{argname}" is not available. ' + "This can happen when the fixture has already been torn down." + ) + return fixturedef.cached_result[0] + + def _iter_chain(self) -> Iterator[SubRequest]: + """Yield all SubRequests in the chain, from self up. + + Note: does *not* yield the TopRequest. + """ + current = self + while isinstance(current, SubRequest): + yield current + current = current._parent_request + + def _get_active_fixturedef(self, argname: str) -> FixtureDef[object]: + if argname == "request": + return RequestFixtureDef(self) + + # If we already finished computing a fixture by this name in this item, + # return it. + fixturedef = self._fixture_defs.get(argname) + if fixturedef is not None: + self._check_scope(fixturedef, fixturedef._scope) + return fixturedef + + # Find the appropriate fixturedef. + fixturedefs = self._arg2fixturedefs.get(argname, None) + if fixturedefs is None: + # We arrive here because of a dynamic call to + # getfixturevalue(argname) which was naturally + # not known at parsing/collection time. + fixturedefs = self._fixturemanager.getfixturedefs(argname, self._pyfuncitem) + if fixturedefs is not None: + self._arg2fixturedefs[argname] = fixturedefs + # No fixtures defined with this name. + if fixturedefs is None: + raise FixtureLookupError(argname, self) + # The are no fixtures with this name applicable for the function. + if not fixturedefs: + raise FixtureLookupError(argname, self) + + # A fixture may override another fixture with the same name, e.g. a + # fixture in a module can override a fixture in a conftest, a fixture in + # a class can override a fixture in the module, and so on. + # An overriding fixture can request its own name (possibly indirectly); + # in this case it gets the value of the fixture it overrides, one level + # up. + # Check how many `argname`s deep we are, and take the next one. + # `fixturedefs` is sorted from furthest to closest, so use negative + # indexing to go in reverse. + index = -1 + for request in self._iter_chain(): + if request.fixturename == argname: + index -= 1 + # If already consumed all of the available levels, fail. + if -index > len(fixturedefs): + raise FixtureLookupError(argname, self) + fixturedef = fixturedefs[index] + + # Prepare a SubRequest object for calling the fixture. + try: + callspec = self._pyfuncitem.callspec + except AttributeError: + callspec = None + if callspec is not None and argname in callspec.params: + param = callspec.params[argname] + param_index = callspec.indices[argname] + # The parametrize invocation scope overrides the fixture's scope. + scope = callspec._arg2scope[argname] + else: + param = NOTSET + param_index = 0 + scope = fixturedef._scope + self._check_fixturedef_without_param(fixturedef) + # The parametrize invocation scope only controls caching behavior while + # allowing wider-scoped fixtures to keep depending on the parametrized + # fixture. Scope control is enforced for parametrized fixtures + # by recreating the whole fixture tree on parameter change. + # Hence `fixturedef._scope`, not `scope`. + self._check_scope(fixturedef, fixturedef._scope) + subrequest = SubRequest( + self, scope, param, param_index, fixturedef, _ispytest=True + ) + + # Make sure the fixture value is cached, running it if it isn't + fixturedef.execute(request=subrequest) + + self._fixture_defs[argname] = fixturedef + return fixturedef + + def _check_fixturedef_without_param(self, fixturedef: FixtureDef[object]) -> None: + """Check that this request is allowed to execute this fixturedef without + a param.""" + funcitem = self._pyfuncitem + has_params = fixturedef.params is not None + fixtures_not_supported = getattr(funcitem, "nofuncargs", False) + if has_params and fixtures_not_supported: + msg = ( + f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n" + f"Node id: {funcitem.nodeid}\n" + f"Function type: {type(funcitem).__name__}" + ) + fail(msg, pytrace=False) + if has_params: + frame = inspect.stack()[3] + frameinfo = inspect.getframeinfo(frame[0]) + source_path = absolutepath(frameinfo.filename) + source_lineno = frameinfo.lineno + try: + source_path_str = str(source_path.relative_to(funcitem.config.rootpath)) + except ValueError: + source_path_str = str(source_path) + location = getlocation(fixturedef.func, funcitem.config.rootpath) + msg = ( + "The requested fixture has no parameter defined for test:\n" + f" {funcitem.nodeid}\n\n" + f"Requested fixture '{fixturedef.argname}' defined in:\n" + f"{location}\n\n" + f"Requested here:\n" + f"{source_path_str}:{source_lineno}" + ) + fail(msg, pytrace=False) + + def _get_fixturestack(self) -> list[FixtureDef[Any]]: + values = [request._fixturedef for request in self._iter_chain()] + values.reverse() + return values + + +@final +class TopRequest(FixtureRequest): + """The type of the ``request`` fixture in a test function.""" + + def __init__(self, pyfuncitem: Function, *, _ispytest: bool = False) -> None: + super().__init__( + fixturename=None, + pyfuncitem=pyfuncitem, + arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(), + fixture_defs={}, + _ispytest=_ispytest, + ) + + @property + def _scope(self) -> Scope: + return Scope.Function + + def _check_scope( + self, + requested_fixturedef: FixtureDef[object], + requested_scope: Scope, + ) -> None: + # TopRequest always has function scope so always valid. + pass + + @property + def node(self): + return self._pyfuncitem + + def __repr__(self) -> str: + return f"" + + def _fillfixtures(self) -> None: + item = self._pyfuncitem + for argname in item.fixturenames: + if argname not in item.funcargs: + item.funcargs[argname] = self.getfixturevalue(argname) + + def addfinalizer(self, finalizer: Callable[[], object]) -> None: + self.node.addfinalizer(finalizer) + + +@final +class SubRequest(FixtureRequest): + """The type of the ``request`` fixture in a fixture function requested + (transitively) by a test function.""" + + def __init__( + self, + request: FixtureRequest, + scope: Scope, + param: Any, + param_index: int, + fixturedef: FixtureDef[object], + *, + _ispytest: bool = False, + ) -> None: + super().__init__( + pyfuncitem=request._pyfuncitem, + fixturename=fixturedef.argname, + fixture_defs=request._fixture_defs, + arg2fixturedefs=request._arg2fixturedefs, + _ispytest=_ispytest, + ) + self._parent_request: Final[FixtureRequest] = request + self._scope_field: Final = scope + self._fixturedef: Final[FixtureDef[object]] = fixturedef + if param is not NOTSET: + self.param = param + self.param_index: Final = param_index + + def __repr__(self) -> str: + return f"" + + @property + def _scope(self) -> Scope: + return self._scope_field + + @property + def node(self): + scope = self._scope + if scope is Scope.Function: + # This might also be a non-function Item despite its attribute name. + node: nodes.Node | None = self._pyfuncitem + elif scope is Scope.Package: + node = get_scope_package(self._pyfuncitem, self._fixturedef) + else: + node = get_scope_node(self._pyfuncitem, scope) + if node is None and scope is Scope.Class: + # Fallback to function item itself. + node = self._pyfuncitem + assert node, ( + f'Could not obtain a node for scope "{scope}" for function {self._pyfuncitem!r}' + ) + return node + + def _check_scope( + self, + requested_fixturedef: FixtureDef[object], + requested_scope: Scope, + ) -> None: + if self._scope > requested_scope: + # Try to report something helpful. + argname = requested_fixturedef.argname + fixture_stack = "\n".join( + self._format_fixturedef_line(fixturedef) + for fixturedef in self._get_fixturestack() + ) + requested_fixture = self._format_fixturedef_line(requested_fixturedef) + fail( + f"ScopeMismatch: You tried to access the {requested_scope.value} scoped " + f"fixture {argname} with a {self._scope.value} scoped request object. " + f"Requesting fixture stack:\n{fixture_stack}\n" + f"Requested fixture:\n{requested_fixture}", + pytrace=False, + ) + + def _format_fixturedef_line(self, fixturedef: FixtureDef[object]) -> str: + factory = fixturedef.func + path, lineno = getfslineno(factory) + if isinstance(path, Path): + path = bestrelpath(self._pyfuncitem.session.path, path) + sig = signature(factory) + return f"{path}:{lineno + 1}: def {factory.__name__}{sig}" + + def addfinalizer(self, finalizer: Callable[[], object]) -> None: + self._fixturedef.addfinalizer(finalizer) + + +@final +class FixtureLookupError(LookupError): + """Could not return a requested fixture (missing or invalid).""" + + def __init__( + self, argname: str | None, request: FixtureRequest, msg: str | None = None + ) -> None: + self.argname = argname + self.request = request + self.fixturestack = request._get_fixturestack() + self.msg = msg + + def formatrepr(self) -> FixtureLookupErrorRepr: + tblines: list[str] = [] + addline = tblines.append + stack = [self.request._pyfuncitem.obj] + stack.extend(map(lambda x: x.func, self.fixturestack)) + msg = self.msg + # This function currently makes an assumption that a non-None msg means we + # have a non-empty `self.fixturestack`. This is currently true, but if + # somebody at some point want to extend the use of FixtureLookupError to + # new cases it might break. + # Add the assert to make it clearer to developer that this will fail, otherwise + # it crashes because `fspath` does not get set due to `stack` being empty. + assert self.msg is None or self.fixturestack, ( + "formatrepr assumptions broken, rewrite it to handle it" + ) + if msg is not None: + # The last fixture raise an error, let's present + # it at the requesting side. + stack = stack[:-1] + for function in stack: + fspath, lineno = getfslineno(function) + try: + lines, _ = inspect.getsourcelines(get_real_func(function)) + except (OSError, IndexError, TypeError): + error_msg = "file %s, line %s: source code not available" + addline(error_msg % (fspath, lineno + 1)) + else: + addline(f"file {fspath}, line {lineno + 1}") + for i, line in enumerate(lines): + line = line.rstrip() + addline(" " + line) + if line.lstrip().startswith("def"): + break + + if msg is None: + fm = self.request._fixturemanager + available = set() + parent = self.request._pyfuncitem.parent + assert parent is not None + for name, fixturedefs in fm._arg2fixturedefs.items(): + faclist = list(fm._matchfactories(fixturedefs, parent)) + if faclist: + available.add(name) + if self.argname in available: + msg = ( + f" recursive dependency involving fixture '{self.argname}' detected" + ) + else: + msg = f"fixture '{self.argname}' not found" + msg += "\n available fixtures: {}".format(", ".join(sorted(available))) + msg += "\n use 'pytest --fixtures [testpath]' for help on them." + + return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname) + + +class FixtureLookupErrorRepr(TerminalRepr): + def __init__( + self, + filename: str | os.PathLike[str], + firstlineno: int, + tblines: Sequence[str], + errorstring: str, + argname: str | None, + ) -> None: + self.tblines = tblines + self.errorstring = errorstring + self.filename = filename + self.firstlineno = firstlineno + self.argname = argname + + def toterminal(self, tw: TerminalWriter) -> None: + # tw.line("FixtureLookupError: %s" %(self.argname), red=True) + for tbline in self.tblines: + tw.line(tbline.rstrip()) + lines = self.errorstring.split("\n") + if lines: + tw.line( + f"{FormattedExcinfo.fail_marker} {lines[0].strip()}", + red=True, + ) + for line in lines[1:]: + tw.line( + f"{FormattedExcinfo.flow_marker} {line.strip()}", + red=True, + ) + tw.line() + tw.line(f"{os.fspath(self.filename)}:{self.firstlineno + 1}") + + +def call_fixture_func( + fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs +) -> FixtureValue: + if inspect.isgeneratorfunction(fixturefunc): + fixturefunc = cast(Callable[..., Generator[FixtureValue]], fixturefunc) + generator = fixturefunc(**kwargs) + try: + fixture_result = next(generator) + except StopIteration: + raise ValueError(f"{request.fixturename} did not yield a value") from None + finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator) + request.addfinalizer(finalizer) + else: + fixturefunc = cast(Callable[..., FixtureValue], fixturefunc) + fixture_result = fixturefunc(**kwargs) + return fixture_result + + +def _teardown_yield_fixture(fixturefunc, it) -> None: + """Execute the teardown of a fixture function by advancing the iterator + after the yield and ensure the iteration ends (if not it means there is + more than one yield in the function).""" + try: + next(it) + except StopIteration: + pass + else: + fs, lineno = getfslineno(fixturefunc) + fail( + f"fixture function has more than one 'yield':\n\n" + f"{Source(fixturefunc).indent()}\n" + f"{fs}:{lineno + 1}", + pytrace=False, + ) + + +def _eval_scope_callable( + scope_callable: Callable[[str, Config], _ScopeName], + fixture_name: str, + config: Config, +) -> _ScopeName: + try: + # Type ignored because there is no typing mechanism to specify + # keyword arguments, currently. + result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg] + except Exception as e: + raise TypeError( + f"Error evaluating {scope_callable} while defining fixture '{fixture_name}'.\n" + "Expected a function with the signature (*, fixture_name, config)" + ) from e + if not isinstance(result, str): + fail( + f"Expected {scope_callable} to return a 'str' while defining fixture '{fixture_name}', but it returned:\n" + f"{result!r}", + pytrace=False, + ) + return result + + +class FixtureDef(Generic[FixtureValue]): + """A container for a fixture definition. + + Note: At this time, only explicitly documented fields and methods are + considered public stable API. + """ + + def __init__( + self, + config: Config, + baseid: str | None, + argname: str, + func: _FixtureFunc[FixtureValue], + scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] | None, + params: Sequence[object] | None, + ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, + *, + _ispytest: bool = False, + # only used in a deprecationwarning msg, can be removed in pytest9 + _autouse: bool = False, + ) -> None: + check_ispytest(_ispytest) + # The "base" node ID for the fixture. + # + # This is a node ID prefix. A fixture is only available to a node (e.g. + # a `Function` item) if the fixture's baseid is a nodeid of a parent of + # node. + # + # For a fixture found in a Collector's object (e.g. a `Module`s module, + # a `Class`'s class), the baseid is the Collector's nodeid. + # + # For a fixture found in a conftest plugin, the baseid is the conftest's + # directory path relative to the rootdir. + # + # For other plugins, the baseid is the empty string (always matches). + self.baseid: Final = baseid or "" + # Whether the fixture was found from a node or a conftest in the + # collection tree. Will be false for fixtures defined in non-conftest + # plugins. + self.has_location: Final = baseid is not None + # The fixture factory function. + self.func: Final = func + # The name by which the fixture may be requested. + self.argname: Final = argname + if scope is None: + scope = Scope.Function + elif callable(scope): + scope = _eval_scope_callable(scope, argname, config) + if isinstance(scope, str): + scope = Scope.from_user( + scope, descr=f"Fixture '{func.__name__}'", where=baseid + ) + self._scope: Final = scope + # If the fixture is directly parametrized, the parameter values. + self.params: Final = params + # If the fixture is directly parametrized, a tuple of explicit IDs to + # assign to the parameter values, or a callable to generate an ID given + # a parameter value. + self.ids: Final = ids + # The names requested by the fixtures. + self.argnames: Final = getfuncargnames(func, name=argname) + # If the fixture was executed, the current value of the fixture. + # Can change if the fixture is executed with different parameters. + self.cached_result: _FixtureCachedResult[FixtureValue] | None = None + self._finalizers: Final[list[Callable[[], object]]] = [] + + # only used to emit a deprecationwarning, can be removed in pytest9 + self._autouse = _autouse + + @property + def scope(self) -> _ScopeName: + """Scope string, one of "function", "class", "module", "package", "session".""" + return self._scope.value + + def addfinalizer(self, finalizer: Callable[[], object]) -> None: + self._finalizers.append(finalizer) + + def finish(self, request: SubRequest) -> None: + exceptions: list[BaseException] = [] + while self._finalizers: + fin = self._finalizers.pop() + try: + fin() + except BaseException as e: + exceptions.append(e) + node = request.node + node.ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request) + # Even if finalization fails, we invalidate the cached fixture + # value and remove all finalizers because they may be bound methods + # which will keep instances alive. + self.cached_result = None + self._finalizers.clear() + if len(exceptions) == 1: + raise exceptions[0] + elif len(exceptions) > 1: + msg = f'errors while tearing down fixture "{self.argname}" of {node}' + raise BaseExceptionGroup(msg, exceptions[::-1]) + + def execute(self, request: SubRequest) -> FixtureValue: + """Return the value of this fixture, executing it if not cached.""" + # Ensure that the dependent fixtures requested by this fixture are loaded. + # This needs to be done before checking if we have a cached value, since + # if a dependent fixture has their cache invalidated, e.g. due to + # parametrization, they finalize themselves and fixtures depending on it + # (which will likely include this fixture) setting `self.cached_result = None`. + # See #4871 + requested_fixtures_that_should_finalize_us = [] + for argname in self.argnames: + fixturedef = request._get_active_fixturedef(argname) + # Saves requested fixtures in a list so we later can add our finalizer + # to them, ensuring that if a requested fixture gets torn down we get torn + # down first. This is generally handled by SetupState, but still currently + # needed when this fixture is not parametrized but depends on a parametrized + # fixture. + requested_fixtures_that_should_finalize_us.append(fixturedef) + + # Check for (and return) cached value/exception. + if self.cached_result is not None: + request_cache_key = self.cache_key(request) + cache_key = self.cached_result[1] + try: + # Attempt to make a normal == check: this might fail for objects + # which do not implement the standard comparison (like numpy arrays -- #6497). + cache_hit = bool(request_cache_key == cache_key) + except (ValueError, RuntimeError): + # If the comparison raises, use 'is' as fallback. + cache_hit = request_cache_key is cache_key + + if cache_hit: + if self.cached_result[2] is not None: + exc, exc_tb = self.cached_result[2] + raise exc.with_traceback(exc_tb) + else: + return self.cached_result[0] + # We have a previous but differently parametrized fixture instance + # so we need to tear it down before creating a new one. + self.finish(request) + assert self.cached_result is None + + # Add finalizer to requested fixtures we saved previously. + # We make sure to do this after checking for cached value to avoid + # adding our finalizer multiple times. (#12135) + finalizer = functools.partial(self.finish, request=request) + for parent_fixture in requested_fixtures_that_should_finalize_us: + parent_fixture.addfinalizer(finalizer) + + ihook = request.node.ihook + try: + # Setup the fixture, run the code in it, and cache the value + # in self.cached_result. + result: FixtureValue = ihook.pytest_fixture_setup( + fixturedef=self, request=request + ) + finally: + # Schedule our finalizer, even if the setup failed. + request.node.addfinalizer(finalizer) + + return result + + def cache_key(self, request: SubRequest) -> object: + return getattr(request, "param", None) + + def __repr__(self) -> str: + return f"" + + +class RequestFixtureDef(FixtureDef[FixtureRequest]): + """A custom FixtureDef for the special "request" fixture. + + A new one is generated on-demand whenever "request" is requested. + """ + + def __init__(self, request: FixtureRequest) -> None: + super().__init__( + config=request.config, + baseid=None, + argname="request", + func=lambda: request, + scope=Scope.Function, + params=None, + _ispytest=True, + ) + self.cached_result = (request, [0], None) + + def addfinalizer(self, finalizer: Callable[[], object]) -> None: + pass + + +def resolve_fixture_function( + fixturedef: FixtureDef[FixtureValue], request: FixtureRequest +) -> _FixtureFunc[FixtureValue]: + """Get the actual callable that can be called to obtain the fixture + value.""" + fixturefunc = fixturedef.func + # The fixture function needs to be bound to the actual + # request.instance so that code working with "fixturedef" behaves + # as expected. + instance = request.instance + if instance is not None: + # Handle the case where fixture is defined not in a test class, but some other class + # (for example a plugin class with a fixture), see #2270. + if hasattr(fixturefunc, "__self__") and not isinstance( + instance, + fixturefunc.__self__.__class__, + ): + return fixturefunc + fixturefunc = getimfunc(fixturedef.func) + if fixturefunc != fixturedef.func: + fixturefunc = fixturefunc.__get__(instance) + return fixturefunc + + +def pytest_fixture_setup( + fixturedef: FixtureDef[FixtureValue], request: SubRequest +) -> FixtureValue: + """Execution of fixture setup.""" + kwargs = {} + for argname in fixturedef.argnames: + kwargs[argname] = request.getfixturevalue(argname) + + fixturefunc = resolve_fixture_function(fixturedef, request) + my_cache_key = fixturedef.cache_key(request) + + if inspect.isasyncgenfunction(fixturefunc) or inspect.iscoroutinefunction( + fixturefunc + ): + auto_str = " with autouse=True" if fixturedef._autouse else "" + + warnings.warn( + PytestRemovedIn9Warning( + f"{request.node.name!r} requested an async fixture " + f"{request.fixturename!r}{auto_str}, with no plugin or hook that " + "handled it. This is usually an error, as pytest does not natively " + "support it. " + "This will turn into an error in pytest 9.\n" + "See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture" + ), + # no stacklevel will point at users code, so we just point here + stacklevel=1, + ) + + try: + result = call_fixture_func(fixturefunc, request, kwargs) + except TEST_OUTCOME as e: + if isinstance(e, skip.Exception): + # The test requested a fixture which caused a skip. + # Don't show the fixture as the skip location, as then the user + # wouldn't know which test skipped. + e._use_item_location = True + fixturedef.cached_result = (None, my_cache_key, (e, e.__traceback__)) + raise + fixturedef.cached_result = (result, my_cache_key, None) + return result + + +@final +@dataclasses.dataclass(frozen=True) +class FixtureFunctionMarker: + scope: _ScopeName | Callable[[str, Config], _ScopeName] + params: tuple[object, ...] | None + autouse: bool = False + ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None + name: str | None = None + + _ispytest: dataclasses.InitVar[bool] = False + + def __post_init__(self, _ispytest: bool) -> None: + check_ispytest(_ispytest) + + def __call__(self, function: FixtureFunction) -> FixtureFunctionDefinition: + if inspect.isclass(function): + raise ValueError("class fixtures not supported (maybe in the future)") + + if isinstance(function, FixtureFunctionDefinition): + raise ValueError( + f"@pytest.fixture is being applied more than once to the same function {function.__name__!r}" + ) + + if hasattr(function, "pytestmark"): + warnings.warn(MARKED_FIXTURE, stacklevel=2) + + fixture_definition = FixtureFunctionDefinition( + function=function, fixture_function_marker=self, _ispytest=True + ) + + name = self.name or function.__name__ + if name == "request": + location = getlocation(function) + fail( + f"'request' is a reserved word for fixtures, use another name:\n {location}", + pytrace=False, + ) + + return fixture_definition + + +# TODO: paramspec/return type annotation tracking and storing +class FixtureFunctionDefinition: + def __init__( + self, + *, + function: Callable[..., Any], + fixture_function_marker: FixtureFunctionMarker, + instance: object | None = None, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + self.name = fixture_function_marker.name or function.__name__ + # In order to show the function that this fixture contains in messages. + # Set the __name__ to be same as the function __name__ or the given fixture name. + self.__name__ = self.name + self._fixture_function_marker = fixture_function_marker + if instance is not None: + self._fixture_function = cast( + Callable[..., Any], function.__get__(instance) + ) + else: + self._fixture_function = function + functools.update_wrapper(self, function) + + def __repr__(self) -> str: + return f"" + + def __get__(self, instance, owner=None): + """Behave like a method if the function it was applied to was a method.""" + return FixtureFunctionDefinition( + function=self._fixture_function, + fixture_function_marker=self._fixture_function_marker, + instance=instance, + _ispytest=True, + ) + + def __call__(self, *args: Any, **kwds: Any) -> Any: + message = ( + f'Fixture "{self.name}" called directly. Fixtures are not meant to be called directly,\n' + "but are created automatically when test functions request them as parameters.\n" + "See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and\n" + "https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly" + ) + fail(message, pytrace=False) + + def _get_wrapped_function(self) -> Callable[..., Any]: + return self._fixture_function + + +@overload +def fixture( + fixture_function: Callable[..., object], + *, + scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., + params: Iterable[object] | None = ..., + autouse: bool = ..., + ids: Sequence[object | None] | Callable[[Any], object | None] | None = ..., + name: str | None = ..., +) -> FixtureFunctionDefinition: ... + + +@overload +def fixture( + fixture_function: None = ..., + *, + scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., + params: Iterable[object] | None = ..., + autouse: bool = ..., + ids: Sequence[object | None] | Callable[[Any], object | None] | None = ..., + name: str | None = None, +) -> FixtureFunctionMarker: ... + + +def fixture( + fixture_function: FixtureFunction | None = None, + *, + scope: _ScopeName | Callable[[str, Config], _ScopeName] = "function", + params: Iterable[object] | None = None, + autouse: bool = False, + ids: Sequence[object | None] | Callable[[Any], object | None] | None = None, + name: str | None = None, +) -> FixtureFunctionMarker | FixtureFunctionDefinition: + """Decorator to mark a fixture factory function. + + This decorator can be used, with or without parameters, to define a + fixture function. + + The name of the fixture function can later be referenced to cause its + invocation ahead of running tests: test modules or classes can use the + ``pytest.mark.usefixtures(fixturename)`` marker. + + Test functions can directly use fixture names as input arguments in which + case the fixture instance returned from the fixture function will be + injected. + + Fixtures can provide their values to test functions using ``return`` or + ``yield`` statements. When using ``yield`` the code block after the + ``yield`` statement is executed as teardown code regardless of the test + outcome, and must yield exactly once. + + :param scope: + The scope for which this fixture is shared; one of ``"function"`` + (default), ``"class"``, ``"module"``, ``"package"`` or ``"session"``. + + This parameter may also be a callable which receives ``(fixture_name, config)`` + as parameters, and must return a ``str`` with one of the values mentioned above. + + See :ref:`dynamic scope` in the docs for more information. + + :param params: + An optional list of parameters which will cause multiple invocations + of the fixture function and all of the tests using it. The current + parameter is available in ``request.param``. + + :param autouse: + If True, the fixture func is activated for all tests that can see it. + If False (the default), an explicit reference is needed to activate + the fixture. + + :param ids: + Sequence of ids each corresponding to the params so that they are + part of the test id. If no ids are provided they will be generated + automatically from the params. + + :param name: + The name of the fixture. This defaults to the name of the decorated + function. If a fixture is used in the same module in which it is + defined, the function name of the fixture will be shadowed by the + function arg that requests the fixture; one way to resolve this is to + name the decorated function ``fixture_`` and then use + ``@pytest.fixture(name='')``. + """ + fixture_marker = FixtureFunctionMarker( + scope=scope, + params=tuple(params) if params is not None else None, + autouse=autouse, + ids=None if ids is None else ids if callable(ids) else tuple(ids), + name=name, + _ispytest=True, + ) + + # Direct decoration. + if fixture_function: + return fixture_marker(fixture_function) + + return fixture_marker + + +def yield_fixture( + fixture_function=None, + *args, + scope="function", + params=None, + autouse=False, + ids=None, + name=None, +): + """(Return a) decorator to mark a yield-fixture factory function. + + .. deprecated:: 3.0 + Use :py:func:`pytest.fixture` directly instead. + """ + warnings.warn(YIELD_FIXTURE, stacklevel=2) + return fixture( + fixture_function, + *args, + scope=scope, + params=params, + autouse=autouse, + ids=ids, + name=name, + ) + + +@fixture(scope="session") +def pytestconfig(request: FixtureRequest) -> Config: + """Session-scoped fixture that returns the session's :class:`pytest.Config` + object. + + Example:: + + def test_foo(pytestconfig): + if pytestconfig.get_verbosity() > 0: + ... + + """ + return request.config + + +def pytest_addoption(parser: Parser) -> None: + parser.addini( + "usefixtures", + type="args", + default=[], + help="List of default fixtures to be used with this project", + ) + group = parser.getgroup("general") + group.addoption( + "--fixtures", + "--funcargs", + action="store_true", + dest="showfixtures", + default=False, + help="Show available fixtures, sorted by plugin appearance " + "(fixtures with leading '_' are only shown with '-v')", + ) + group.addoption( + "--fixtures-per-test", + action="store_true", + dest="show_fixtures_per_test", + default=False, + help="Show fixtures per test", + ) + + +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: + if config.option.showfixtures: + showfixtures(config) + return 0 + if config.option.show_fixtures_per_test: + show_fixtures_per_test(config) + return 0 + return None + + +def _get_direct_parametrize_args(node: nodes.Node) -> set[str]: + """Return all direct parametrization arguments of a node, so we don't + mistake them for fixtures. + + Check https://github.com/pytest-dev/pytest/issues/5036. + + These things are done later as well when dealing with parametrization + so this could be improved. + """ + parametrize_argnames: set[str] = set() + for marker in node.iter_markers(name="parametrize"): + if not marker.kwargs.get("indirect", False): + p_argnames, _ = ParameterSet._parse_parametrize_args( + *marker.args, **marker.kwargs + ) + parametrize_argnames.update(p_argnames) + return parametrize_argnames + + +def deduplicate_names(*seqs: Iterable[str]) -> tuple[str, ...]: + """De-duplicate the sequence of names while keeping the original order.""" + # Ideally we would use a set, but it does not preserve insertion order. + return tuple(dict.fromkeys(name for seq in seqs for name in seq)) + + +class FixtureManager: + """pytest fixture definitions and information is stored and managed + from this class. + + During collection fm.parsefactories() is called multiple times to parse + fixture function definitions into FixtureDef objects and internal + data structures. + + During collection of test functions, metafunc-mechanics instantiate + a FuncFixtureInfo object which is cached per node/func-name. + This FuncFixtureInfo object is later retrieved by Function nodes + which themselves offer a fixturenames attribute. + + The FuncFixtureInfo object holds information about fixtures and FixtureDefs + relevant for a particular function. An initial list of fixtures is + assembled like this: + + - config-defined usefixtures + - autouse-marked fixtures along the collection chain up from the function + - usefixtures markers at module/class/function level + - test function funcargs + + Subsequently the funcfixtureinfo.fixturenames attribute is computed + as the closure of the fixtures needed to setup the initial fixtures, + i.e. fixtures needed by fixture functions themselves are appended + to the fixturenames list. + + Upon the test-setup phases all fixturenames are instantiated, retrieved + by a lookup of their FuncFixtureInfo. + """ + + def __init__(self, session: Session) -> None: + self.session = session + self.config: Config = session.config + # Maps a fixture name (argname) to all of the FixtureDefs in the test + # suite/plugins defined with this name. Populated by parsefactories(). + # TODO: The order of the FixtureDefs list of each arg is significant, + # explain. + self._arg2fixturedefs: Final[dict[str, list[FixtureDef[Any]]]] = {} + self._holderobjseen: Final[set[object]] = set() + # A mapping from a nodeid to a list of autouse fixtures it defines. + self._nodeid_autousenames: Final[dict[str, list[str]]] = { + "": self.config.getini("usefixtures"), + } + session.config.pluginmanager.register(self, "funcmanage") + + def getfixtureinfo( + self, + node: nodes.Item, + func: Callable[..., object] | None, + cls: type | None, + ) -> FuncFixtureInfo: + """Calculate the :class:`FuncFixtureInfo` for an item. + + If ``func`` is None, or if the item sets an attribute + ``nofuncargs = True``, then ``func`` is not examined at all. + + :param node: + The item requesting the fixtures. + :param func: + The item's function. + :param cls: + If the function is a method, the method's class. + """ + if func is not None and not getattr(node, "nofuncargs", False): + argnames = getfuncargnames(func, name=node.name, cls=cls) + else: + argnames = () + usefixturesnames = self._getusefixturesnames(node) + autousenames = self._getautousenames(node) + initialnames = deduplicate_names(autousenames, usefixturesnames, argnames) + + direct_parametrize_args = _get_direct_parametrize_args(node) + + names_closure, arg2fixturedefs = self.getfixtureclosure( + parentnode=node, + initialnames=initialnames, + ignore_args=direct_parametrize_args, + ) + + return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs) + + def pytest_plugin_registered(self, plugin: _PluggyPlugin, plugin_name: str) -> None: + # Fixtures defined in conftest plugins are only visible to within the + # conftest's directory. This is unlike fixtures in non-conftest plugins + # which have global visibility. So for conftests, construct the base + # nodeid from the plugin name (which is the conftest path). + if plugin_name and plugin_name.endswith("conftest.py"): + # Note: we explicitly do *not* use `plugin.__file__` here -- The + # difference is that plugin_name has the correct capitalization on + # case-insensitive systems (Windows) and other normalization issues + # (issue #11816). + conftestpath = absolutepath(plugin_name) + try: + nodeid = str(conftestpath.parent.relative_to(self.config.rootpath)) + except ValueError: + nodeid = "" + if nodeid == ".": + nodeid = "" + if os.sep != nodes.SEP: + nodeid = nodeid.replace(os.sep, nodes.SEP) + else: + nodeid = None + + self.parsefactories(plugin, nodeid) + + def _getautousenames(self, node: nodes.Node) -> Iterator[str]: + """Return the names of autouse fixtures applicable to node.""" + for parentnode in node.listchain(): + basenames = self._nodeid_autousenames.get(parentnode.nodeid) + if basenames: + yield from basenames + + def _getusefixturesnames(self, node: nodes.Item) -> Iterator[str]: + """Return the names of usefixtures fixtures applicable to node.""" + for marker_node, mark in node.iter_markers_with_node(name="usefixtures"): + if not mark.args: + marker_node.warn( + PytestWarning( + f"usefixtures() in {node.nodeid} without arguments has no effect" + ) + ) + yield from mark.args + + def getfixtureclosure( + self, + parentnode: nodes.Node, + initialnames: tuple[str, ...], + ignore_args: AbstractSet[str], + ) -> tuple[list[str], dict[str, Sequence[FixtureDef[Any]]]]: + # Collect the closure of all fixtures, starting with the given + # fixturenames as the initial set. As we have to visit all + # factory definitions anyway, we also return an arg2fixturedefs + # mapping so that the caller can reuse it and does not have + # to re-discover fixturedefs again for each fixturename + # (discovering matching fixtures for a given name/node is expensive). + + fixturenames_closure = list(initialnames) + + arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] = {} + + # Track the index for each fixture name in the simulated stack. + # Needed for handling override chains correctly, similar to _get_active_fixturedef. + # Using negative indices: -1 is the most specific (last), -2 is second to last, etc. + current_indices: dict[str, int] = {} + + def process_argname(argname: str) -> None: + # Optimization: already processed this argname. + if current_indices.get(argname) == -1: + return + + if argname not in fixturenames_closure: + fixturenames_closure.append(argname) + + if argname in ignore_args: + return + + fixturedefs = arg2fixturedefs.get(argname) + if not fixturedefs: + fixturedefs = self.getfixturedefs(argname, parentnode) + if not fixturedefs: + # Fixture not defined or not visible (will error during runtest). + return + arg2fixturedefs[argname] = fixturedefs + + index = current_indices.get(argname, -1) + if -index > len(fixturedefs): + # Exhausted the override chain (will error during runtest). + return + fixturedef = fixturedefs[index] + + current_indices[argname] = index - 1 + for dep in fixturedef.argnames: + process_argname(dep) + current_indices[argname] = index + + for name in initialnames: + process_argname(name) + + def sort_by_scope(arg_name: str) -> Scope: + try: + fixturedefs = arg2fixturedefs[arg_name] + except KeyError: + return Scope.Function + else: + return fixturedefs[-1]._scope + + fixturenames_closure.sort(key=sort_by_scope, reverse=True) + return fixturenames_closure, arg2fixturedefs + + def pytest_generate_tests(self, metafunc: Metafunc) -> None: + """Generate new tests based on parametrized fixtures used by the given metafunc""" + + def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]: + args, _ = ParameterSet._parse_parametrize_args(*mark.args, **mark.kwargs) + return args + + for argname in metafunc.fixturenames: + # Get the FixtureDefs for the argname. + fixture_defs = metafunc._arg2fixturedefs.get(argname) + if not fixture_defs: + # Will raise FixtureLookupError at setup time if not parametrized somewhere + # else (e.g @pytest.mark.parametrize) + continue + + # If the test itself parametrizes using this argname, give it + # precedence. + if any( + argname in get_parametrize_mark_argnames(mark) + for mark in metafunc.definition.iter_markers("parametrize") + ): + continue + + # In the common case we only look at the fixture def with the + # closest scope (last in the list). But if the fixture overrides + # another fixture, while requesting the super fixture, keep going + # in case the super fixture is parametrized (#1953). + for fixturedef in reversed(fixture_defs): + # Fixture is parametrized, apply it and stop. + if fixturedef.params is not None: + metafunc.parametrize( + argname, + fixturedef.params, + indirect=True, + scope=fixturedef.scope, + ids=fixturedef.ids, + ) + break + + # Not requesting the overridden super fixture, stop. + if argname not in fixturedef.argnames: + break + + # Try next super fixture, if any. + + def pytest_collection_modifyitems(self, items: list[nodes.Item]) -> None: + # Separate parametrized setups. + items[:] = reorder_items(items) + + def _register_fixture( + self, + *, + name: str, + func: _FixtureFunc[object], + nodeid: str | None, + scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] = "function", + params: Sequence[object] | None = None, + ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, + autouse: bool = False, + ) -> None: + """Register a fixture + + :param name: + The fixture's name. + :param func: + The fixture's implementation function. + :param nodeid: + The visibility of the fixture. The fixture will be available to the + node with this nodeid and its children in the collection tree. + None means that the fixture is visible to the entire collection tree, + e.g. a fixture defined for general use in a plugin. + :param scope: + The fixture's scope. + :param params: + The fixture's parametrization params. + :param ids: + The fixture's IDs. + :param autouse: + Whether this is an autouse fixture. + """ + fixture_def = FixtureDef( + config=self.config, + baseid=nodeid, + argname=name, + func=func, + scope=scope, + params=params, + ids=ids, + _ispytest=True, + _autouse=autouse, + ) + + faclist = self._arg2fixturedefs.setdefault(name, []) + if fixture_def.has_location: + faclist.append(fixture_def) + else: + # fixturedefs with no location are at the front + # so this inserts the current fixturedef after the + # existing fixturedefs from external plugins but + # before the fixturedefs provided in conftests. + i = len([f for f in faclist if not f.has_location]) + faclist.insert(i, fixture_def) + if autouse: + self._nodeid_autousenames.setdefault(nodeid or "", []).append(name) + + @overload + def parsefactories( + self, + node_or_obj: nodes.Node, + ) -> None: + raise NotImplementedError() + + @overload + def parsefactories( + self, + node_or_obj: object, + nodeid: str | None, + ) -> None: + raise NotImplementedError() + + def parsefactories( + self, + node_or_obj: nodes.Node | object, + nodeid: str | NotSetType | None = NOTSET, + ) -> None: + """Collect fixtures from a collection node or object. + + Found fixtures are parsed into `FixtureDef`s and saved. + + If `node_or_object` is a collection node (with an underlying Python + object), the node's object is traversed and the node's nodeid is used to + determine the fixtures' visibility. `nodeid` must not be specified in + this case. + + If `node_or_object` is an object (e.g. a plugin), the object is + traversed and the given `nodeid` is used to determine the fixtures' + visibility. `nodeid` must be specified in this case; None and "" mean + total visibility. + """ + if nodeid is not NOTSET: + holderobj = node_or_obj + else: + assert isinstance(node_or_obj, nodes.Node) + holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined] + assert isinstance(node_or_obj.nodeid, str) + nodeid = node_or_obj.nodeid + if holderobj in self._holderobjseen: + return + + # Avoid accessing `@property` (and other descriptors) when iterating fixtures. + if not safe_isclass(holderobj) and not isinstance(holderobj, types.ModuleType): + holderobj_tp: object = type(holderobj) + else: + holderobj_tp = holderobj + + self._holderobjseen.add(holderobj) + for name in dir(holderobj): + # The attribute can be an arbitrary descriptor, so the attribute + # access below can raise. safe_getattr() ignores such exceptions. + obj_ub = safe_getattr(holderobj_tp, name, None) + if type(obj_ub) is FixtureFunctionDefinition: + marker = obj_ub._fixture_function_marker + if marker.name: + fixture_name = marker.name + else: + fixture_name = name + + # OK we know it is a fixture -- now safe to look up on the _instance_. + try: + obj = getattr(holderobj, name) + # if the fixture is named in the decorator we cannot find it in the module + except AttributeError: + obj = obj_ub + + func = obj._get_wrapped_function() + + self._register_fixture( + name=fixture_name, + nodeid=nodeid, + func=func, + scope=marker.scope, + params=marker.params, + ids=marker.ids, + autouse=marker.autouse, + ) + + def getfixturedefs( + self, argname: str, node: nodes.Node + ) -> Sequence[FixtureDef[Any]] | None: + """Get FixtureDefs for a fixture name which are applicable + to a given node. + + Returns None if there are no fixtures at all defined with the given + name. (This is different from the case in which there are fixtures + with the given name, but none applicable to the node. In this case, + an empty result is returned). + + :param argname: Name of the fixture to search for. + :param node: The requesting Node. + """ + try: + fixturedefs = self._arg2fixturedefs[argname] + except KeyError: + return None + return tuple(self._matchfactories(fixturedefs, node)) + + def _matchfactories( + self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node + ) -> Iterator[FixtureDef[Any]]: + parentnodeids = {n.nodeid for n in node.iter_parents()} + for fixturedef in fixturedefs: + if fixturedef.baseid in parentnodeids: + yield fixturedef + + +def show_fixtures_per_test(config: Config) -> int | ExitCode: + from _pytest.main import wrap_session + + return wrap_session(config, _show_fixtures_per_test) + + +_PYTEST_DIR = Path(_pytest.__file__).parent + + +def _pretty_fixture_path(invocation_dir: Path, func) -> str: + loc = Path(getlocation(func, invocation_dir)) + prefix = Path("...", "_pytest") + try: + return str(prefix / loc.relative_to(_PYTEST_DIR)) + except ValueError: + return bestrelpath(invocation_dir, loc) + + +def _show_fixtures_per_test(config: Config, session: Session) -> None: + import _pytest.config + + session.perform_collect() + invocation_dir = config.invocation_params.dir + tw = _pytest.config.create_terminal_writer(config) + verbose = config.get_verbosity() + + def get_best_relpath(func) -> str: + loc = getlocation(func, invocation_dir) + return bestrelpath(invocation_dir, Path(loc)) + + def write_fixture(fixture_def: FixtureDef[object]) -> None: + argname = fixture_def.argname + if verbose <= 0 and argname.startswith("_"): + return + prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func) + tw.write(f"{argname}", green=True) + tw.write(f" -- {prettypath}", yellow=True) + tw.write("\n") + fixture_doc = inspect.getdoc(fixture_def.func) + if fixture_doc: + write_docstring( + tw, + fixture_doc.split("\n\n", maxsplit=1)[0] + if verbose <= 0 + else fixture_doc, + ) + else: + tw.line(" no docstring available", red=True) + + def write_item(item: nodes.Item) -> None: + # Not all items have _fixtureinfo attribute. + info: FuncFixtureInfo | None = getattr(item, "_fixtureinfo", None) + if info is None or not info.name2fixturedefs: + # This test item does not use any fixtures. + return + tw.line() + tw.sep("-", f"fixtures used by {item.name}") + # TODO: Fix this type ignore. + tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined] + # dict key not used in loop but needed for sorting. + for _, fixturedefs in sorted(info.name2fixturedefs.items()): + assert fixturedefs is not None + if not fixturedefs: + continue + # Last item is expected to be the one used by the test item. + write_fixture(fixturedefs[-1]) + + for session_item in session.items: + write_item(session_item) + + +def showfixtures(config: Config) -> int | ExitCode: + from _pytest.main import wrap_session + + return wrap_session(config, _showfixtures_main) + + +def _showfixtures_main(config: Config, session: Session) -> None: + import _pytest.config + + session.perform_collect() + invocation_dir = config.invocation_params.dir + tw = _pytest.config.create_terminal_writer(config) + verbose = config.get_verbosity() + + fm = session._fixturemanager + + available = [] + seen: set[tuple[str, str]] = set() + + for argname, fixturedefs in fm._arg2fixturedefs.items(): + assert fixturedefs is not None + if not fixturedefs: + continue + for fixturedef in fixturedefs: + loc = getlocation(fixturedef.func, invocation_dir) + if (fixturedef.argname, loc) in seen: + continue + seen.add((fixturedef.argname, loc)) + available.append( + ( + len(fixturedef.baseid), + fixturedef.func.__module__, + _pretty_fixture_path(invocation_dir, fixturedef.func), + fixturedef.argname, + fixturedef, + ) + ) + + available.sort() + currentmodule = None + for baseid, module, prettypath, argname, fixturedef in available: + if currentmodule != module: + if not module.startswith("_pytest."): + tw.line() + tw.sep("-", f"fixtures defined from {module}") + currentmodule = module + if verbose <= 0 and argname.startswith("_"): + continue + tw.write(f"{argname}", green=True) + if fixturedef.scope != "function": + tw.write(f" [{fixturedef.scope} scope]", cyan=True) + tw.write(f" -- {prettypath}", yellow=True) + tw.write("\n") + doc = inspect.getdoc(fixturedef.func) + if doc: + write_docstring( + tw, doc.split("\n\n", maxsplit=1)[0] if verbose <= 0 else doc + ) + else: + tw.line(" no docstring available", red=True) + tw.line() + + +def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: + for line in doc.split("\n"): + tw.line(indent + line) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/freeze_support.py b/Backend/venv/lib/python3.12/site-packages/_pytest/freeze_support.py new file mode 100644 index 00000000..959ff071 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/freeze_support.py @@ -0,0 +1,45 @@ +"""Provides a function to report all internal modules for using freezing +tools.""" + +from __future__ import annotations + +from collections.abc import Iterator +import types + + +def freeze_includes() -> list[str]: + """Return a list of module names used by pytest that should be + included by cx_freeze.""" + import _pytest + + result = list(_iter_all_modules(_pytest)) + return result + + +def _iter_all_modules( + package: str | types.ModuleType, + prefix: str = "", +) -> Iterator[str]: + """Iterate over the names of all modules that can be found in the given + package, recursively. + + >>> import _pytest + >>> list(_iter_all_modules(_pytest)) + ['_pytest._argcomplete', '_pytest._code.code', ...] + """ + import os + import pkgutil + + if isinstance(package, str): + path = package + else: + # Type ignored because typeshed doesn't define ModuleType.__path__ + # (only defined on packages). + package_path = package.__path__ + path, prefix = package_path[0], package.__name__ + "." + for _, name, is_package in pkgutil.iter_modules([path]): + if is_package: + for m in _iter_all_modules(os.path.join(path, name), prefix=name + "."): + yield prefix + m + else: + yield prefix + name diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/helpconfig.py b/Backend/venv/lib/python3.12/site-packages/_pytest/helpconfig.py new file mode 100644 index 00000000..6a22c9f5 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/helpconfig.py @@ -0,0 +1,293 @@ +# mypy: allow-untyped-defs +"""Version info, help messages, tracing configuration.""" + +from __future__ import annotations + +import argparse +from collections.abc import Generator +from collections.abc import Sequence +import os +import sys +from typing import Any + +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config import PrintHelp +from _pytest.config.argparsing import Parser +from _pytest.terminal import TerminalReporter +import pytest + + +class HelpAction(argparse.Action): + """An argparse Action that will raise a PrintHelp exception in order to skip + the rest of the argument parsing when --help is passed. + + This prevents argparse from raising UsageError when `--help` is used along + with missing required arguments when any are defined, for example by + ``pytest_addoption``. This is similar to the way that the builtin argparse + --help option is implemented by raising SystemExit. + + To opt in to this behavior, the parse caller must set + `namespace._raise_print_help = True`. Otherwise it just sets the option. + """ + + def __init__( + self, option_strings: Sequence[str], dest: str, *, help: str | None = None + ) -> None: + super().__init__( + option_strings=option_strings, + dest=dest, + nargs=0, + const=True, + default=False, + help=help, + ) + + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: str | Sequence[Any] | None, + option_string: str | None = None, + ) -> None: + setattr(namespace, self.dest, self.const) + + if getattr(namespace, "_raise_print_help", False): + raise PrintHelp + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("debugconfig") + group.addoption( + "--version", + "-V", + action="count", + default=0, + dest="version", + help="Display pytest version and information about plugins. " + "When given twice, also display information about plugins.", + ) + group._addoption( # private to use reserved lower-case short option + "-h", + "--help", + action=HelpAction, + dest="help", + help="Show help message and configuration info", + ) + group._addoption( # private to use reserved lower-case short option + "-p", + action="append", + dest="plugins", + default=[], + metavar="name", + help="Early-load given plugin module name or entry point (multi-allowed). " + "To avoid loading of plugins, use the `no:` prefix, e.g. " + "`no:doctest`. See also --disable-plugin-autoload.", + ) + group.addoption( + "--disable-plugin-autoload", + action="store_true", + default=False, + help="Disable plugin auto-loading through entry point packaging metadata. " + "Only plugins explicitly specified in -p or env var PYTEST_PLUGINS will be loaded.", + ) + group.addoption( + "--traceconfig", + "--trace-config", + action="store_true", + default=False, + help="Trace considerations of conftest.py files", + ) + group.addoption( + "--debug", + action="store", + nargs="?", + const="pytestdebug.log", + dest="debug", + metavar="DEBUG_FILE_NAME", + help="Store internal tracing debug information in this log file. " + "This file is opened with 'w' and truncated as a result, care advised. " + "Default: pytestdebug.log.", + ) + group._addoption( # private to use reserved lower-case short option + "-o", + "--override-ini", + dest="override_ini", + action="append", + help='Override configuration option with "option=value" style, ' + "e.g. `-o strict_xfail=True -o cache_dir=cache`.", + ) + + +@pytest.hookimpl(wrapper=True) +def pytest_cmdline_parse() -> Generator[None, Config, Config]: + config = yield + + if config.option.debug: + # --debug | --debug was provided. + path = config.option.debug + debugfile = open(path, "w", encoding="utf-8") + debugfile.write( + "versions pytest-{}, " + "python-{}\ninvocation_dir={}\ncwd={}\nargs={}\n\n".format( + pytest.__version__, + ".".join(map(str, sys.version_info)), + config.invocation_params.dir, + os.getcwd(), + config.invocation_params.args, + ) + ) + config.trace.root.setwriter(debugfile.write) + undo_tracing = config.pluginmanager.enable_tracing() + sys.stderr.write(f"writing pytest debug information to {path}\n") + + def unset_tracing() -> None: + debugfile.close() + sys.stderr.write(f"wrote pytest debug information to {debugfile.name}\n") + config.trace.root.setwriter(None) + undo_tracing() + + config.add_cleanup(unset_tracing) + + return config + + +def show_version_verbose(config: Config) -> None: + """Show verbose pytest version installation, including plugins.""" + sys.stdout.write( + f"This is pytest version {pytest.__version__}, imported from {pytest.__file__}\n" + ) + plugininfo = getpluginversioninfo(config) + if plugininfo: + for line in plugininfo: + sys.stdout.write(line + "\n") + + +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: + # Note: a single `--version` argument is handled directly by `Config.main()` to avoid starting up the entire + # pytest infrastructure just to display the version (#13574). + if config.option.version > 1: + show_version_verbose(config) + return ExitCode.OK + elif config.option.help: + config._do_configure() + showhelp(config) + config._ensure_unconfigure() + return ExitCode.OK + return None + + +def showhelp(config: Config) -> None: + import textwrap + + reporter: TerminalReporter | None = config.pluginmanager.get_plugin( + "terminalreporter" + ) + assert reporter is not None + tw = reporter._tw + tw.write(config._parser.optparser.format_help()) + tw.line() + tw.line( + "[pytest] configuration options in the first " + "pytest.toml|pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:" + ) + tw.line() + + columns = tw.fullwidth # costly call + indent_len = 24 # based on argparse's max_help_position=24 + indent = " " * indent_len + for name in config._parser._inidict: + help, type, _default = config._parser._inidict[name] + if help is None: + raise TypeError(f"help argument cannot be None for {name}") + spec = f"{name} ({type}):" + tw.write(f" {spec}") + spec_len = len(spec) + if spec_len > (indent_len - 3): + # Display help starting at a new line. + tw.line() + helplines = textwrap.wrap( + help, + columns, + initial_indent=indent, + subsequent_indent=indent, + break_on_hyphens=False, + ) + + for line in helplines: + tw.line(line) + else: + # Display help starting after the spec, following lines indented. + tw.write(" " * (indent_len - spec_len - 2)) + wrapped = textwrap.wrap(help, columns - indent_len, break_on_hyphens=False) + + if wrapped: + tw.line(wrapped[0]) + for line in wrapped[1:]: + tw.line(indent + line) + + tw.line() + tw.line("Environment variables:") + vars = [ + ( + "CI", + "When set to a non-empty value, pytest knows it is running in a " + "CI process and does not truncate summary info", + ), + ("BUILD_NUMBER", "Equivalent to CI"), + ("PYTEST_ADDOPTS", "Extra command line options"), + ("PYTEST_PLUGINS", "Comma-separated plugins to load during startup"), + ("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "Set to disable plugin auto-loading"), + ("PYTEST_DEBUG", "Set to enable debug tracing of pytest's internals"), + ("PYTEST_DEBUG_TEMPROOT", "Override the system temporary directory"), + ("PYTEST_THEME", "The Pygments style to use for code output"), + ("PYTEST_THEME_MODE", "Set the PYTEST_THEME to be either 'dark' or 'light'"), + ] + for name, help in vars: + tw.line(f" {name:<24} {help}") + tw.line() + tw.line() + + tw.line("to see available markers type: pytest --markers") + tw.line("to see available fixtures type: pytest --fixtures") + tw.line( + "(shown according to specified file_or_dir or current dir " + "if not specified; fixtures with leading '_' are only shown " + "with the '-v' option" + ) + + for warningreport in reporter.stats.get("warnings", []): + tw.line("warning : " + warningreport.message, red=True) + + +def getpluginversioninfo(config: Config) -> list[str]: + lines = [] + plugininfo = config.pluginmanager.list_plugin_distinfo() + if plugininfo: + lines.append("registered third-party plugins:") + for plugin, dist in plugininfo: + loc = getattr(plugin, "__file__", repr(plugin)) + content = f"{dist.project_name}-{dist.version} at {loc}" + lines.append(" " + content) + return lines + + +def pytest_report_header(config: Config) -> list[str]: + lines = [] + if config.option.debug or config.option.traceconfig: + lines.append(f"using: pytest-{pytest.__version__}") + + verinfo = getpluginversioninfo(config) + if verinfo: + lines.extend(verinfo) + + if config.option.traceconfig: + lines.append("active plugins:") + items = config.pluginmanager.list_name_plugin() + for name, plugin in items: + if hasattr(plugin, "__file__"): + r = plugin.__file__ + else: + r = repr(plugin) + lines.append(f" {name:<20}: {r}") + return lines diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/hookspec.py b/Backend/venv/lib/python3.12/site-packages/_pytest/hookspec.py new file mode 100644 index 00000000..c5bcc36a --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/hookspec.py @@ -0,0 +1,1342 @@ +# mypy: allow-untyped-defs +# ruff: noqa: T100 +"""Hook specifications for pytest plugins which are invoked by pytest itself +and by builtin plugins.""" + +from __future__ import annotations + +from collections.abc import Mapping +from collections.abc import Sequence +from pathlib import Path +from typing import Any +from typing import TYPE_CHECKING + +from pluggy import HookspecMarker + +from .deprecated import HOOK_LEGACY_PATH_ARG + + +if TYPE_CHECKING: + import pdb + from typing import Literal + import warnings + + from _pytest._code.code import ExceptionInfo + from _pytest._code.code import ExceptionRepr + from _pytest.compat import LEGACY_PATH + from _pytest.config import _PluggyPlugin + from _pytest.config import Config + from _pytest.config import ExitCode + from _pytest.config import PytestPluginManager + from _pytest.config.argparsing import Parser + from _pytest.fixtures import FixtureDef + from _pytest.fixtures import SubRequest + from _pytest.main import Session + from _pytest.nodes import Collector + from _pytest.nodes import Item + from _pytest.outcomes import Exit + from _pytest.python import Class + from _pytest.python import Function + from _pytest.python import Metafunc + from _pytest.python import Module + from _pytest.reports import CollectReport + from _pytest.reports import TestReport + from _pytest.runner import CallInfo + from _pytest.terminal import TerminalReporter + from _pytest.terminal import TestShortLogReport + + +hookspec = HookspecMarker("pytest") + +# ------------------------------------------------------------------------- +# Initialization hooks called for every plugin +# ------------------------------------------------------------------------- + + +@hookspec(historic=True) +def pytest_addhooks(pluginmanager: PytestPluginManager) -> None: + """Called at plugin registration time to allow adding new hooks via a call to + :func:`pluginmanager.add_hookspecs(module_or_class, prefix) `. + + :param pluginmanager: The pytest plugin manager. + + .. note:: + This hook is incompatible with hook wrappers. + + Use in conftest plugins + ======================= + + If a conftest plugin implements this hook, it will be called immediately + when the conftest is registered. + """ + + +@hookspec(historic=True) +def pytest_plugin_registered( + plugin: _PluggyPlugin, + plugin_name: str, + manager: PytestPluginManager, +) -> None: + """A new pytest plugin got registered. + + :param plugin: The plugin module or instance. + :param plugin_name: The name by which the plugin is registered. + :param manager: The pytest plugin manager. + + .. note:: + This hook is incompatible with hook wrappers. + + Use in conftest plugins + ======================= + + If a conftest plugin implements this hook, it will be called immediately + when the conftest is registered, once for each plugin registered thus far + (including itself!), and for all plugins thereafter when they are + registered. + """ + + +@hookspec(historic=True) +def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None: + """Register argparse-style options and config-style config values, + called once at the beginning of a test run. + + :param parser: + To add command line options, call + :py:func:`parser.addoption(...) `. + To add config-file values call :py:func:`parser.addini(...) + `. + + :param pluginmanager: + The pytest plugin manager, which can be used to install :py:func:`~pytest.hookspec`'s + or :py:func:`~pytest.hookimpl`'s and allow one plugin to call another plugin's hooks + to change how command line options are added. + + Options can later be accessed through the + :py:class:`config ` object, respectively: + + - :py:func:`config.getoption(name) ` to + retrieve the value of a command line option. + + - :py:func:`config.getini(name) ` to retrieve + a value read from a configuration file. + + The config object is passed around on many internal objects via the ``.config`` + attribute or can be retrieved as the ``pytestconfig`` fixture. + + .. note:: + This hook is incompatible with hook wrappers. + + Use in conftest plugins + ======================= + + If a conftest plugin implements this hook, it will be called immediately + when the conftest is registered. + + This hook is only called for :ref:`initial conftests `. + """ + + +@hookspec(historic=True) +def pytest_configure(config: Config) -> None: + """Allow plugins and conftest files to perform initial configuration. + + .. note:: + This hook is incompatible with hook wrappers. + + :param config: The pytest config object. + + Use in conftest plugins + ======================= + + This hook is called for every :ref:`initial conftest ` file + after command line options have been parsed. After that, the hook is called + for other conftest files as they are registered. + """ + + +# ------------------------------------------------------------------------- +# Bootstrapping hooks called for plugins registered early enough: +# internal and 3rd party plugins. +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) +def pytest_cmdline_parse( + pluginmanager: PytestPluginManager, args: list[str] +) -> Config | None: + """Return an initialized :class:`~pytest.Config`, parsing the specified args. + + Stops at first non-None result, see :ref:`firstresult`. + + .. note:: + This hook is only called for plugin classes passed to the + ``plugins`` arg when using `pytest.main`_ to perform an in-process + test run. + + :param pluginmanager: The pytest plugin manager. + :param args: List of arguments passed on the command line. + :returns: A pytest config object. + + Use in conftest plugins + ======================= + + This hook is not called for conftest files. + """ + + +def pytest_load_initial_conftests( + early_config: Config, parser: Parser, args: list[str] +) -> None: + """Called to implement the loading of :ref:`initial conftest files + ` ahead of command line option parsing. + + :param early_config: The pytest config object. + :param args: Arguments passed on the command line. + :param parser: To add command line options. + + Use in conftest plugins + ======================= + + This hook is not called for conftest files. + """ + + +@hookspec(firstresult=True) +def pytest_cmdline_main(config: Config) -> ExitCode | int | None: + """Called for performing the main command line action. + + The default implementation will invoke the configure hooks and + :hook:`pytest_runtestloop`. + + Stops at first non-None result, see :ref:`firstresult`. + + :param config: The pytest config object. + :returns: The exit code. + + Use in conftest plugins + ======================= + + This hook is only called for :ref:`initial conftests `. + """ + + +# ------------------------------------------------------------------------- +# collection hooks +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) +def pytest_collection(session: Session) -> object | None: + """Perform the collection phase for the given session. + + Stops at first non-None result, see :ref:`firstresult`. + The return value is not used, but only stops further processing. + + The default collection phase is this (see individual hooks for full details): + + 1. Starting from ``session`` as the initial collector: + + 1. ``pytest_collectstart(collector)`` + 2. ``report = pytest_make_collect_report(collector)`` + 3. ``pytest_exception_interact(collector, call, report)`` if an interactive exception occurred + 4. For each collected node: + + 1. If an item, ``pytest_itemcollected(item)`` + 2. If a collector, recurse into it. + + 5. ``pytest_collectreport(report)`` + + 2. ``pytest_collection_modifyitems(session, config, items)`` + + 1. ``pytest_deselected(items)`` for any deselected items (may be called multiple times) + + 3. ``pytest_collection_finish(session)`` + 4. Set ``session.items`` to the list of collected items + 5. Set ``session.testscollected`` to the number of collected items + + You can implement this hook to only perform some action before collection, + for example the terminal plugin uses it to start displaying the collection + counter (and returns `None`). + + :param session: The pytest session object. + + Use in conftest plugins + ======================= + + This hook is only called for :ref:`initial conftests `. + """ + + +def pytest_collection_modifyitems( + session: Session, config: Config, items: list[Item] +) -> None: + """Called after collection has been performed. May filter or re-order + the items in-place. + + When items are deselected (filtered out from ``items``), + the hook :hook:`pytest_deselected` must be called explicitly + with the deselected items to properly notify other plugins, + e.g. with ``config.hook.pytest_deselected(items=deselected_items)``. + + :param session: The pytest session object. + :param config: The pytest config object. + :param items: List of item objects. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ + + +def pytest_collection_finish(session: Session) -> None: + """Called after collection has been performed and modified. + + :param session: The pytest session object. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ + + +@hookspec( + firstresult=True, + warn_on_impl_args={ + "path": HOOK_LEGACY_PATH_ARG.format( + pylib_path_arg="path", pathlib_path_arg="collection_path" + ), + }, +) +def pytest_ignore_collect( + collection_path: Path, path: LEGACY_PATH, config: Config +) -> bool | None: + """Return ``True`` to ignore this path for collection. + + Return ``None`` to let other plugins ignore the path for collection. + + Returning ``False`` will forcefully *not* ignore this path for collection, + without giving a chance for other plugins to ignore this path. + + This hook is consulted for all files and directories prior to calling + more specific hooks. + + Stops at first non-None result, see :ref:`firstresult`. + + :param collection_path: The path to analyze. + :type collection_path: pathlib.Path + :param path: The path to analyze (deprecated). + :param config: The pytest config object. + + .. versionchanged:: 7.0.0 + The ``collection_path`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``path`` parameter. The ``path`` parameter + has been deprecated. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collection path, only + conftest files in parent directories of the collection path are consulted + (if the path is a directory, its own conftest file is *not* consulted - a + directory cannot ignore itself!). + """ + + +@hookspec(firstresult=True) +def pytest_collect_directory(path: Path, parent: Collector) -> Collector | None: + """Create a :class:`~pytest.Collector` for the given directory, or None if + not relevant. + + .. versionadded:: 8.0 + + For best results, the returned collector should be a subclass of + :class:`~pytest.Directory`, but this is not required. + + The new node needs to have the specified ``parent`` as a parent. + + Stops at first non-None result, see :ref:`firstresult`. + + :param path: The path to analyze. + :type path: pathlib.Path + + See :ref:`custom directory collectors` for a simple example of use of this + hook. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collection path, only + conftest files in parent directories of the collection path are consulted + (if the path is a directory, its own conftest file is *not* consulted - a + directory cannot collect itself!). + """ + + +@hookspec( + warn_on_impl_args={ + "path": HOOK_LEGACY_PATH_ARG.format( + pylib_path_arg="path", pathlib_path_arg="file_path" + ), + }, +) +def pytest_collect_file( + file_path: Path, path: LEGACY_PATH, parent: Collector +) -> Collector | None: + """Create a :class:`~pytest.Collector` for the given path, or None if not relevant. + + For best results, the returned collector should be a subclass of + :class:`~pytest.File`, but this is not required. + + The new node needs to have the specified ``parent`` as a parent. + + :param file_path: The path to analyze. + :type file_path: pathlib.Path + :param path: The path to collect (deprecated). + + .. versionchanged:: 7.0.0 + The ``file_path`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``path`` parameter. The ``path`` parameter + has been deprecated. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given file path, only + conftest files in parent directories of the file path are consulted. + """ + + +# logging hooks for collection + + +def pytest_collectstart(collector: Collector) -> None: + """Collector starts collecting. + + :param collector: + The collector. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collector, only + conftest files in the collector's directory and its parent directories are + consulted. + """ + + +def pytest_itemcollected(item: Item) -> None: + """We just collected a test item. + + :param item: + The item. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +def pytest_collectreport(report: CollectReport) -> None: + """Collector finished collecting. + + :param report: + The collect report. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collector, only + conftest files in the collector's directory and its parent directories are + consulted. + """ + + +def pytest_deselected(items: Sequence[Item]) -> None: + """Called for deselected test items, e.g. by keyword. + + Note that this hook has two integration aspects for plugins: + + - it can be *implemented* to be notified of deselected items + - it must be *called* from :hook:`pytest_collection_modifyitems` + implementations when items are deselected (to properly notify other plugins). + + May be called multiple times. + + :param items: + The items. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. + """ + + +@hookspec(firstresult=True) +def pytest_make_collect_report(collector: Collector) -> CollectReport | None: + """Perform :func:`collector.collect() ` and return + a :class:`~pytest.CollectReport`. + + Stops at first non-None result, see :ref:`firstresult`. + + :param collector: + The collector. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collector, only + conftest files in the collector's directory and its parent directories are + consulted. + """ + + +# ------------------------------------------------------------------------- +# Python test function related hooks +# ------------------------------------------------------------------------- + + +@hookspec( + firstresult=True, + warn_on_impl_args={ + "path": HOOK_LEGACY_PATH_ARG.format( + pylib_path_arg="path", pathlib_path_arg="module_path" + ), + }, +) +def pytest_pycollect_makemodule( + module_path: Path, path: LEGACY_PATH, parent +) -> Module | None: + """Return a :class:`pytest.Module` collector or None for the given path. + + This hook will be called for each matching test module path. + The :hook:`pytest_collect_file` hook needs to be used if you want to + create test modules for files that do not match as a test module. + + Stops at first non-None result, see :ref:`firstresult`. + + :param module_path: The path of the module to collect. + :type module_path: pathlib.Path + :param path: The path of the module to collect (deprecated). + + .. versionchanged:: 7.0.0 + The ``module_path`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``path`` parameter. + + The ``path`` parameter has been deprecated in favor of ``fspath``. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given parent collector, + only conftest files in the collector's directory and its parent directories + are consulted. + """ + + +@hookspec(firstresult=True) +def pytest_pycollect_makeitem( + collector: Module | Class, name: str, obj: object +) -> None | Item | Collector | list[Item | Collector]: + """Return a custom item/collector for a Python object in a module, or None. + + Stops at first non-None result, see :ref:`firstresult`. + + :param collector: + The module/class collector. + :param name: + The name of the object in the module/class. + :param obj: + The object. + :returns: + The created items/collectors. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collector, only + conftest files in the collector's directory and its parent directories + are consulted. + """ + + +@hookspec(firstresult=True) +def pytest_pyfunc_call(pyfuncitem: Function) -> object | None: + """Call underlying test function. + + Stops at first non-None result, see :ref:`firstresult`. + + :param pyfuncitem: + The function item. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only + conftest files in the item's directory and its parent directories + are consulted. + """ + + +def pytest_generate_tests(metafunc: Metafunc) -> None: + """Generate (multiple) parametrized calls to a test function. + + :param metafunc: + The :class:`~pytest.Metafunc` helper for the test function. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given function definition, + only conftest files in the functions's directory and its parent directories + are consulted. + """ + + +@hookspec(firstresult=True) +def pytest_make_parametrize_id(config: Config, val: object, argname: str) -> str | None: + """Return a user-friendly string representation of the given ``val`` + that will be used by @pytest.mark.parametrize calls, or None if the hook + doesn't know about ``val``. + + The parameter name is available as ``argname``, if required. + + Stops at first non-None result, see :ref:`firstresult`. + + :param config: The pytest config object. + :param val: The parametrized value. + :param argname: The automatic parameter name produced by pytest. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. + """ + + +# ------------------------------------------------------------------------- +# runtest related hooks +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) +def pytest_runtestloop(session: Session) -> object | None: + """Perform the main runtest loop (after collection finished). + + The default hook implementation performs the runtest protocol for all items + collected in the session (``session.items``), unless the collection failed + or the ``collectonly`` pytest option is set. + + If at any point :py:func:`pytest.exit` is called, the loop is + terminated immediately. + + If at any point ``session.shouldfail`` or ``session.shouldstop`` are set, the + loop is terminated after the runtest protocol for the current item is finished. + + :param session: The pytest session object. + + Stops at first non-None result, see :ref:`firstresult`. + The return value is not used, but only stops further processing. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. + """ + + +@hookspec(firstresult=True) +def pytest_runtest_protocol(item: Item, nextitem: Item | None) -> object | None: + """Perform the runtest protocol for a single test item. + + The default runtest protocol is this (see individual hooks for full details): + + - ``pytest_runtest_logstart(nodeid, location)`` + + - Setup phase: + - ``call = pytest_runtest_setup(item)`` (wrapped in ``CallInfo(when="setup")``) + - ``report = pytest_runtest_makereport(item, call)`` + - ``pytest_runtest_logreport(report)`` + - ``pytest_exception_interact(call, report)`` if an interactive exception occurred + + - Call phase, if the setup passed and the ``setuponly`` pytest option is not set: + - ``call = pytest_runtest_call(item)`` (wrapped in ``CallInfo(when="call")``) + - ``report = pytest_runtest_makereport(item, call)`` + - ``pytest_runtest_logreport(report)`` + - ``pytest_exception_interact(call, report)`` if an interactive exception occurred + + - Teardown phase: + - ``call = pytest_runtest_teardown(item, nextitem)`` (wrapped in ``CallInfo(when="teardown")``) + - ``report = pytest_runtest_makereport(item, call)`` + - ``pytest_runtest_logreport(report)`` + - ``pytest_exception_interact(call, report)`` if an interactive exception occurred + + - ``pytest_runtest_logfinish(nodeid, location)`` + + :param item: Test item for which the runtest protocol is performed. + :param nextitem: The scheduled-to-be-next test item (or None if this is the end my friend). + + Stops at first non-None result, see :ref:`firstresult`. + The return value is not used, but only stops further processing. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. + """ + + +def pytest_runtest_logstart(nodeid: str, location: tuple[str, int | None, str]) -> None: + """Called at the start of running the runtest protocol for a single item. + + See :hook:`pytest_runtest_protocol` for a description of the runtest protocol. + + :param nodeid: Full node ID of the item. + :param location: A tuple of ``(filename, lineno, testname)`` + where ``filename`` is a file path relative to ``config.rootpath`` + and ``lineno`` is 0-based. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +def pytest_runtest_logfinish( + nodeid: str, location: tuple[str, int | None, str] +) -> None: + """Called at the end of running the runtest protocol for a single item. + + See :hook:`pytest_runtest_protocol` for a description of the runtest protocol. + + :param nodeid: Full node ID of the item. + :param location: A tuple of ``(filename, lineno, testname)`` + where ``filename`` is a file path relative to ``config.rootpath`` + and ``lineno`` is 0-based. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +def pytest_runtest_setup(item: Item) -> None: + """Called to perform the setup phase for a test item. + + The default implementation runs ``setup()`` on ``item`` and all of its + parents (which haven't been setup yet). This includes obtaining the + values of fixtures required by the item (which haven't been obtained + yet). + + :param item: + The item. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +def pytest_runtest_call(item: Item) -> None: + """Called to run the test for test item (the call phase). + + The default implementation calls ``item.runtest()``. + + :param item: + The item. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +def pytest_runtest_teardown(item: Item, nextitem: Item | None) -> None: + """Called to perform the teardown phase for a test item. + + The default implementation runs the finalizers and calls ``teardown()`` + on ``item`` and all of its parents (which need to be torn down). This + includes running the teardown phase of fixtures required by the item (if + they go out of scope). + + :param item: + The item. + :param nextitem: + The scheduled-to-be-next test item (None if no further test item is + scheduled). This argument is used to perform exact teardowns, i.e. + calling just enough finalizers so that nextitem only needs to call + setup functions. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +@hookspec(firstresult=True) +def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport | None: + """Called to create a :class:`~pytest.TestReport` for each of + the setup, call and teardown runtest phases of a test item. + + See :hook:`pytest_runtest_protocol` for a description of the runtest protocol. + + :param item: The item. + :param call: The :class:`~pytest.CallInfo` for the phase. + + Stops at first non-None result, see :ref:`firstresult`. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +def pytest_runtest_logreport(report: TestReport) -> None: + """Process the :class:`~pytest.TestReport` produced for each + of the setup, call and teardown runtest phases of an item. + + See :hook:`pytest_runtest_protocol` for a description of the runtest protocol. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +@hookspec(firstresult=True) +def pytest_report_to_serializable( + config: Config, + report: CollectReport | TestReport, +) -> dict[str, Any] | None: + """Serialize the given report object into a data structure suitable for + sending over the wire, e.g. converted to JSON. + + :param config: The pytest config object. + :param report: The report. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. The exact details may depend + on the plugin which calls the hook. + """ + + +@hookspec(firstresult=True) +def pytest_report_from_serializable( + config: Config, + data: dict[str, Any], +) -> CollectReport | TestReport | None: + """Restore a report object previously serialized with + :hook:`pytest_report_to_serializable`. + + :param config: The pytest config object. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. The exact details may depend + on the plugin which calls the hook. + """ + + +# ------------------------------------------------------------------------- +# Fixture related hooks +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) +def pytest_fixture_setup( + fixturedef: FixtureDef[Any], request: SubRequest +) -> object | None: + """Perform fixture setup execution. + + :param fixturedef: + The fixture definition object. + :param request: + The fixture request object. + :returns: + The return value of the call to the fixture function. + + Stops at first non-None result, see :ref:`firstresult`. + + .. note:: + If the fixture function returns None, other implementations of + this hook function will continue to be called, according to the + behavior of the :ref:`firstresult` option. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given fixture, only + conftest files in the fixture scope's directory and its parent directories + are consulted. + """ + + +def pytest_fixture_post_finalizer( + fixturedef: FixtureDef[Any], request: SubRequest +) -> None: + """Called after fixture teardown, but before the cache is cleared, so + the fixture result ``fixturedef.cached_result`` is still available (not + ``None``). + + :param fixturedef: + The fixture definition object. + :param request: + The fixture request object. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given fixture, only + conftest files in the fixture scope's directory and its parent directories + are consulted. + """ + + +# ------------------------------------------------------------------------- +# test session related hooks +# ------------------------------------------------------------------------- + + +def pytest_sessionstart(session: Session) -> None: + """Called after the ``Session`` object has been created and before performing collection + and entering the run test loop. + + :param session: The pytest session object. + + Use in conftest plugins + ======================= + + This hook is only called for :ref:`initial conftests `. + """ + + +def pytest_sessionfinish( + session: Session, + exitstatus: int | ExitCode, +) -> None: + """Called after whole test run finished, right before returning the exit status to the system. + + :param session: The pytest session object. + :param exitstatus: The status which pytest will return to the system. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. + """ + + +def pytest_unconfigure(config: Config) -> None: + """Called before test process is exited. + + :param config: The pytest config object. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. + """ + + +# ------------------------------------------------------------------------- +# hooks for customizing the assert methods +# ------------------------------------------------------------------------- + + +def pytest_assertrepr_compare( + config: Config, op: str, left: object, right: object +) -> list[str] | None: + """Return explanation for comparisons in failing assert expressions. + + Return None for no custom explanation, otherwise return a list + of strings. The strings will be joined by newlines but any newlines + *in* a string will be escaped. Note that all but the first line will + be indented slightly, the intention is for the first line to be a summary. + + :param config: The pytest config object. + :param op: The operator, e.g. `"=="`, `"!="`, `"not in"`. + :param left: The left operand. + :param right: The right operand. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +def pytest_assertion_pass(item: Item, lineno: int, orig: str, expl: str) -> None: + """Called whenever an assertion passes. + + .. versionadded:: 5.0 + + Use this hook to do some processing after a passing assertion. + The original assertion information is available in the `orig` string + and the pytest introspected assertion information is available in the + `expl` string. + + This hook must be explicitly enabled by the :confval:`enable_assertion_pass_hook` + configuration option: + + .. tab:: toml + + .. code-block:: toml + + [pytest] + enable_assertion_pass_hook = true + + .. tab:: ini + + .. code-block:: ini + + [pytest] + enable_assertion_pass_hook = true + + You need to **clean the .pyc** files in your project directory and interpreter libraries + when enabling this option, as assertions will require to be re-written. + + :param item: pytest item object of current test. + :param lineno: Line number of the assert statement. + :param orig: String with the original assertion. + :param expl: String with the assert explanation. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +# ------------------------------------------------------------------------- +# Hooks for influencing reporting (invoked from _pytest_terminal). +# ------------------------------------------------------------------------- + + +@hookspec( + warn_on_impl_args={ + "startdir": HOOK_LEGACY_PATH_ARG.format( + pylib_path_arg="startdir", pathlib_path_arg="start_path" + ), + }, +) +def pytest_report_header( # type:ignore[empty-body] + config: Config, start_path: Path, startdir: LEGACY_PATH +) -> str | list[str]: + """Return a string or list of strings to be displayed as header info for terminal reporting. + + :param config: The pytest config object. + :param start_path: The starting dir. + :type start_path: pathlib.Path + :param startdir: The starting dir (deprecated). + + .. note:: + + Lines returned by a plugin are displayed before those of plugins which + ran before it. + If you want to have your line(s) displayed first, use + :ref:`trylast=True `. + + .. versionchanged:: 7.0.0 + The ``start_path`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``startdir`` parameter. The ``startdir`` parameter + has been deprecated. + + Use in conftest plugins + ======================= + + This hook is only called for :ref:`initial conftests `. + """ + + +@hookspec( + warn_on_impl_args={ + "startdir": HOOK_LEGACY_PATH_ARG.format( + pylib_path_arg="startdir", pathlib_path_arg="start_path" + ), + }, +) +def pytest_report_collectionfinish( # type:ignore[empty-body] + config: Config, + start_path: Path, + startdir: LEGACY_PATH, + items: Sequence[Item], +) -> str | list[str]: + """Return a string or list of strings to be displayed after collection + has finished successfully. + + These strings will be displayed after the standard "collected X items" message. + + .. versionadded:: 3.2 + + :param config: The pytest config object. + :param start_path: The starting dir. + :type start_path: pathlib.Path + :param startdir: The starting dir (deprecated). + :param items: List of pytest items that are going to be executed; this list should not be modified. + + .. note:: + + Lines returned by a plugin are displayed before those of plugins which + ran before it. + If you want to have your line(s) displayed first, use + :ref:`trylast=True `. + + .. versionchanged:: 7.0.0 + The ``start_path`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``startdir`` parameter. The ``startdir`` parameter + has been deprecated. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ + + +@hookspec(firstresult=True) +def pytest_report_teststatus( # type:ignore[empty-body] + report: CollectReport | TestReport, config: Config +) -> TestShortLogReport | tuple[str, str, str | tuple[str, Mapping[str, bool]]]: + """Return result-category, shortletter and verbose word for status + reporting. + + The result-category is a category in which to count the result, for + example "passed", "skipped", "error" or the empty string. + + The shortletter is shown as testing progresses, for example ".", "s", + "E" or the empty string. + + The verbose word is shown as testing progresses in verbose mode, for + example "PASSED", "SKIPPED", "ERROR" or the empty string. + + pytest may style these implicitly according to the report outcome. + To provide explicit styling, return a tuple for the verbose word, + for example ``"rerun", "R", ("RERUN", {"yellow": True})``. + + :param report: The report object whose status is to be returned. + :param config: The pytest config object. + :returns: The test status. + + Stops at first non-None result, see :ref:`firstresult`. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ + + +def pytest_terminal_summary( + terminalreporter: TerminalReporter, + exitstatus: ExitCode, + config: Config, +) -> None: + """Add a section to terminal summary reporting. + + :param terminalreporter: The internal terminal reporter object. + :param exitstatus: The exit status that will be reported back to the OS. + :param config: The pytest config object. + + .. versionadded:: 4.2 + The ``config`` parameter. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ + + +@hookspec(historic=True) +def pytest_warning_recorded( + warning_message: warnings.WarningMessage, + when: Literal["config", "collect", "runtest"], + nodeid: str, + location: tuple[str, int, str] | None, +) -> None: + """Process a warning captured by the internal pytest warnings plugin. + + :param warning_message: + The captured warning. This is the same object produced by :class:`warnings.catch_warnings`, + and contains the same attributes as the parameters of :py:func:`warnings.showwarning`. + + :param when: + Indicates when the warning was captured. Possible values: + + * ``"config"``: during pytest configuration/initialization stage. + * ``"collect"``: during test collection. + * ``"runtest"``: during test execution. + + :param nodeid: + Full id of the item. Empty string for warnings that are not specific to + a particular node. + + :param location: + When available, holds information about the execution context of the captured + warning (filename, linenumber, function). ``function`` evaluates to + when the execution context is at the module level. + + .. versionadded:: 6.0 + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. If the warning is specific to a + particular node, only conftest files in parent directories of the node are + consulted. + """ + + +# ------------------------------------------------------------------------- +# Hooks for influencing skipping +# ------------------------------------------------------------------------- + + +def pytest_markeval_namespace( # type:ignore[empty-body] + config: Config, +) -> dict[str, Any]: + """Called when constructing the globals dictionary used for + evaluating string conditions in xfail/skipif markers. + + This is useful when the condition for a marker requires + objects that are expensive or impossible to obtain during + collection time, which is required by normal boolean + conditions. + + .. versionadded:: 6.2 + + :param config: The pytest config object. + :returns: A dictionary of additional globals to add. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in parent directories of the item are consulted. + """ + + +# ------------------------------------------------------------------------- +# error handling and internal debugging hooks +# ------------------------------------------------------------------------- + + +def pytest_internalerror( + excrepr: ExceptionRepr, + excinfo: ExceptionInfo[BaseException], +) -> bool | None: + """Called for internal errors. + + Return True to suppress the fallback handling of printing an + INTERNALERROR message directly to sys.stderr. + + :param excrepr: The exception repr object. + :param excinfo: The exception info. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ + + +def pytest_keyboard_interrupt( + excinfo: ExceptionInfo[KeyboardInterrupt | Exit], +) -> None: + """Called for keyboard interrupt. + + :param excinfo: The exception info. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ + + +def pytest_exception_interact( + node: Item | Collector, + call: CallInfo[Any], + report: CollectReport | TestReport, +) -> None: + """Called when an exception was raised which can potentially be + interactively handled. + + May be called during collection (see :hook:`pytest_make_collect_report`), + in which case ``report`` is a :class:`~pytest.CollectReport`. + + May be called during runtest of an item (see :hook:`pytest_runtest_protocol`), + in which case ``report`` is a :class:`~pytest.TestReport`. + + This hook is not called if the exception that was raised is an internal + exception like ``skip.Exception``. + + :param node: + The item or collector. + :param call: + The call information. Contains the exception. + :param report: + The collection or test report. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given node, only conftest + files in parent directories of the node are consulted. + """ + + +def pytest_enter_pdb(config: Config, pdb: pdb.Pdb) -> None: + """Called upon pdb.set_trace(). + + Can be used by plugins to take special action just before the python + debugger enters interactive mode. + + :param config: The pytest config object. + :param pdb: The Pdb instance. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ + + +def pytest_leave_pdb(config: Config, pdb: pdb.Pdb) -> None: + """Called when leaving pdb (e.g. with continue after pdb.set_trace()). + + Can be used by plugins to take special action just after the python + debugger leaves interactive mode. + + :param config: The pytest config object. + :param pdb: The Pdb instance. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/junitxml.py b/Backend/venv/lib/python3.12/site-packages/_pytest/junitxml.py new file mode 100644 index 00000000..ae8d2b94 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/junitxml.py @@ -0,0 +1,695 @@ +# mypy: allow-untyped-defs +"""Report test results in JUnit-XML format, for use with Jenkins and build +integration servers. + +Based on initial code from Ross Lawley. + +Output conforms to +https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd +""" + +from __future__ import annotations + +from collections.abc import Callable +import functools +import os +import platform +import re +import xml.etree.ElementTree as ET + +from _pytest import nodes +from _pytest import timing +from _pytest._code.code import ExceptionRepr +from _pytest._code.code import ReprFileLocation +from _pytest.config import Config +from _pytest.config import filename_arg +from _pytest.config.argparsing import Parser +from _pytest.fixtures import FixtureRequest +from _pytest.reports import TestReport +from _pytest.stash import StashKey +from _pytest.terminal import TerminalReporter +import pytest + + +xml_key = StashKey["LogXML"]() + + +def bin_xml_escape(arg: object) -> str: + r"""Visually escape invalid XML characters. + + For example, transforms + 'hello\aworld\b' + into + 'hello#x07world#x08' + Note that the #xABs are *not* XML escapes - missing the ampersand «. + The idea is to escape visually for the user rather than for XML itself. + """ + + def repl(matchobj: re.Match[str]) -> str: + i = ord(matchobj.group()) + if i <= 0xFF: + return f"#x{i:02X}" + else: + return f"#x{i:04X}" + + # The spec range of valid chars is: + # Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] + # For an unknown(?) reason, we disallow #x7F (DEL) as well. + illegal_xml_re = ( + "[^\u0009\u000a\u000d\u0020-\u007e\u0080-\ud7ff\ue000-\ufffd\u10000-\u10ffff]" + ) + return re.sub(illegal_xml_re, repl, str(arg)) + + +def merge_family(left, right) -> None: + result = {} + for kl, vl in left.items(): + for kr, vr in right.items(): + if not isinstance(vl, list): + raise TypeError(type(vl)) + result[kl] = vl + vr + left.update(result) + + +families = { # pylint: disable=dict-init-mutate + "_base": {"testcase": ["classname", "name"]}, + "_base_legacy": {"testcase": ["file", "line", "url"]}, +} +# xUnit 1.x inherits legacy attributes. +families["xunit1"] = families["_base"].copy() +merge_family(families["xunit1"], families["_base_legacy"]) + +# xUnit 2.x uses strict base attributes. +families["xunit2"] = families["_base"] + + +class _NodeReporter: + def __init__(self, nodeid: str | TestReport, xml: LogXML) -> None: + self.id = nodeid + self.xml = xml + self.add_stats = self.xml.add_stats + self.family = self.xml.family + self.duration = 0.0 + self.properties: list[tuple[str, str]] = [] + self.nodes: list[ET.Element] = [] + self.attrs: dict[str, str] = {} + + def append(self, node: ET.Element) -> None: + self.xml.add_stats(node.tag) + self.nodes.append(node) + + def add_property(self, name: str, value: object) -> None: + self.properties.append((str(name), bin_xml_escape(value))) + + def add_attribute(self, name: str, value: object) -> None: + self.attrs[str(name)] = bin_xml_escape(value) + + def make_properties_node(self) -> ET.Element | None: + """Return a Junit node containing custom properties, if any.""" + if self.properties: + properties = ET.Element("properties") + for name, value in self.properties: + properties.append(ET.Element("property", name=name, value=value)) + return properties + return None + + def record_testreport(self, testreport: TestReport) -> None: + names = mangle_test_address(testreport.nodeid) + existing_attrs = self.attrs + classnames = names[:-1] + if self.xml.prefix: + classnames.insert(0, self.xml.prefix) + attrs: dict[str, str] = { + "classname": ".".join(classnames), + "name": bin_xml_escape(names[-1]), + "file": testreport.location[0], + } + if testreport.location[1] is not None: + attrs["line"] = str(testreport.location[1]) + if hasattr(testreport, "url"): + attrs["url"] = testreport.url + self.attrs = attrs + self.attrs.update(existing_attrs) # Restore any user-defined attributes. + + # Preserve legacy testcase behavior. + if self.family == "xunit1": + return + + # Filter out attributes not permitted by this test family. + # Including custom attributes because they are not valid here. + temp_attrs = {} + for key in self.attrs: + if key in families[self.family]["testcase"]: + temp_attrs[key] = self.attrs[key] + self.attrs = temp_attrs + + def to_xml(self) -> ET.Element: + testcase = ET.Element("testcase", self.attrs, time=f"{self.duration:.3f}") + properties = self.make_properties_node() + if properties is not None: + testcase.append(properties) + testcase.extend(self.nodes) + return testcase + + def _add_simple(self, tag: str, message: str, data: str | None = None) -> None: + node = ET.Element(tag, message=message) + node.text = bin_xml_escape(data) + self.append(node) + + def write_captured_output(self, report: TestReport) -> None: + if not self.xml.log_passing_tests and report.passed: + return + + content_out = report.capstdout + content_log = report.caplog + content_err = report.capstderr + if self.xml.logging == "no": + return + content_all = "" + if self.xml.logging in ["log", "all"]: + content_all = self._prepare_content(content_log, " Captured Log ") + if self.xml.logging in ["system-out", "out-err", "all"]: + content_all += self._prepare_content(content_out, " Captured Out ") + self._write_content(report, content_all, "system-out") + content_all = "" + if self.xml.logging in ["system-err", "out-err", "all"]: + content_all += self._prepare_content(content_err, " Captured Err ") + self._write_content(report, content_all, "system-err") + content_all = "" + if content_all: + self._write_content(report, content_all, "system-out") + + def _prepare_content(self, content: str, header: str) -> str: + return "\n".join([header.center(80, "-"), content, ""]) + + def _write_content(self, report: TestReport, content: str, jheader: str) -> None: + tag = ET.Element(jheader) + tag.text = bin_xml_escape(content) + self.append(tag) + + def append_pass(self, report: TestReport) -> None: + self.add_stats("passed") + + def append_failure(self, report: TestReport) -> None: + # msg = str(report.longrepr.reprtraceback.extraline) + if hasattr(report, "wasxfail"): + self._add_simple("skipped", "xfail-marked test passes unexpectedly") + else: + assert report.longrepr is not None + reprcrash: ReprFileLocation | None = getattr( + report.longrepr, "reprcrash", None + ) + if reprcrash is not None: + message = reprcrash.message + else: + message = str(report.longrepr) + message = bin_xml_escape(message) + self._add_simple("failure", message, str(report.longrepr)) + + def append_collect_error(self, report: TestReport) -> None: + # msg = str(report.longrepr.reprtraceback.extraline) + assert report.longrepr is not None + self._add_simple("error", "collection failure", str(report.longrepr)) + + def append_collect_skipped(self, report: TestReport) -> None: + self._add_simple("skipped", "collection skipped", str(report.longrepr)) + + def append_error(self, report: TestReport) -> None: + assert report.longrepr is not None + reprcrash: ReprFileLocation | None = getattr(report.longrepr, "reprcrash", None) + if reprcrash is not None: + reason = reprcrash.message + else: + reason = str(report.longrepr) + + if report.when == "teardown": + msg = f'failed on teardown with "{reason}"' + else: + msg = f'failed on setup with "{reason}"' + self._add_simple("error", bin_xml_escape(msg), str(report.longrepr)) + + def append_skipped(self, report: TestReport) -> None: + if hasattr(report, "wasxfail"): + xfailreason = report.wasxfail + if xfailreason.startswith("reason: "): + xfailreason = xfailreason[8:] + xfailreason = bin_xml_escape(xfailreason) + skipped = ET.Element("skipped", type="pytest.xfail", message=xfailreason) + self.append(skipped) + else: + assert isinstance(report.longrepr, tuple) + filename, lineno, skipreason = report.longrepr + if skipreason.startswith("Skipped: "): + skipreason = skipreason[9:] + details = f"{filename}:{lineno}: {skipreason}" + + skipped = ET.Element( + "skipped", type="pytest.skip", message=bin_xml_escape(skipreason) + ) + skipped.text = bin_xml_escape(details) + self.append(skipped) + self.write_captured_output(report) + + def finalize(self) -> None: + data = self.to_xml() + self.__dict__.clear() + # Type ignored because mypy doesn't like overriding a method. + # Also the return value doesn't match... + self.to_xml = lambda: data # type: ignore[method-assign] + + +def _warn_incompatibility_with_xunit2( + request: FixtureRequest, fixture_name: str +) -> None: + """Emit a PytestWarning about the given fixture being incompatible with newer xunit revisions.""" + from _pytest.warning_types import PytestWarning + + xml = request.config.stash.get(xml_key, None) + if xml is not None and xml.family not in ("xunit1", "legacy"): + request.node.warn( + PytestWarning( + f"{fixture_name} is incompatible with junit_family '{xml.family}' (use 'legacy' or 'xunit1')" + ) + ) + + +@pytest.fixture +def record_property(request: FixtureRequest) -> Callable[[str, object], None]: + """Add extra properties to the calling test. + + User properties become part of the test report and are available to the + configured reporters, like JUnit XML. + + The fixture is callable with ``name, value``. The value is automatically + XML-encoded. + + Example:: + + def test_function(record_property): + record_property("example_key", 1) + """ + _warn_incompatibility_with_xunit2(request, "record_property") + + def append_property(name: str, value: object) -> None: + request.node.user_properties.append((name, value)) + + return append_property + + +@pytest.fixture +def record_xml_attribute(request: FixtureRequest) -> Callable[[str, object], None]: + """Add extra xml attributes to the tag for the calling test. + + The fixture is callable with ``name, value``. The value is + automatically XML-encoded. + """ + from _pytest.warning_types import PytestExperimentalApiWarning + + request.node.warn( + PytestExperimentalApiWarning("record_xml_attribute is an experimental feature") + ) + + _warn_incompatibility_with_xunit2(request, "record_xml_attribute") + + # Declare noop + def add_attr_noop(name: str, value: object) -> None: + pass + + attr_func = add_attr_noop + + xml = request.config.stash.get(xml_key, None) + if xml is not None: + node_reporter = xml.node_reporter(request.node.nodeid) + attr_func = node_reporter.add_attribute + + return attr_func + + +def _check_record_param_type(param: str, v: str) -> None: + """Used by record_testsuite_property to check that the given parameter name is of the proper + type.""" + __tracebackhide__ = True + if not isinstance(v, str): + msg = "{param} parameter needs to be a string, but {g} given" # type: ignore[unreachable] + raise TypeError(msg.format(param=param, g=type(v).__name__)) + + +@pytest.fixture(scope="session") +def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object], None]: + """Record a new ```` tag as child of the root ````. + + This is suitable to writing global information regarding the entire test + suite, and is compatible with ``xunit2`` JUnit family. + + This is a ``session``-scoped fixture which is called with ``(name, value)``. Example: + + .. code-block:: python + + def test_foo(record_testsuite_property): + record_testsuite_property("ARCH", "PPC") + record_testsuite_property("STORAGE_TYPE", "CEPH") + + :param name: + The property name. + :param value: + The property value. Will be converted to a string. + + .. warning:: + + Currently this fixture **does not work** with the + `pytest-xdist `__ plugin. See + :issue:`7767` for details. + """ + __tracebackhide__ = True + + def record_func(name: str, value: object) -> None: + """No-op function in case --junit-xml was not passed in the command-line.""" + __tracebackhide__ = True + _check_record_param_type("name", name) + + xml = request.config.stash.get(xml_key, None) + if xml is not None: + record_func = xml.add_global_property + return record_func + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("terminal reporting") + group.addoption( + "--junitxml", + "--junit-xml", + action="store", + dest="xmlpath", + metavar="path", + type=functools.partial(filename_arg, optname="--junitxml"), + default=None, + help="Create junit-xml style report file at given path", + ) + group.addoption( + "--junitprefix", + "--junit-prefix", + action="store", + metavar="str", + default=None, + help="Prepend prefix to classnames in junit-xml output", + ) + parser.addini( + "junit_suite_name", "Test suite name for JUnit report", default="pytest" + ) + parser.addini( + "junit_logging", + "Write captured log messages to JUnit report: " + "one of no|log|system-out|system-err|out-err|all", + default="no", + ) + parser.addini( + "junit_log_passing_tests", + "Capture log information for passing tests to JUnit report: ", + type="bool", + default=True, + ) + parser.addini( + "junit_duration_report", + "Duration time to report: one of total|call", + default="total", + ) # choices=['total', 'call']) + parser.addini( + "junit_family", + "Emit XML for schema: one of legacy|xunit1|xunit2", + default="xunit2", + ) + + +def pytest_configure(config: Config) -> None: + xmlpath = config.option.xmlpath + # Prevent opening xmllog on worker nodes (xdist). + if xmlpath and not hasattr(config, "workerinput"): + junit_family = config.getini("junit_family") + config.stash[xml_key] = LogXML( + xmlpath, + config.option.junitprefix, + config.getini("junit_suite_name"), + config.getini("junit_logging"), + config.getini("junit_duration_report"), + junit_family, + config.getini("junit_log_passing_tests"), + ) + config.pluginmanager.register(config.stash[xml_key]) + + +def pytest_unconfigure(config: Config) -> None: + xml = config.stash.get(xml_key, None) + if xml: + del config.stash[xml_key] + config.pluginmanager.unregister(xml) + + +def mangle_test_address(address: str) -> list[str]: + path, possible_open_bracket, params = address.partition("[") + names = path.split("::") + # Convert file path to dotted path. + names[0] = names[0].replace(nodes.SEP, ".") + names[0] = re.sub(r"\.py$", "", names[0]) + # Put any params back. + names[-1] += possible_open_bracket + params + return names + + +class LogXML: + def __init__( + self, + logfile, + prefix: str | None, + suite_name: str = "pytest", + logging: str = "no", + report_duration: str = "total", + family="xunit1", + log_passing_tests: bool = True, + ) -> None: + logfile = os.path.expanduser(os.path.expandvars(logfile)) + self.logfile = os.path.normpath(os.path.abspath(logfile)) + self.prefix = prefix + self.suite_name = suite_name + self.logging = logging + self.log_passing_tests = log_passing_tests + self.report_duration = report_duration + self.family = family + self.stats: dict[str, int] = dict.fromkeys( + ["error", "passed", "failure", "skipped"], 0 + ) + self.node_reporters: dict[tuple[str | TestReport, object], _NodeReporter] = {} + self.node_reporters_ordered: list[_NodeReporter] = [] + self.global_properties: list[tuple[str, str]] = [] + + # List of reports that failed on call but teardown is pending. + self.open_reports: list[TestReport] = [] + self.cnt_double_fail_tests = 0 + + # Replaces convenience family with real family. + if self.family == "legacy": + self.family = "xunit1" + + def finalize(self, report: TestReport) -> None: + nodeid = getattr(report, "nodeid", report) + # Local hack to handle xdist report order. + workernode = getattr(report, "node", None) + reporter = self.node_reporters.pop((nodeid, workernode)) + + for propname, propvalue in report.user_properties: + reporter.add_property(propname, str(propvalue)) + + if reporter is not None: + reporter.finalize() + + def node_reporter(self, report: TestReport | str) -> _NodeReporter: + nodeid: str | TestReport = getattr(report, "nodeid", report) + # Local hack to handle xdist report order. + workernode = getattr(report, "node", None) + + key = nodeid, workernode + + if key in self.node_reporters: + # TODO: breaks for --dist=each + return self.node_reporters[key] + + reporter = _NodeReporter(nodeid, self) + + self.node_reporters[key] = reporter + self.node_reporters_ordered.append(reporter) + + return reporter + + def add_stats(self, key: str) -> None: + if key in self.stats: + self.stats[key] += 1 + + def _opentestcase(self, report: TestReport) -> _NodeReporter: + reporter = self.node_reporter(report) + reporter.record_testreport(report) + return reporter + + def pytest_runtest_logreport(self, report: TestReport) -> None: + """Handle a setup/call/teardown report, generating the appropriate + XML tags as necessary. + + Note: due to plugins like xdist, this hook may be called in interlaced + order with reports from other nodes. For example: + + Usual call order: + -> setup node1 + -> call node1 + -> teardown node1 + -> setup node2 + -> call node2 + -> teardown node2 + + Possible call order in xdist: + -> setup node1 + -> call node1 + -> setup node2 + -> call node2 + -> teardown node2 + -> teardown node1 + """ + close_report = None + if report.passed: + if report.when == "call": # ignore setup/teardown + reporter = self._opentestcase(report) + reporter.append_pass(report) + elif report.failed: + if report.when == "teardown": + # The following vars are needed when xdist plugin is used. + report_wid = getattr(report, "worker_id", None) + report_ii = getattr(report, "item_index", None) + close_report = next( + ( + rep + for rep in self.open_reports + if ( + rep.nodeid == report.nodeid + and getattr(rep, "item_index", None) == report_ii + and getattr(rep, "worker_id", None) == report_wid + ) + ), + None, + ) + if close_report: + # We need to open new testcase in case we have failure in + # call and error in teardown in order to follow junit + # schema. + self.finalize(close_report) + self.cnt_double_fail_tests += 1 + reporter = self._opentestcase(report) + if report.when == "call": + reporter.append_failure(report) + self.open_reports.append(report) + if not self.log_passing_tests: + reporter.write_captured_output(report) + else: + reporter.append_error(report) + elif report.skipped: + reporter = self._opentestcase(report) + reporter.append_skipped(report) + self.update_testcase_duration(report) + if report.when == "teardown": + reporter = self._opentestcase(report) + reporter.write_captured_output(report) + + self.finalize(report) + report_wid = getattr(report, "worker_id", None) + report_ii = getattr(report, "item_index", None) + close_report = next( + ( + rep + for rep in self.open_reports + if ( + rep.nodeid == report.nodeid + and getattr(rep, "item_index", None) == report_ii + and getattr(rep, "worker_id", None) == report_wid + ) + ), + None, + ) + if close_report: + self.open_reports.remove(close_report) + + def update_testcase_duration(self, report: TestReport) -> None: + """Accumulate total duration for nodeid from given report and update + the Junit.testcase with the new total if already created.""" + if self.report_duration in {"total", report.when}: + reporter = self.node_reporter(report) + reporter.duration += getattr(report, "duration", 0.0) + + def pytest_collectreport(self, report: TestReport) -> None: + if not report.passed: + reporter = self._opentestcase(report) + if report.failed: + reporter.append_collect_error(report) + else: + reporter.append_collect_skipped(report) + + def pytest_internalerror(self, excrepr: ExceptionRepr) -> None: + reporter = self.node_reporter("internal") + reporter.attrs.update(classname="pytest", name="internal") + reporter._add_simple("error", "internal error", str(excrepr)) + + def pytest_sessionstart(self) -> None: + self.suite_start = timing.Instant() + + def pytest_sessionfinish(self) -> None: + dirname = os.path.dirname(os.path.abspath(self.logfile)) + # exist_ok avoids filesystem race conditions between checking path existence and requesting creation + os.makedirs(dirname, exist_ok=True) + + with open(self.logfile, "w", encoding="utf-8") as logfile: + duration = self.suite_start.elapsed() + + numtests = ( + self.stats["passed"] + + self.stats["failure"] + + self.stats["skipped"] + + self.stats["error"] + - self.cnt_double_fail_tests + ) + logfile.write('') + + suite_node = ET.Element( + "testsuite", + name=self.suite_name, + errors=str(self.stats["error"]), + failures=str(self.stats["failure"]), + skipped=str(self.stats["skipped"]), + tests=str(numtests), + time=f"{duration.seconds:.3f}", + timestamp=self.suite_start.as_utc().astimezone().isoformat(), + hostname=platform.node(), + ) + global_properties = self._get_global_properties_node() + if global_properties is not None: + suite_node.append(global_properties) + for node_reporter in self.node_reporters_ordered: + suite_node.append(node_reporter.to_xml()) + testsuites = ET.Element("testsuites") + testsuites.set("name", "pytest tests") + testsuites.append(suite_node) + logfile.write(ET.tostring(testsuites, encoding="unicode")) + + def pytest_terminal_summary( + self, terminalreporter: TerminalReporter, config: pytest.Config + ) -> None: + if config.get_verbosity() >= 0: + terminalreporter.write_sep("-", f"generated xml file: {self.logfile}") + + def add_global_property(self, name: str, value: object) -> None: + __tracebackhide__ = True + _check_record_param_type("name", name) + self.global_properties.append((name, bin_xml_escape(value))) + + def _get_global_properties_node(self) -> ET.Element | None: + """Return a Junit node containing custom properties, if any.""" + if self.global_properties: + properties = ET.Element("properties") + for name, value in self.global_properties: + properties.append(ET.Element("property", name=name, value=value)) + return properties + return None diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/legacypath.py b/Backend/venv/lib/python3.12/site-packages/_pytest/legacypath.py new file mode 100644 index 00000000..59e8ef6e --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/legacypath.py @@ -0,0 +1,468 @@ +# mypy: allow-untyped-defs +"""Add backward compatibility support for the legacy py path type.""" + +from __future__ import annotations + +import dataclasses +from pathlib import Path +import shlex +import subprocess +from typing import Final +from typing import final +from typing import TYPE_CHECKING + +from iniconfig import SectionWrapper + +from _pytest.cacheprovider import Cache +from _pytest.compat import LEGACY_PATH +from _pytest.compat import legacy_path +from _pytest.config import Config +from _pytest.config import hookimpl +from _pytest.config import PytestPluginManager +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import fixture +from _pytest.fixtures import FixtureRequest +from _pytest.main import Session +from _pytest.monkeypatch import MonkeyPatch +from _pytest.nodes import Collector +from _pytest.nodes import Item +from _pytest.nodes import Node +from _pytest.pytester import HookRecorder +from _pytest.pytester import Pytester +from _pytest.pytester import RunResult +from _pytest.terminal import TerminalReporter +from _pytest.tmpdir import TempPathFactory + + +if TYPE_CHECKING: + import pexpect + + +@final +class Testdir: + """ + Similar to :class:`Pytester`, but this class works with legacy legacy_path objects instead. + + All methods just forward to an internal :class:`Pytester` instance, converting results + to `legacy_path` objects as necessary. + """ + + __test__ = False + + CLOSE_STDIN: Final = Pytester.CLOSE_STDIN + TimeoutExpired: Final = Pytester.TimeoutExpired + + def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None: + check_ispytest(_ispytest) + self._pytester = pytester + + @property + def tmpdir(self) -> LEGACY_PATH: + """Temporary directory where tests are executed.""" + return legacy_path(self._pytester.path) + + @property + def test_tmproot(self) -> LEGACY_PATH: + return legacy_path(self._pytester._test_tmproot) + + @property + def request(self): + return self._pytester._request + + @property + def plugins(self): + return self._pytester.plugins + + @plugins.setter + def plugins(self, plugins): + self._pytester.plugins = plugins + + @property + def monkeypatch(self) -> MonkeyPatch: + return self._pytester._monkeypatch + + def make_hook_recorder(self, pluginmanager) -> HookRecorder: + """See :meth:`Pytester.make_hook_recorder`.""" + return self._pytester.make_hook_recorder(pluginmanager) + + def chdir(self) -> None: + """See :meth:`Pytester.chdir`.""" + return self._pytester.chdir() + + def finalize(self) -> None: + return self._pytester._finalize() + + def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH: + """See :meth:`Pytester.makefile`.""" + if ext and not ext.startswith("."): + # pytester.makefile is going to throw a ValueError in a way that + # testdir.makefile did not, because + # pathlib.Path is stricter suffixes than py.path + # This ext arguments is likely user error, but since testdir has + # allowed this, we will prepend "." as a workaround to avoid breaking + # testdir usage that worked before + ext = "." + ext + return legacy_path(self._pytester.makefile(ext, *args, **kwargs)) + + def makeconftest(self, source) -> LEGACY_PATH: + """See :meth:`Pytester.makeconftest`.""" + return legacy_path(self._pytester.makeconftest(source)) + + def makeini(self, source) -> LEGACY_PATH: + """See :meth:`Pytester.makeini`.""" + return legacy_path(self._pytester.makeini(source)) + + def getinicfg(self, source: str) -> SectionWrapper: + """See :meth:`Pytester.getinicfg`.""" + return self._pytester.getinicfg(source) + + def makepyprojecttoml(self, source) -> LEGACY_PATH: + """See :meth:`Pytester.makepyprojecttoml`.""" + return legacy_path(self._pytester.makepyprojecttoml(source)) + + def makepyfile(self, *args, **kwargs) -> LEGACY_PATH: + """See :meth:`Pytester.makepyfile`.""" + return legacy_path(self._pytester.makepyfile(*args, **kwargs)) + + def maketxtfile(self, *args, **kwargs) -> LEGACY_PATH: + """See :meth:`Pytester.maketxtfile`.""" + return legacy_path(self._pytester.maketxtfile(*args, **kwargs)) + + def syspathinsert(self, path=None) -> None: + """See :meth:`Pytester.syspathinsert`.""" + return self._pytester.syspathinsert(path) + + def mkdir(self, name) -> LEGACY_PATH: + """See :meth:`Pytester.mkdir`.""" + return legacy_path(self._pytester.mkdir(name)) + + def mkpydir(self, name) -> LEGACY_PATH: + """See :meth:`Pytester.mkpydir`.""" + return legacy_path(self._pytester.mkpydir(name)) + + def copy_example(self, name=None) -> LEGACY_PATH: + """See :meth:`Pytester.copy_example`.""" + return legacy_path(self._pytester.copy_example(name)) + + def getnode(self, config: Config, arg) -> Item | Collector | None: + """See :meth:`Pytester.getnode`.""" + return self._pytester.getnode(config, arg) + + def getpathnode(self, path): + """See :meth:`Pytester.getpathnode`.""" + return self._pytester.getpathnode(path) + + def genitems(self, colitems: list[Item | Collector]) -> list[Item]: + """See :meth:`Pytester.genitems`.""" + return self._pytester.genitems(colitems) + + def runitem(self, source): + """See :meth:`Pytester.runitem`.""" + return self._pytester.runitem(source) + + def inline_runsource(self, source, *cmdlineargs): + """See :meth:`Pytester.inline_runsource`.""" + return self._pytester.inline_runsource(source, *cmdlineargs) + + def inline_genitems(self, *args): + """See :meth:`Pytester.inline_genitems`.""" + return self._pytester.inline_genitems(*args) + + def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False): + """See :meth:`Pytester.inline_run`.""" + return self._pytester.inline_run( + *args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc + ) + + def runpytest_inprocess(self, *args, **kwargs) -> RunResult: + """See :meth:`Pytester.runpytest_inprocess`.""" + return self._pytester.runpytest_inprocess(*args, **kwargs) + + def runpytest(self, *args, **kwargs) -> RunResult: + """See :meth:`Pytester.runpytest`.""" + return self._pytester.runpytest(*args, **kwargs) + + def parseconfig(self, *args) -> Config: + """See :meth:`Pytester.parseconfig`.""" + return self._pytester.parseconfig(*args) + + def parseconfigure(self, *args) -> Config: + """See :meth:`Pytester.parseconfigure`.""" + return self._pytester.parseconfigure(*args) + + def getitem(self, source, funcname="test_func"): + """See :meth:`Pytester.getitem`.""" + return self._pytester.getitem(source, funcname) + + def getitems(self, source): + """See :meth:`Pytester.getitems`.""" + return self._pytester.getitems(source) + + def getmodulecol(self, source, configargs=(), withinit=False): + """See :meth:`Pytester.getmodulecol`.""" + return self._pytester.getmodulecol( + source, configargs=configargs, withinit=withinit + ) + + def collect_by_name(self, modcol: Collector, name: str) -> Item | Collector | None: + """See :meth:`Pytester.collect_by_name`.""" + return self._pytester.collect_by_name(modcol, name) + + def popen( + self, + cmdargs, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=CLOSE_STDIN, + **kw, + ): + """See :meth:`Pytester.popen`.""" + return self._pytester.popen(cmdargs, stdout, stderr, stdin, **kw) + + def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult: + """See :meth:`Pytester.run`.""" + return self._pytester.run(*cmdargs, timeout=timeout, stdin=stdin) + + def runpython(self, script) -> RunResult: + """See :meth:`Pytester.runpython`.""" + return self._pytester.runpython(script) + + def runpython_c(self, command): + """See :meth:`Pytester.runpython_c`.""" + return self._pytester.runpython_c(command) + + def runpytest_subprocess(self, *args, timeout=None) -> RunResult: + """See :meth:`Pytester.runpytest_subprocess`.""" + return self._pytester.runpytest_subprocess(*args, timeout=timeout) + + def spawn_pytest(self, string: str, expect_timeout: float = 10.0) -> pexpect.spawn: + """See :meth:`Pytester.spawn_pytest`.""" + return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout) + + def spawn(self, cmd: str, expect_timeout: float = 10.0) -> pexpect.spawn: + """See :meth:`Pytester.spawn`.""" + return self._pytester.spawn(cmd, expect_timeout=expect_timeout) + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return str(self.tmpdir) + + +class LegacyTestdirPlugin: + @staticmethod + @fixture + def testdir(pytester: Pytester) -> Testdir: + """ + Identical to :fixture:`pytester`, and provides an instance whose methods return + legacy ``LEGACY_PATH`` objects instead when applicable. + + New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. + """ + return Testdir(pytester, _ispytest=True) + + +@final +@dataclasses.dataclass +class TempdirFactory: + """Backward compatibility wrapper that implements ``py.path.local`` + for :class:`TempPathFactory`. + + .. note:: + These days, it is preferred to use ``tmp_path_factory``. + + :ref:`About the tmpdir and tmpdir_factory fixtures`. + + """ + + _tmppath_factory: TempPathFactory + + def __init__( + self, tmppath_factory: TempPathFactory, *, _ispytest: bool = False + ) -> None: + check_ispytest(_ispytest) + self._tmppath_factory = tmppath_factory + + def mktemp(self, basename: str, numbered: bool = True) -> LEGACY_PATH: + """Same as :meth:`TempPathFactory.mktemp`, but returns a ``py.path.local`` object.""" + return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve()) + + def getbasetemp(self) -> LEGACY_PATH: + """Same as :meth:`TempPathFactory.getbasetemp`, but returns a ``py.path.local`` object.""" + return legacy_path(self._tmppath_factory.getbasetemp().resolve()) + + +class LegacyTmpdirPlugin: + @staticmethod + @fixture(scope="session") + def tmpdir_factory(request: FixtureRequest) -> TempdirFactory: + """Return a :class:`pytest.TempdirFactory` instance for the test session.""" + # Set dynamically by pytest_configure(). + return request.config._tmpdirhandler # type: ignore + + @staticmethod + @fixture + def tmpdir(tmp_path: Path) -> LEGACY_PATH: + """Return a temporary directory (as `legacy_path`_ object) + which is unique to each test function invocation. + The temporary directory is created as a subdirectory + of the base temporary directory, with configurable retention, + as discussed in :ref:`temporary directory location and retention`. + + .. note:: + These days, it is preferred to use ``tmp_path``. + + :ref:`About the tmpdir and tmpdir_factory fixtures`. + + .. _legacy_path: https://py.readthedocs.io/en/latest/path.html + """ + return legacy_path(tmp_path) + + +def Cache_makedir(self: Cache, name: str) -> LEGACY_PATH: + """Return a directory path object with the given name. + + Same as :func:`mkdir`, but returns a legacy py path instance. + """ + return legacy_path(self.mkdir(name)) + + +def FixtureRequest_fspath(self: FixtureRequest) -> LEGACY_PATH: + """(deprecated) The file system path of the test module which collected this test.""" + return legacy_path(self.path) + + +def TerminalReporter_startdir(self: TerminalReporter) -> LEGACY_PATH: + """The directory from which pytest was invoked. + + Prefer to use ``startpath`` which is a :class:`pathlib.Path`. + + :type: LEGACY_PATH + """ + return legacy_path(self.startpath) + + +def Config_invocation_dir(self: Config) -> LEGACY_PATH: + """The directory from which pytest was invoked. + + Prefer to use :attr:`invocation_params.dir `, + which is a :class:`pathlib.Path`. + + :type: LEGACY_PATH + """ + return legacy_path(str(self.invocation_params.dir)) + + +def Config_rootdir(self: Config) -> LEGACY_PATH: + """The path to the :ref:`rootdir `. + + Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`. + + :type: LEGACY_PATH + """ + return legacy_path(str(self.rootpath)) + + +def Config_inifile(self: Config) -> LEGACY_PATH | None: + """The path to the :ref:`configfile `. + + Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`. + + :type: Optional[LEGACY_PATH] + """ + return legacy_path(str(self.inipath)) if self.inipath else None + + +def Session_startdir(self: Session) -> LEGACY_PATH: + """The path from which pytest was invoked. + + Prefer to use ``startpath`` which is a :class:`pathlib.Path`. + + :type: LEGACY_PATH + """ + return legacy_path(self.startpath) + + +def Config__getini_unknown_type(self, name: str, type: str, value: str | list[str]): + if type == "pathlist": + # TODO: This assert is probably not valid in all cases. + assert self.inipath is not None + dp = self.inipath.parent + input_values = shlex.split(value) if isinstance(value, str) else value + return [legacy_path(str(dp / x)) for x in input_values] + else: + raise ValueError(f"unknown configuration type: {type}", value) + + +def Node_fspath(self: Node) -> LEGACY_PATH: + """(deprecated) returns a legacy_path copy of self.path""" + return legacy_path(self.path) + + +def Node_fspath_set(self: Node, value: LEGACY_PATH) -> None: + self.path = Path(value) + + +@hookimpl(tryfirst=True) +def pytest_load_initial_conftests(early_config: Config) -> None: + """Monkeypatch legacy path attributes in several classes, as early as possible.""" + mp = MonkeyPatch() + early_config.add_cleanup(mp.undo) + + # Add Cache.makedir(). + mp.setattr(Cache, "makedir", Cache_makedir, raising=False) + + # Add FixtureRequest.fspath property. + mp.setattr(FixtureRequest, "fspath", property(FixtureRequest_fspath), raising=False) + + # Add TerminalReporter.startdir property. + mp.setattr( + TerminalReporter, "startdir", property(TerminalReporter_startdir), raising=False + ) + + # Add Config.{invocation_dir,rootdir,inifile} properties. + mp.setattr(Config, "invocation_dir", property(Config_invocation_dir), raising=False) + mp.setattr(Config, "rootdir", property(Config_rootdir), raising=False) + mp.setattr(Config, "inifile", property(Config_inifile), raising=False) + + # Add Session.startdir property. + mp.setattr(Session, "startdir", property(Session_startdir), raising=False) + + # Add pathlist configuration type. + mp.setattr(Config, "_getini_unknown_type", Config__getini_unknown_type) + + # Add Node.fspath property. + mp.setattr(Node, "fspath", property(Node_fspath, Node_fspath_set), raising=False) + + +@hookimpl +def pytest_configure(config: Config) -> None: + """Installs the LegacyTmpdirPlugin if the ``tmpdir`` plugin is also installed.""" + if config.pluginmanager.has_plugin("tmpdir"): + mp = MonkeyPatch() + config.add_cleanup(mp.undo) + # Create TmpdirFactory and attach it to the config object. + # + # This is to comply with existing plugins which expect the handler to be + # available at pytest_configure time, but ideally should be moved entirely + # to the tmpdir_factory session fixture. + try: + tmp_path_factory = config._tmp_path_factory # type: ignore[attr-defined] + except AttributeError: + # tmpdir plugin is blocked. + pass + else: + _tmpdirhandler = TempdirFactory(tmp_path_factory, _ispytest=True) + mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False) + + config.pluginmanager.register(LegacyTmpdirPlugin, "legacypath-tmpdir") + + +@hookimpl +def pytest_plugin_registered(plugin: object, manager: PytestPluginManager) -> None: + # pytester is not loaded by default and is commonly loaded from a conftest, + # so checking for it in `pytest_configure` is not enough. + is_pytester = plugin is manager.get_plugin("pytester") + if is_pytester and not manager.is_registered(LegacyTestdirPlugin): + manager.register(LegacyTestdirPlugin, "legacypath-pytester") diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/logging.py b/Backend/venv/lib/python3.12/site-packages/_pytest/logging.py new file mode 100644 index 00000000..e4fed579 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/logging.py @@ -0,0 +1,960 @@ +# mypy: allow-untyped-defs +"""Access and control log capturing.""" + +from __future__ import annotations + +from collections.abc import Generator +from collections.abc import Mapping +from collections.abc import Set as AbstractSet +from contextlib import contextmanager +from contextlib import nullcontext +from datetime import datetime +from datetime import timedelta +from datetime import timezone +import io +from io import StringIO +import logging +from logging import LogRecord +import os +from pathlib import Path +import re +from types import TracebackType +from typing import final +from typing import Generic +from typing import Literal +from typing import TYPE_CHECKING +from typing import TypeVar + +from _pytest import nodes +from _pytest._io import TerminalWriter +from _pytest.capture import CaptureManager +from _pytest.config import _strtobool +from _pytest.config import Config +from _pytest.config import create_terminal_writer +from _pytest.config import hookimpl +from _pytest.config import UsageError +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import fixture +from _pytest.fixtures import FixtureRequest +from _pytest.main import Session +from _pytest.stash import StashKey +from _pytest.terminal import TerminalReporter + + +if TYPE_CHECKING: + logging_StreamHandler = logging.StreamHandler[StringIO] +else: + logging_StreamHandler = logging.StreamHandler + +DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s" +DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S" +_ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m") +caplog_handler_key = StashKey["LogCaptureHandler"]() +caplog_records_key = StashKey[dict[str, list[logging.LogRecord]]]() + + +def _remove_ansi_escape_sequences(text: str) -> str: + return _ANSI_ESCAPE_SEQ.sub("", text) + + +class DatetimeFormatter(logging.Formatter): + """A logging formatter which formats record with + :func:`datetime.datetime.strftime` formatter instead of + :func:`time.strftime` in case of microseconds in format string. + """ + + def formatTime(self, record: LogRecord, datefmt: str | None = None) -> str: + if datefmt and "%f" in datefmt: + ct = self.converter(record.created) + tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone) + # Construct `datetime.datetime` object from `struct_time` + # and msecs information from `record` + # Using int() instead of round() to avoid it exceeding 1_000_000 and causing a ValueError (#11861). + dt = datetime(*ct[0:6], microsecond=int(record.msecs * 1000), tzinfo=tz) + return dt.strftime(datefmt) + # Use `logging.Formatter` for non-microsecond formats + return super().formatTime(record, datefmt) + + +class ColoredLevelFormatter(DatetimeFormatter): + """A logging formatter which colorizes the %(levelname)..s part of the + log format passed to __init__.""" + + LOGLEVEL_COLOROPTS: Mapping[int, AbstractSet[str]] = { + logging.CRITICAL: {"red"}, + logging.ERROR: {"red", "bold"}, + logging.WARNING: {"yellow"}, + logging.WARN: {"yellow"}, + logging.INFO: {"green"}, + logging.DEBUG: {"purple"}, + logging.NOTSET: set(), + } + LEVELNAME_FMT_REGEX = re.compile(r"%\(levelname\)([+-.]?\d*(?:\.\d+)?s)") + + def __init__(self, terminalwriter: TerminalWriter, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._terminalwriter = terminalwriter + self._original_fmt = self._style._fmt + self._level_to_fmt_mapping: dict[int, str] = {} + + for level, color_opts in self.LOGLEVEL_COLOROPTS.items(): + self.add_color_level(level, *color_opts) + + def add_color_level(self, level: int, *color_opts: str) -> None: + """Add or update color opts for a log level. + + :param level: + Log level to apply a style to, e.g. ``logging.INFO``. + :param color_opts: + ANSI escape sequence color options. Capitalized colors indicates + background color, i.e. ``'green', 'Yellow', 'bold'`` will give bold + green text on yellow background. + + .. warning:: + This is an experimental API. + """ + assert self._fmt is not None + levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt) + if not levelname_fmt_match: + return + levelname_fmt = levelname_fmt_match.group() + + formatted_levelname = levelname_fmt % {"levelname": logging.getLevelName(level)} + + # add ANSI escape sequences around the formatted levelname + color_kwargs = {name: True for name in color_opts} + colorized_formatted_levelname = self._terminalwriter.markup( + formatted_levelname, **color_kwargs + ) + self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub( + colorized_formatted_levelname, self._fmt + ) + + def format(self, record: logging.LogRecord) -> str: + fmt = self._level_to_fmt_mapping.get(record.levelno, self._original_fmt) + self._style._fmt = fmt + return super().format(record) + + +class PercentStyleMultiline(logging.PercentStyle): + """A logging style with special support for multiline messages. + + If the message of a record consists of multiple lines, this style + formats the message as if each line were logged separately. + """ + + def __init__(self, fmt: str, auto_indent: int | str | bool | None) -> None: + super().__init__(fmt) + self._auto_indent = self._get_auto_indent(auto_indent) + + @staticmethod + def _get_auto_indent(auto_indent_option: int | str | bool | None) -> int: + """Determine the current auto indentation setting. + + Specify auto indent behavior (on/off/fixed) by passing in + extra={"auto_indent": [value]} to the call to logging.log() or + using a --log-auto-indent [value] command line or the + log_auto_indent [value] config option. + + Default behavior is auto-indent off. + + Using the string "True" or "on" or the boolean True as the value + turns auto indent on, using the string "False" or "off" or the + boolean False or the int 0 turns it off, and specifying a + positive integer fixes the indentation position to the value + specified. + + Any other values for the option are invalid, and will silently be + converted to the default. + + :param None|bool|int|str auto_indent_option: + User specified option for indentation from command line, config + or extra kwarg. Accepts int, bool or str. str option accepts the + same range of values as boolean config options, as well as + positive integers represented in str form. + + :returns: + Indentation value, which can be + -1 (automatically determine indentation) or + 0 (auto-indent turned off) or + >0 (explicitly set indentation position). + """ + if auto_indent_option is None: + return 0 + elif isinstance(auto_indent_option, bool): + if auto_indent_option: + return -1 + else: + return 0 + elif isinstance(auto_indent_option, int): + return int(auto_indent_option) + elif isinstance(auto_indent_option, str): + try: + return int(auto_indent_option) + except ValueError: + pass + try: + if _strtobool(auto_indent_option): + return -1 + except ValueError: + return 0 + + return 0 + + def format(self, record: logging.LogRecord) -> str: + if "\n" in record.message: + if hasattr(record, "auto_indent"): + # Passed in from the "extra={}" kwarg on the call to logging.log(). + auto_indent = self._get_auto_indent(record.auto_indent) + else: + auto_indent = self._auto_indent + + if auto_indent: + lines = record.message.splitlines() + formatted = self._fmt % {**record.__dict__, "message": lines[0]} + + if auto_indent < 0: + indentation = _remove_ansi_escape_sequences(formatted).find( + lines[0] + ) + else: + # Optimizes logging by allowing a fixed indentation. + indentation = auto_indent + lines[0] = formatted + return ("\n" + " " * indentation).join(lines) + return self._fmt % record.__dict__ + + +def get_option_ini(config: Config, *names: str): + for name in names: + ret = config.getoption(name) # 'default' arg won't work as expected + if ret is None: + ret = config.getini(name) + if ret: + return ret + + +def pytest_addoption(parser: Parser) -> None: + """Add options to control log capturing.""" + group = parser.getgroup("logging") + + def add_option_ini(option, dest, default=None, type=None, **kwargs): + parser.addini( + dest, default=default, type=type, help="Default value for " + option + ) + group.addoption(option, dest=dest, **kwargs) + + add_option_ini( + "--log-level", + dest="log_level", + default=None, + metavar="LEVEL", + help=( + "Level of messages to catch/display." + " Not set by default, so it depends on the root/parent log handler's" + ' effective level, where it is "WARNING" by default.' + ), + ) + add_option_ini( + "--log-format", + dest="log_format", + default=DEFAULT_LOG_FORMAT, + help="Log format used by the logging module", + ) + add_option_ini( + "--log-date-format", + dest="log_date_format", + default=DEFAULT_LOG_DATE_FORMAT, + help="Log date format used by the logging module", + ) + parser.addini( + "log_cli", + default=False, + type="bool", + help='Enable log display during test run (also known as "live logging")', + ) + add_option_ini( + "--log-cli-level", dest="log_cli_level", default=None, help="CLI logging level" + ) + add_option_ini( + "--log-cli-format", + dest="log_cli_format", + default=None, + help="Log format used by the logging module", + ) + add_option_ini( + "--log-cli-date-format", + dest="log_cli_date_format", + default=None, + help="Log date format used by the logging module", + ) + add_option_ini( + "--log-file", + dest="log_file", + default=None, + help="Path to a file when logging will be written to", + ) + add_option_ini( + "--log-file-mode", + dest="log_file_mode", + default="w", + choices=["w", "a"], + help="Log file open mode", + ) + add_option_ini( + "--log-file-level", + dest="log_file_level", + default=None, + help="Log file logging level", + ) + add_option_ini( + "--log-file-format", + dest="log_file_format", + default=None, + help="Log format used by the logging module", + ) + add_option_ini( + "--log-file-date-format", + dest="log_file_date_format", + default=None, + help="Log date format used by the logging module", + ) + add_option_ini( + "--log-auto-indent", + dest="log_auto_indent", + default=None, + help="Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer.", + ) + group.addoption( + "--log-disable", + action="append", + default=[], + dest="logger_disable", + help="Disable a logger by name. Can be passed multiple times.", + ) + + +_HandlerType = TypeVar("_HandlerType", bound=logging.Handler) + + +# Not using @contextmanager for performance reasons. +class catching_logs(Generic[_HandlerType]): + """Context manager that prepares the whole logging machinery properly.""" + + __slots__ = ("handler", "level", "orig_level") + + def __init__(self, handler: _HandlerType, level: int | None = None) -> None: + self.handler = handler + self.level = level + + def __enter__(self) -> _HandlerType: + root_logger = logging.getLogger() + if self.level is not None: + self.handler.setLevel(self.level) + root_logger.addHandler(self.handler) + if self.level is not None: + self.orig_level = root_logger.level + root_logger.setLevel(min(self.orig_level, self.level)) + return self.handler + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + root_logger = logging.getLogger() + if self.level is not None: + root_logger.setLevel(self.orig_level) + root_logger.removeHandler(self.handler) + + +class LogCaptureHandler(logging_StreamHandler): + """A logging handler that stores log records and the log text.""" + + def __init__(self) -> None: + """Create a new log handler.""" + super().__init__(StringIO()) + self.records: list[logging.LogRecord] = [] + + def emit(self, record: logging.LogRecord) -> None: + """Keep the log records in a list in addition to the log text.""" + self.records.append(record) + super().emit(record) + + def reset(self) -> None: + self.records = [] + self.stream = StringIO() + + def clear(self) -> None: + self.records.clear() + self.stream = StringIO() + + def handleError(self, record: logging.LogRecord) -> None: + if logging.raiseExceptions: + # Fail the test if the log message is bad (emit failed). + # The default behavior of logging is to print "Logging error" + # to stderr with the call stack and some extra details. + # pytest wants to make such mistakes visible during testing. + raise # noqa: PLE0704 + + +@final +class LogCaptureFixture: + """Provides access and control of log capturing.""" + + def __init__(self, item: nodes.Node, *, _ispytest: bool = False) -> None: + check_ispytest(_ispytest) + self._item = item + self._initial_handler_level: int | None = None + # Dict of log name -> log level. + self._initial_logger_levels: dict[str | None, int] = {} + self._initial_disabled_logging_level: int | None = None + + def _finalize(self) -> None: + """Finalize the fixture. + + This restores the log levels and the disabled logging levels changed by :meth:`set_level`. + """ + # Restore log levels. + if self._initial_handler_level is not None: + self.handler.setLevel(self._initial_handler_level) + for logger_name, level in self._initial_logger_levels.items(): + logger = logging.getLogger(logger_name) + logger.setLevel(level) + # Disable logging at the original disabled logging level. + if self._initial_disabled_logging_level is not None: + logging.disable(self._initial_disabled_logging_level) + self._initial_disabled_logging_level = None + + @property + def handler(self) -> LogCaptureHandler: + """Get the logging handler used by the fixture.""" + return self._item.stash[caplog_handler_key] + + def get_records( + self, when: Literal["setup", "call", "teardown"] + ) -> list[logging.LogRecord]: + """Get the logging records for one of the possible test phases. + + :param when: + Which test phase to obtain the records from. + Valid values are: "setup", "call" and "teardown". + + :returns: The list of captured records at the given stage. + + .. versionadded:: 3.4 + """ + return self._item.stash[caplog_records_key].get(when, []) + + @property + def text(self) -> str: + """The formatted log text.""" + return _remove_ansi_escape_sequences(self.handler.stream.getvalue()) + + @property + def records(self) -> list[logging.LogRecord]: + """The list of log records.""" + return self.handler.records + + @property + def record_tuples(self) -> list[tuple[str, int, str]]: + """A list of a stripped down version of log records intended + for use in assertion comparison. + + The format of the tuple is: + + (logger_name, log_level, message) + """ + return [(r.name, r.levelno, r.getMessage()) for r in self.records] + + @property + def messages(self) -> list[str]: + """A list of format-interpolated log messages. + + Unlike 'records', which contains the format string and parameters for + interpolation, log messages in this list are all interpolated. + + Unlike 'text', which contains the output from the handler, log + messages in this list are unadorned with levels, timestamps, etc, + making exact comparisons more reliable. + + Note that traceback or stack info (from :func:`logging.exception` or + the `exc_info` or `stack_info` arguments to the logging functions) is + not included, as this is added by the formatter in the handler. + + .. versionadded:: 3.7 + """ + return [r.getMessage() for r in self.records] + + def clear(self) -> None: + """Reset the list of log records and the captured log text.""" + self.handler.clear() + + def _force_enable_logging( + self, level: int | str, logger_obj: logging.Logger + ) -> int: + """Enable the desired logging level if the global level was disabled via ``logging.disabled``. + + Only enables logging levels greater than or equal to the requested ``level``. + + Does nothing if the desired ``level`` wasn't disabled. + + :param level: + The logger level caplog should capture. + All logging is enabled if a non-standard logging level string is supplied. + Valid level strings are in :data:`logging._nameToLevel`. + :param logger_obj: The logger object to check. + + :return: The original disabled logging level. + """ + original_disable_level: int = logger_obj.manager.disable + + if isinstance(level, str): + # Try to translate the level string to an int for `logging.disable()` + level = logging.getLevelName(level) + + if not isinstance(level, int): + # The level provided was not valid, so just un-disable all logging. + logging.disable(logging.NOTSET) + elif not logger_obj.isEnabledFor(level): + # Each level is `10` away from other levels. + # https://docs.python.org/3/library/logging.html#logging-levels + disable_level = max(level - 10, logging.NOTSET) + logging.disable(disable_level) + + return original_disable_level + + def set_level(self, level: int | str, logger: str | None = None) -> None: + """Set the threshold level of a logger for the duration of a test. + + Logging messages which are less severe than this level will not be captured. + + .. versionchanged:: 3.4 + The levels of the loggers changed by this function will be + restored to their initial values at the end of the test. + + Will enable the requested logging level if it was disabled via :func:`logging.disable`. + + :param level: The level. + :param logger: The logger to update. If not given, the root logger. + """ + logger_obj = logging.getLogger(logger) + # Save the original log-level to restore it during teardown. + self._initial_logger_levels.setdefault(logger, logger_obj.level) + logger_obj.setLevel(level) + if self._initial_handler_level is None: + self._initial_handler_level = self.handler.level + self.handler.setLevel(level) + initial_disabled_logging_level = self._force_enable_logging(level, logger_obj) + if self._initial_disabled_logging_level is None: + self._initial_disabled_logging_level = initial_disabled_logging_level + + @contextmanager + def at_level(self, level: int | str, logger: str | None = None) -> Generator[None]: + """Context manager that sets the level for capturing of logs. After + the end of the 'with' statement the level is restored to its original + value. + + Will enable the requested logging level if it was disabled via :func:`logging.disable`. + + :param level: The level. + :param logger: The logger to update. If not given, the root logger. + """ + logger_obj = logging.getLogger(logger) + orig_level = logger_obj.level + logger_obj.setLevel(level) + handler_orig_level = self.handler.level + self.handler.setLevel(level) + original_disable_level = self._force_enable_logging(level, logger_obj) + try: + yield + finally: + logger_obj.setLevel(orig_level) + self.handler.setLevel(handler_orig_level) + logging.disable(original_disable_level) + + @contextmanager + def filtering(self, filter_: logging.Filter) -> Generator[None]: + """Context manager that temporarily adds the given filter to the caplog's + :meth:`handler` for the 'with' statement block, and removes that filter at the + end of the block. + + :param filter_: A custom :class:`logging.Filter` object. + + .. versionadded:: 7.5 + """ + self.handler.addFilter(filter_) + try: + yield + finally: + self.handler.removeFilter(filter_) + + +@fixture +def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture]: + """Access and control log capturing. + + Captured logs are available through the following properties/methods:: + + * caplog.messages -> list of format-interpolated log messages + * caplog.text -> string containing formatted log output + * caplog.records -> list of logging.LogRecord instances + * caplog.record_tuples -> list of (logger_name, level, message) tuples + * caplog.clear() -> clear captured records and formatted log output string + """ + result = LogCaptureFixture(request.node, _ispytest=True) + yield result + result._finalize() + + +def get_log_level_for_setting(config: Config, *setting_names: str) -> int | None: + for setting_name in setting_names: + log_level = config.getoption(setting_name) + if log_level is None: + log_level = config.getini(setting_name) + if log_level: + break + else: + return None + + if isinstance(log_level, str): + log_level = log_level.upper() + try: + return int(getattr(logging, log_level, log_level)) + except ValueError as e: + # Python logging does not recognise this as a logging level + raise UsageError( + f"'{log_level}' is not recognized as a logging level name for " + f"'{setting_name}'. Please consider passing the " + "logging level num instead." + ) from e + + +# run after terminalreporter/capturemanager are configured +@hookimpl(trylast=True) +def pytest_configure(config: Config) -> None: + config.pluginmanager.register(LoggingPlugin(config), "logging-plugin") + + +class LoggingPlugin: + """Attaches to the logging module and captures log messages for each test.""" + + def __init__(self, config: Config) -> None: + """Create a new plugin to capture log messages. + + The formatter can be safely shared across all handlers so + create a single one for the entire test session here. + """ + self._config = config + + # Report logging. + self.formatter = self._create_formatter( + get_option_ini(config, "log_format"), + get_option_ini(config, "log_date_format"), + get_option_ini(config, "log_auto_indent"), + ) + self.log_level = get_log_level_for_setting(config, "log_level") + self.caplog_handler = LogCaptureHandler() + self.caplog_handler.setFormatter(self.formatter) + self.report_handler = LogCaptureHandler() + self.report_handler.setFormatter(self.formatter) + + # File logging. + self.log_file_level = get_log_level_for_setting( + config, "log_file_level", "log_level" + ) + log_file = get_option_ini(config, "log_file") or os.devnull + if log_file != os.devnull: + directory = os.path.dirname(os.path.abspath(log_file)) + if not os.path.isdir(directory): + os.makedirs(directory) + + self.log_file_mode = get_option_ini(config, "log_file_mode") or "w" + self.log_file_handler = _FileHandler( + log_file, mode=self.log_file_mode, encoding="UTF-8" + ) + log_file_format = get_option_ini(config, "log_file_format", "log_format") + log_file_date_format = get_option_ini( + config, "log_file_date_format", "log_date_format" + ) + + log_file_formatter = DatetimeFormatter( + log_file_format, datefmt=log_file_date_format + ) + self.log_file_handler.setFormatter(log_file_formatter) + + # CLI/live logging. + self.log_cli_level = get_log_level_for_setting( + config, "log_cli_level", "log_level" + ) + if self._log_cli_enabled(): + terminal_reporter = config.pluginmanager.get_plugin("terminalreporter") + # Guaranteed by `_log_cli_enabled()`. + assert terminal_reporter is not None + capture_manager = config.pluginmanager.get_plugin("capturemanager") + # if capturemanager plugin is disabled, live logging still works. + self.log_cli_handler: ( + _LiveLoggingStreamHandler | _LiveLoggingNullHandler + ) = _LiveLoggingStreamHandler(terminal_reporter, capture_manager) + else: + self.log_cli_handler = _LiveLoggingNullHandler() + log_cli_formatter = self._create_formatter( + get_option_ini(config, "log_cli_format", "log_format"), + get_option_ini(config, "log_cli_date_format", "log_date_format"), + get_option_ini(config, "log_auto_indent"), + ) + self.log_cli_handler.setFormatter(log_cli_formatter) + self._disable_loggers(loggers_to_disable=config.option.logger_disable) + + def _disable_loggers(self, loggers_to_disable: list[str]) -> None: + if not loggers_to_disable: + return + + for name in loggers_to_disable: + logger = logging.getLogger(name) + logger.disabled = True + + def _create_formatter(self, log_format, log_date_format, auto_indent): + # Color option doesn't exist if terminal plugin is disabled. + color = getattr(self._config.option, "color", "no") + if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search( + log_format + ): + formatter: logging.Formatter = ColoredLevelFormatter( + create_terminal_writer(self._config), log_format, log_date_format + ) + else: + formatter = DatetimeFormatter(log_format, log_date_format) + + formatter._style = PercentStyleMultiline( + formatter._style._fmt, auto_indent=auto_indent + ) + + return formatter + + def set_log_path(self, fname: str) -> None: + """Set the filename parameter for Logging.FileHandler(). + + Creates parent directory if it does not exist. + + .. warning:: + This is an experimental API. + """ + fpath = Path(fname) + + if not fpath.is_absolute(): + fpath = self._config.rootpath / fpath + + if not fpath.parent.exists(): + fpath.parent.mkdir(exist_ok=True, parents=True) + + # https://github.com/python/mypy/issues/11193 + stream: io.TextIOWrapper = fpath.open(mode=self.log_file_mode, encoding="UTF-8") # type: ignore[assignment] + old_stream = self.log_file_handler.setStream(stream) + if old_stream: + old_stream.close() + + def _log_cli_enabled(self) -> bool: + """Return whether live logging is enabled.""" + enabled = self._config.getoption( + "--log-cli-level" + ) is not None or self._config.getini("log_cli") + if not enabled: + return False + + terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter") + if terminal_reporter is None: + # terminal reporter is disabled e.g. by pytest-xdist. + return False + + return True + + @hookimpl(wrapper=True, tryfirst=True) + def pytest_sessionstart(self) -> Generator[None]: + self.log_cli_handler.set_when("sessionstart") + + with catching_logs(self.log_cli_handler, level=self.log_cli_level): + with catching_logs(self.log_file_handler, level=self.log_file_level): + return (yield) + + @hookimpl(wrapper=True, tryfirst=True) + def pytest_collection(self) -> Generator[None]: + self.log_cli_handler.set_when("collection") + + with catching_logs(self.log_cli_handler, level=self.log_cli_level): + with catching_logs(self.log_file_handler, level=self.log_file_level): + return (yield) + + @hookimpl(wrapper=True) + def pytest_runtestloop(self, session: Session) -> Generator[None, object, object]: + if session.config.option.collectonly: + return (yield) + + if self._log_cli_enabled() and self._config.get_verbosity() < 1: + # The verbose flag is needed to avoid messy test progress output. + self._config.option.verbose = 1 + + with catching_logs(self.log_cli_handler, level=self.log_cli_level): + with catching_logs(self.log_file_handler, level=self.log_file_level): + return (yield) # Run all the tests. + + @hookimpl + def pytest_runtest_logstart(self) -> None: + self.log_cli_handler.reset() + self.log_cli_handler.set_when("start") + + @hookimpl + def pytest_runtest_logreport(self) -> None: + self.log_cli_handler.set_when("logreport") + + @contextmanager + def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None]: + """Implement the internals of the pytest_runtest_xxx() hooks.""" + with ( + catching_logs( + self.caplog_handler, + level=self.log_level, + ) as caplog_handler, + catching_logs( + self.report_handler, + level=self.log_level, + ) as report_handler, + ): + caplog_handler.reset() + report_handler.reset() + item.stash[caplog_records_key][when] = caplog_handler.records + item.stash[caplog_handler_key] = caplog_handler + + try: + yield + finally: + log = report_handler.stream.getvalue().strip() + item.add_report_section(when, "log", log) + + @hookimpl(wrapper=True) + def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None]: + self.log_cli_handler.set_when("setup") + + empty: dict[str, list[logging.LogRecord]] = {} + item.stash[caplog_records_key] = empty + with self._runtest_for(item, "setup"): + yield + + @hookimpl(wrapper=True) + def pytest_runtest_call(self, item: nodes.Item) -> Generator[None]: + self.log_cli_handler.set_when("call") + + with self._runtest_for(item, "call"): + yield + + @hookimpl(wrapper=True) + def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None]: + self.log_cli_handler.set_when("teardown") + + try: + with self._runtest_for(item, "teardown"): + yield + finally: + del item.stash[caplog_records_key] + del item.stash[caplog_handler_key] + + @hookimpl + def pytest_runtest_logfinish(self) -> None: + self.log_cli_handler.set_when("finish") + + @hookimpl(wrapper=True, tryfirst=True) + def pytest_sessionfinish(self) -> Generator[None]: + self.log_cli_handler.set_when("sessionfinish") + + with catching_logs(self.log_cli_handler, level=self.log_cli_level): + with catching_logs(self.log_file_handler, level=self.log_file_level): + return (yield) + + @hookimpl + def pytest_unconfigure(self) -> None: + # Close the FileHandler explicitly. + # (logging.shutdown might have lost the weakref?!) + self.log_file_handler.close() + + +class _FileHandler(logging.FileHandler): + """A logging FileHandler with pytest tweaks.""" + + def handleError(self, record: logging.LogRecord) -> None: + # Handled by LogCaptureHandler. + pass + + +class _LiveLoggingStreamHandler(logging_StreamHandler): + """A logging StreamHandler used by the live logging feature: it will + write a newline before the first log message in each test. + + During live logging we must also explicitly disable stdout/stderr + capturing otherwise it will get captured and won't appear in the + terminal. + """ + + # Officially stream needs to be a IO[str], but TerminalReporter + # isn't. So force it. + stream: TerminalReporter = None # type: ignore + + def __init__( + self, + terminal_reporter: TerminalReporter, + capture_manager: CaptureManager | None, + ) -> None: + super().__init__(stream=terminal_reporter) # type: ignore[arg-type] + self.capture_manager = capture_manager + self.reset() + self.set_when(None) + self._test_outcome_written = False + + def reset(self) -> None: + """Reset the handler; should be called before the start of each test.""" + self._first_record_emitted = False + + def set_when(self, when: str | None) -> None: + """Prepare for the given test phase (setup/call/teardown).""" + self._when = when + self._section_name_shown = False + if when == "start": + self._test_outcome_written = False + + def emit(self, record: logging.LogRecord) -> None: + ctx_manager = ( + self.capture_manager.global_and_fixture_disabled() + if self.capture_manager + else nullcontext() + ) + with ctx_manager: + if not self._first_record_emitted: + self.stream.write("\n") + self._first_record_emitted = True + elif self._when in ("teardown", "finish"): + if not self._test_outcome_written: + self._test_outcome_written = True + self.stream.write("\n") + if not self._section_name_shown and self._when: + self.stream.section("live log " + self._when, sep="-", bold=True) + self._section_name_shown = True + super().emit(record) + + def handleError(self, record: logging.LogRecord) -> None: + # Handled by LogCaptureHandler. + pass + + +class _LiveLoggingNullHandler(logging.NullHandler): + """A logging handler used when live logging is disabled.""" + + def reset(self) -> None: + pass + + def set_when(self, when: str) -> None: + pass + + def handleError(self, record: logging.LogRecord) -> None: + # Handled by LogCaptureHandler. + pass diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/main.py b/Backend/venv/lib/python3.12/site-packages/_pytest/main.py new file mode 100644 index 00000000..9bc930df --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/main.py @@ -0,0 +1,1203 @@ +"""Core implementation of the testing process: init, session, runtest loop.""" + +from __future__ import annotations + +import argparse +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Sequence +from collections.abc import Set as AbstractSet +import dataclasses +import fnmatch +import functools +import importlib +import importlib.util +import os +from pathlib import Path +import sys +from typing import final +from typing import Literal +from typing import overload +from typing import TYPE_CHECKING +import warnings + +import pluggy + +from _pytest import nodes +import _pytest._code +from _pytest.config import Config +from _pytest.config import directory_arg +from _pytest.config import ExitCode +from _pytest.config import hookimpl +from _pytest.config import PytestPluginManager +from _pytest.config import UsageError +from _pytest.config.argparsing import OverrideIniAction +from _pytest.config.argparsing import Parser +from _pytest.config.compat import PathAwareHookProxy +from _pytest.outcomes import exit +from _pytest.pathlib import absolutepath +from _pytest.pathlib import bestrelpath +from _pytest.pathlib import fnmatch_ex +from _pytest.pathlib import safe_exists +from _pytest.pathlib import samefile_nofollow +from _pytest.pathlib import scandir +from _pytest.reports import CollectReport +from _pytest.reports import TestReport +from _pytest.runner import collect_one_node +from _pytest.runner import SetupState +from _pytest.warning_types import PytestWarning + + +if TYPE_CHECKING: + from typing_extensions import Self + + from _pytest.fixtures import FixtureManager + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("general") + group._addoption( # private to use reserved lower-case short option + "-x", + "--exitfirst", + action="store_const", + dest="maxfail", + const=1, + help="Exit instantly on first error or failed test", + ) + group.addoption( + "--maxfail", + metavar="num", + action="store", + type=int, + dest="maxfail", + default=0, + help="Exit after first num failures or errors", + ) + group.addoption( + "--strict-config", + action=OverrideIniAction, + ini_option="strict_config", + ini_value="true", + help="Enables the strict_config option", + ) + group.addoption( + "--strict-markers", + action=OverrideIniAction, + ini_option="strict_markers", + ini_value="true", + help="Enables the strict_markers option", + ) + group.addoption( + "--strict", + action=OverrideIniAction, + ini_option="strict", + ini_value="true", + help="Enables the strict option", + ) + parser.addini( + "strict_config", + "Any warnings encountered while parsing the `pytest` section of the " + "configuration file raise errors", + type="bool", + # None => fallback to `strict`. + default=None, + ) + parser.addini( + "strict_markers", + "Markers not registered in the `markers` section of the configuration " + "file raise errors", + type="bool", + # None => fallback to `strict`. + default=None, + ) + parser.addini( + "strict", + "Enables all strictness options, currently: " + "strict_config, strict_markers, strict_xfail, strict_parametrization_ids", + type="bool", + default=False, + ) + + group = parser.getgroup("pytest-warnings") + group.addoption( + "-W", + "--pythonwarnings", + action="append", + help="Set which warnings to report, see -W option of Python itself", + ) + parser.addini( + "filterwarnings", + type="linelist", + help="Each line specifies a pattern for " + "warnings.filterwarnings. " + "Processed after -W/--pythonwarnings.", + ) + + group = parser.getgroup("collect", "collection") + group.addoption( + "--collectonly", + "--collect-only", + "--co", + action="store_true", + help="Only collect tests, don't execute them", + ) + group.addoption( + "--pyargs", + action="store_true", + help="Try to interpret all arguments as Python packages", + ) + group.addoption( + "--ignore", + action="append", + metavar="path", + help="Ignore path during collection (multi-allowed)", + ) + group.addoption( + "--ignore-glob", + action="append", + metavar="path", + help="Ignore path pattern during collection (multi-allowed)", + ) + group.addoption( + "--deselect", + action="append", + metavar="nodeid_prefix", + help="Deselect item (via node id prefix) during collection (multi-allowed)", + ) + group.addoption( + "--confcutdir", + dest="confcutdir", + default=None, + metavar="dir", + type=functools.partial(directory_arg, optname="--confcutdir"), + help="Only load conftest.py's relative to specified dir", + ) + group.addoption( + "--noconftest", + action="store_true", + dest="noconftest", + default=False, + help="Don't load any conftest.py files", + ) + group.addoption( + "--keepduplicates", + "--keep-duplicates", + action="store_true", + dest="keepduplicates", + default=False, + help="Keep duplicate tests", + ) + group.addoption( + "--collect-in-virtualenv", + action="store_true", + dest="collect_in_virtualenv", + default=False, + help="Don't ignore tests in a local virtualenv directory", + ) + group.addoption( + "--continue-on-collection-errors", + action="store_true", + default=False, + dest="continue_on_collection_errors", + help="Force test execution even if collection errors occur", + ) + group.addoption( + "--import-mode", + default="prepend", + choices=["prepend", "append", "importlib"], + dest="importmode", + help="Prepend/append to sys.path when importing test modules and conftest " + "files. Default: prepend.", + ) + parser.addini( + "norecursedirs", + "Directory patterns to avoid for recursion", + type="args", + default=[ + "*.egg", + ".*", + "_darcs", + "build", + "CVS", + "dist", + "node_modules", + "venv", + "{arch}", + ], + ) + parser.addini( + "testpaths", + "Directories to search for tests when no files or directories are given on the " + "command line", + type="args", + default=[], + ) + parser.addini( + "collect_imported_tests", + "Whether to collect tests in imported modules outside `testpaths`", + type="bool", + default=True, + ) + parser.addini( + "consider_namespace_packages", + type="bool", + default=False, + help="Consider namespace packages when resolving module names during import", + ) + + group = parser.getgroup("debugconfig", "test session debugging and configuration") + group._addoption( # private to use reserved lower-case short option + "-c", + "--config-file", + metavar="FILE", + type=str, + dest="inifilename", + help="Load configuration from `FILE` instead of trying to locate one of the " + "implicit configuration files.", + ) + group.addoption( + "--rootdir", + action="store", + dest="rootdir", + help="Define root directory for tests. Can be relative path: 'root_dir', './root_dir', " + "'root_dir/another_dir/'; absolute path: '/home/user/root_dir'; path with variables: " + "'$HOME/root_dir'.", + ) + group.addoption( + "--basetemp", + dest="basetemp", + default=None, + type=validate_basetemp, + metavar="dir", + help=( + "Base temporary directory for this test run. " + "(Warning: this directory is removed if it exists.)" + ), + ) + + +def validate_basetemp(path: str) -> str: + # GH 7119 + msg = "basetemp must not be empty, the current working directory or any parent directory of it" + + # empty path + if not path: + raise argparse.ArgumentTypeError(msg) + + def is_ancestor(base: Path, query: Path) -> bool: + """Return whether query is an ancestor of base.""" + if base == query: + return True + return query in base.parents + + # check if path is an ancestor of cwd + if is_ancestor(Path.cwd(), Path(path).absolute()): + raise argparse.ArgumentTypeError(msg) + + # check symlinks for ancestors + if is_ancestor(Path.cwd().resolve(), Path(path).resolve()): + raise argparse.ArgumentTypeError(msg) + + return path + + +def wrap_session( + config: Config, doit: Callable[[Config, Session], int | ExitCode | None] +) -> int | ExitCode: + """Skeleton command line program.""" + session = Session.from_config(config) + session.exitstatus = ExitCode.OK + initstate = 0 + try: + try: + config._do_configure() + initstate = 1 + config.hook.pytest_sessionstart(session=session) + initstate = 2 + session.exitstatus = doit(config, session) or 0 + except UsageError: + session.exitstatus = ExitCode.USAGE_ERROR + raise + except Failed: + session.exitstatus = ExitCode.TESTS_FAILED + except (KeyboardInterrupt, exit.Exception): + excinfo = _pytest._code.ExceptionInfo.from_current() + exitstatus: int | ExitCode = ExitCode.INTERRUPTED + if isinstance(excinfo.value, exit.Exception): + if excinfo.value.returncode is not None: + exitstatus = excinfo.value.returncode + if initstate < 2: + sys.stderr.write(f"{excinfo.typename}: {excinfo.value.msg}\n") + config.hook.pytest_keyboard_interrupt(excinfo=excinfo) + session.exitstatus = exitstatus + except BaseException: + session.exitstatus = ExitCode.INTERNAL_ERROR + excinfo = _pytest._code.ExceptionInfo.from_current() + try: + config.notify_exception(excinfo, config.option) + except exit.Exception as exc: + if exc.returncode is not None: + session.exitstatus = exc.returncode + sys.stderr.write(f"{type(exc).__name__}: {exc}\n") + else: + if isinstance(excinfo.value, SystemExit): + sys.stderr.write("mainloop: caught unexpected SystemExit!\n") + + finally: + # Explicitly break reference cycle. + excinfo = None # type: ignore + os.chdir(session.startpath) + if initstate >= 2: + try: + config.hook.pytest_sessionfinish( + session=session, exitstatus=session.exitstatus + ) + except exit.Exception as exc: + if exc.returncode is not None: + session.exitstatus = exc.returncode + sys.stderr.write(f"{type(exc).__name__}: {exc}\n") + config._ensure_unconfigure() + return session.exitstatus + + +def pytest_cmdline_main(config: Config) -> int | ExitCode: + return wrap_session(config, _main) + + +def _main(config: Config, session: Session) -> int | ExitCode | None: + """Default command line protocol for initialization, session, + running tests and reporting.""" + config.hook.pytest_collection(session=session) + config.hook.pytest_runtestloop(session=session) + + if session.testsfailed: + return ExitCode.TESTS_FAILED + elif session.testscollected == 0: + return ExitCode.NO_TESTS_COLLECTED + return None + + +def pytest_collection(session: Session) -> None: + session.perform_collect() + + +def pytest_runtestloop(session: Session) -> bool: + if session.testsfailed and not session.config.option.continue_on_collection_errors: + raise session.Interrupted( + f"{session.testsfailed} error{'s' if session.testsfailed != 1 else ''} during collection" + ) + + if session.config.option.collectonly: + return True + + for i, item in enumerate(session.items): + nextitem = session.items[i + 1] if i + 1 < len(session.items) else None + item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) + if session.shouldfail: + raise session.Failed(session.shouldfail) + if session.shouldstop: + raise session.Interrupted(session.shouldstop) + return True + + +def _in_venv(path: Path) -> bool: + """Attempt to detect if ``path`` is the root of a Virtual Environment by + checking for the existence of the pyvenv.cfg file. + + [https://peps.python.org/pep-0405/] + + For regression protection we also check for conda environments that do not include pyenv.cfg yet -- + https://github.com/conda/conda/issues/13337 is the conda issue tracking adding pyenv.cfg. + + Checking for the `conda-meta/history` file per https://github.com/pytest-dev/pytest/issues/12652#issuecomment-2246336902. + + """ + try: + return ( + path.joinpath("pyvenv.cfg").is_file() + or path.joinpath("conda-meta", "history").is_file() + ) + except OSError: + return False + + +def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None: + if collection_path.name == "__pycache__": + return True + + ignore_paths = config._getconftest_pathlist( + "collect_ignore", path=collection_path.parent + ) + ignore_paths = ignore_paths or [] + excludeopt = config.getoption("ignore") + if excludeopt: + ignore_paths.extend(absolutepath(x) for x in excludeopt) + + if collection_path in ignore_paths: + return True + + ignore_globs = config._getconftest_pathlist( + "collect_ignore_glob", path=collection_path.parent + ) + ignore_globs = ignore_globs or [] + excludeglobopt = config.getoption("ignore_glob") + if excludeglobopt: + ignore_globs.extend(absolutepath(x) for x in excludeglobopt) + + if any(fnmatch.fnmatch(str(collection_path), str(glob)) for glob in ignore_globs): + return True + + allow_in_venv = config.getoption("collect_in_virtualenv") + if not allow_in_venv and _in_venv(collection_path): + return True + + if collection_path.is_dir(): + norecursepatterns = config.getini("norecursedirs") + if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns): + return True + + return None + + +def pytest_collect_directory( + path: Path, parent: nodes.Collector +) -> nodes.Collector | None: + return Dir.from_parent(parent, path=path) + + +def pytest_collection_modifyitems(items: list[nodes.Item], config: Config) -> None: + deselect_prefixes = tuple(config.getoption("deselect") or []) + if not deselect_prefixes: + return + + remaining = [] + deselected = [] + for colitem in items: + if colitem.nodeid.startswith(deselect_prefixes): + deselected.append(colitem) + else: + remaining.append(colitem) + + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = remaining + + +class FSHookProxy: + def __init__( + self, + pm: PytestPluginManager, + remove_mods: AbstractSet[object], + ) -> None: + self.pm = pm + self.remove_mods = remove_mods + + def __getattr__(self, name: str) -> pluggy.HookCaller: + x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods) + self.__dict__[name] = x + return x + + +class Interrupted(KeyboardInterrupt): + """Signals that the test run was interrupted.""" + + __module__ = "builtins" # For py3. + + +class Failed(Exception): + """Signals a stop as failed test run.""" + + +@dataclasses.dataclass +class _bestrelpath_cache(dict[Path, str]): + __slots__ = ("path",) + + path: Path + + def __missing__(self, path: Path) -> str: + r = bestrelpath(self.path, path) + self[path] = r + return r + + +@final +class Dir(nodes.Directory): + """Collector of files in a file system directory. + + .. versionadded:: 8.0 + + .. note:: + + Python directories with an `__init__.py` file are instead collected by + :class:`~pytest.Package` by default. Both are :class:`~pytest.Directory` + collectors. + """ + + @classmethod + def from_parent( # type: ignore[override] + cls, + parent: nodes.Collector, + *, + path: Path, + ) -> Self: + """The public constructor. + + :param parent: The parent collector of this Dir. + :param path: The directory's path. + :type path: pathlib.Path + """ + return super().from_parent(parent=parent, path=path) + + def collect(self) -> Iterable[nodes.Item | nodes.Collector]: + config = self.config + col: nodes.Collector | None + cols: Sequence[nodes.Collector] + ihook = self.ihook + for direntry in scandir(self.path): + if direntry.is_dir(): + path = Path(direntry.path) + if not self.session.isinitpath(path, with_parents=True): + if ihook.pytest_ignore_collect(collection_path=path, config=config): + continue + col = ihook.pytest_collect_directory(path=path, parent=self) + if col is not None: + yield col + + elif direntry.is_file(): + path = Path(direntry.path) + if not self.session.isinitpath(path): + if ihook.pytest_ignore_collect(collection_path=path, config=config): + continue + cols = ihook.pytest_collect_file(file_path=path, parent=self) + yield from cols + + +@final +class Session(nodes.Collector): + """The root of the collection tree. + + ``Session`` collects the initial paths given as arguments to pytest. + """ + + Interrupted = Interrupted + Failed = Failed + # Set on the session by runner.pytest_sessionstart. + _setupstate: SetupState + # Set on the session by fixtures.pytest_sessionstart. + _fixturemanager: FixtureManager + exitstatus: int | ExitCode + + def __init__(self, config: Config) -> None: + super().__init__( + name="", + path=config.rootpath, + fspath=None, + parent=None, + config=config, + session=self, + nodeid="", + ) + self.testsfailed = 0 + self.testscollected = 0 + self._shouldstop: bool | str = False + self._shouldfail: bool | str = False + self.trace = config.trace.root.get("collection") + self._initialpaths: frozenset[Path] = frozenset() + self._initialpaths_with_parents: frozenset[Path] = frozenset() + self._notfound: list[tuple[str, Sequence[nodes.Collector]]] = [] + self._initial_parts: list[CollectionArgument] = [] + self._collection_cache: dict[nodes.Collector, CollectReport] = {} + self.items: list[nodes.Item] = [] + + self._bestrelpathcache: dict[Path, str] = _bestrelpath_cache(config.rootpath) + + self.config.pluginmanager.register(self, name="session") + + @classmethod + def from_config(cls, config: Config) -> Session: + session: Session = cls._create(config=config) + return session + + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__} {self.name} " + f"exitstatus=%r " + f"testsfailed={self.testsfailed} " + f"testscollected={self.testscollected}>" + ) % getattr(self, "exitstatus", "") + + @property + def shouldstop(self) -> bool | str: + return self._shouldstop + + @shouldstop.setter + def shouldstop(self, value: bool | str) -> None: + # The runner checks shouldfail and assumes that if it is set we are + # definitely stopping, so prevent unsetting it. + if value is False and self._shouldstop: + warnings.warn( + PytestWarning( + "session.shouldstop cannot be unset after it has been set; ignoring." + ), + stacklevel=2, + ) + return + self._shouldstop = value + + @property + def shouldfail(self) -> bool | str: + return self._shouldfail + + @shouldfail.setter + def shouldfail(self, value: bool | str) -> None: + # The runner checks shouldfail and assumes that if it is set we are + # definitely stopping, so prevent unsetting it. + if value is False and self._shouldfail: + warnings.warn( + PytestWarning( + "session.shouldfail cannot be unset after it has been set; ignoring." + ), + stacklevel=2, + ) + return + self._shouldfail = value + + @property + def startpath(self) -> Path: + """The path from which pytest was invoked. + + .. versionadded:: 7.0.0 + """ + return self.config.invocation_params.dir + + def _node_location_to_relpath(self, node_path: Path) -> str: + # bestrelpath is a quite slow function. + return self._bestrelpathcache[node_path] + + @hookimpl(tryfirst=True) + def pytest_collectstart(self) -> None: + if self.shouldfail: + raise self.Failed(self.shouldfail) + if self.shouldstop: + raise self.Interrupted(self.shouldstop) + + @hookimpl(tryfirst=True) + def pytest_runtest_logreport(self, report: TestReport | CollectReport) -> None: + if report.failed and not hasattr(report, "wasxfail"): + self.testsfailed += 1 + maxfail = self.config.getvalue("maxfail") + if maxfail and self.testsfailed >= maxfail: + self.shouldfail = f"stopping after {self.testsfailed} failures" + + pytest_collectreport = pytest_runtest_logreport + + def isinitpath( + self, + path: str | os.PathLike[str], + *, + with_parents: bool = False, + ) -> bool: + """Is path an initial path? + + An initial path is a path explicitly given to pytest on the command + line. + + :param with_parents: + If set, also return True if the path is a parent of an initial path. + + .. versionchanged:: 8.0 + Added the ``with_parents`` parameter. + """ + # Optimization: Path(Path(...)) is much slower than isinstance. + path_ = path if isinstance(path, Path) else Path(path) + if with_parents: + return path_ in self._initialpaths_with_parents + else: + return path_ in self._initialpaths + + def gethookproxy(self, fspath: os.PathLike[str]) -> pluggy.HookRelay: + # Optimization: Path(Path(...)) is much slower than isinstance. + path = fspath if isinstance(fspath, Path) else Path(fspath) + pm = self.config.pluginmanager + # Check if we have the common case of running + # hooks with all conftest.py files. + my_conftestmodules = pm._getconftestmodules(path) + remove_mods = pm._conftest_plugins.difference(my_conftestmodules) + proxy: pluggy.HookRelay + if remove_mods: + # One or more conftests are not in use at this path. + proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods)) # type: ignore[arg-type,assignment] + else: + # All plugins are active for this fspath. + proxy = self.config.hook + return proxy + + def _collect_path( + self, + path: Path, + path_cache: dict[Path, Sequence[nodes.Collector]], + ) -> Sequence[nodes.Collector]: + """Create a Collector for the given path. + + `path_cache` makes it so the same Collectors are returned for the same + path. + """ + if path in path_cache: + return path_cache[path] + + if path.is_dir(): + ihook = self.gethookproxy(path.parent) + col: nodes.Collector | None = ihook.pytest_collect_directory( + path=path, parent=self + ) + cols: Sequence[nodes.Collector] = (col,) if col is not None else () + + elif path.is_file(): + ihook = self.gethookproxy(path) + cols = ihook.pytest_collect_file(file_path=path, parent=self) + + else: + # Broken symlink or invalid/missing file. + cols = () + + path_cache[path] = cols + return cols + + @overload + def perform_collect( + self, args: Sequence[str] | None = ..., genitems: Literal[True] = ... + ) -> Sequence[nodes.Item]: ... + + @overload + def perform_collect( + self, args: Sequence[str] | None = ..., genitems: bool = ... + ) -> Sequence[nodes.Item | nodes.Collector]: ... + + def perform_collect( + self, args: Sequence[str] | None = None, genitems: bool = True + ) -> Sequence[nodes.Item | nodes.Collector]: + """Perform the collection phase for this session. + + This is called by the default :hook:`pytest_collection` hook + implementation; see the documentation of this hook for more details. + For testing purposes, it may also be called directly on a fresh + ``Session``. + + This function normally recursively expands any collectors collected + from the session to their items, and only items are returned. For + testing purposes, this may be suppressed by passing ``genitems=False``, + in which case the return value contains these collectors unexpanded, + and ``session.items`` is empty. + """ + if args is None: + args = self.config.args + + self.trace("perform_collect", self, args) + self.trace.root.indent += 1 + + hook = self.config.hook + + self._notfound = [] + self._initial_parts = [] + self._collection_cache = {} + self.items = [] + items: Sequence[nodes.Item | nodes.Collector] = self.items + consider_namespace_packages: bool = self.config.getini( + "consider_namespace_packages" + ) + try: + initialpaths: list[Path] = [] + initialpaths_with_parents: list[Path] = [] + + collection_args = [ + resolve_collection_argument( + self.config.invocation_params.dir, + arg, + i, + as_pypath=self.config.option.pyargs, + consider_namespace_packages=consider_namespace_packages, + ) + for i, arg in enumerate(args) + ] + + if not self.config.getoption("keepduplicates"): + # Normalize the collection arguments -- remove duplicates and overlaps. + self._initial_parts = normalize_collection_arguments(collection_args) + else: + self._initial_parts = collection_args + + for collection_argument in self._initial_parts: + initialpaths.append(collection_argument.path) + initialpaths_with_parents.append(collection_argument.path) + initialpaths_with_parents.extend(collection_argument.path.parents) + self._initialpaths = frozenset(initialpaths) + self._initialpaths_with_parents = frozenset(initialpaths_with_parents) + + rep = collect_one_node(self) + self.ihook.pytest_collectreport(report=rep) + self.trace.root.indent -= 1 + if self._notfound: + errors = [] + for arg, collectors in self._notfound: + if collectors: + errors.append( + f"not found: {arg}\n(no match in any of {collectors!r})" + ) + else: + errors.append(f"found no collectors for {arg}") + + raise UsageError(*errors) + + if not genitems: + items = rep.result + else: + if rep.passed: + for node in rep.result: + self.items.extend(self.genitems(node)) + + self.config.pluginmanager.check_pending() + hook.pytest_collection_modifyitems( + session=self, config=self.config, items=items + ) + finally: + self._notfound = [] + self._initial_parts = [] + self._collection_cache = {} + hook.pytest_collection_finish(session=self) + + if genitems: + self.testscollected = len(items) + + return items + + def _collect_one_node( + self, + node: nodes.Collector, + handle_dupes: bool = True, + ) -> tuple[CollectReport, bool]: + if node in self._collection_cache and handle_dupes: + rep = self._collection_cache[node] + return rep, True + else: + rep = collect_one_node(node) + self._collection_cache[node] = rep + return rep, False + + def collect(self) -> Iterator[nodes.Item | nodes.Collector]: + # This is a cache for the root directories of the initial paths. + # We can't use collection_cache for Session because of its special + # role as the bootstrapping collector. + path_cache: dict[Path, Sequence[nodes.Collector]] = {} + + pm = self.config.pluginmanager + + for collection_argument in self._initial_parts: + self.trace("processing argument", collection_argument) + self.trace.root.indent += 1 + + argpath = collection_argument.path + names = collection_argument.parts + parametrization = collection_argument.parametrization + module_name = collection_argument.module_name + + # resolve_collection_argument() ensures this. + if argpath.is_dir(): + assert not names, f"invalid arg {(argpath, names)!r}" + + paths = [argpath] + # Add relevant parents of the path, from the root, e.g. + # /a/b/c.py -> [/, /a, /a/b, /a/b/c.py] + if module_name is None: + # Paths outside of the confcutdir should not be considered. + for path in argpath.parents: + if not pm._is_in_confcutdir(path): + break + paths.insert(0, path) + else: + # For --pyargs arguments, only consider paths matching the module + # name. Paths beyond the package hierarchy are not included. + module_name_parts = module_name.split(".") + for i, path in enumerate(argpath.parents, 2): + if i > len(module_name_parts) or path.stem != module_name_parts[-i]: + break + paths.insert(0, path) + + # Start going over the parts from the root, collecting each level + # and discarding all nodes which don't match the level's part. + any_matched_in_initial_part = False + notfound_collectors = [] + work: list[tuple[nodes.Collector | nodes.Item, list[Path | str]]] = [ + (self, [*paths, *names]) + ] + while work: + matchnode, matchparts = work.pop() + + # Pop'd all of the parts, this is a match. + if not matchparts: + yield matchnode + any_matched_in_initial_part = True + continue + + # Should have been matched by now, discard. + if not isinstance(matchnode, nodes.Collector): + continue + + # Collect this level of matching. + # Collecting Session (self) is done directly to avoid endless + # recursion to this function. + subnodes: Sequence[nodes.Collector | nodes.Item] + if isinstance(matchnode, Session): + assert isinstance(matchparts[0], Path) + subnodes = matchnode._collect_path(matchparts[0], path_cache) + else: + # For backward compat, files given directly multiple + # times on the command line should not be deduplicated. + handle_dupes = not ( + len(matchparts) == 1 + and isinstance(matchparts[0], Path) + and matchparts[0].is_file() + ) + rep, duplicate = self._collect_one_node(matchnode, handle_dupes) + if not duplicate and not rep.passed: + # Report collection failures here to avoid failing to + # run some test specified in the command line because + # the module could not be imported (#134). + matchnode.ihook.pytest_collectreport(report=rep) + if not rep.passed: + continue + subnodes = rep.result + + # Prune this level. + any_matched_in_collector = False + for node in reversed(subnodes): + # Path part e.g. `/a/b/` in `/a/b/test_file.py::TestIt::test_it`. + if isinstance(matchparts[0], Path): + is_match = node.path == matchparts[0] + if sys.platform == "win32" and not is_match: + # In case the file paths do not match, fallback to samefile() to + # account for short-paths on Windows (#11895). But use a version + # which doesn't resolve symlinks, otherwise we might match the + # same file more than once (#12039). + is_match = samefile_nofollow(node.path, matchparts[0]) + + # Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`. + else: + if len(matchparts) == 1: + # This the last part, one parametrization goes. + if parametrization is not None: + # A parametrized arg must match exactly. + is_match = node.name == matchparts[0] + parametrization + else: + # A non-parameterized arg matches all parametrizations (if any). + # TODO: Remove the hacky split once the collection structure + # contains parametrization. + is_match = node.name.split("[")[0] == matchparts[0] + else: + is_match = node.name == matchparts[0] + if is_match: + work.append((node, matchparts[1:])) + any_matched_in_collector = True + + if not any_matched_in_collector: + notfound_collectors.append(matchnode) + + if not any_matched_in_initial_part: + report_arg = "::".join((str(argpath), *names)) + self._notfound.append((report_arg, notfound_collectors)) + + self.trace.root.indent -= 1 + + def genitems(self, node: nodes.Item | nodes.Collector) -> Iterator[nodes.Item]: + self.trace("genitems", node) + if isinstance(node, nodes.Item): + node.ihook.pytest_itemcollected(item=node) + yield node + else: + assert isinstance(node, nodes.Collector) + # For backward compat, dedup only applies to files. + handle_dupes = not isinstance(node, nodes.File) + rep, duplicate = self._collect_one_node(node, handle_dupes) + if rep.passed: + for subnode in rep.result: + yield from self.genitems(subnode) + if not duplicate: + node.ihook.pytest_collectreport(report=rep) + + +def search_pypath( + module_name: str, *, consider_namespace_packages: bool = False +) -> str | None: + """Search sys.path for the given a dotted module name, and return its file + system path if found.""" + try: + spec = importlib.util.find_spec(module_name) + # AttributeError: looks like package module, but actually filename + # ImportError: module does not exist + # ValueError: not a module name + except (AttributeError, ImportError, ValueError): + return None + + if spec is None: + return None + + if ( + spec.submodule_search_locations is None + or len(spec.submodule_search_locations) == 0 + ): + # Must be a simple module. + return spec.origin + + if consider_namespace_packages: + # If submodule_search_locations is set, it's a package (regular or namespace). + # Typically there is a single entry, but documentation claims it can be empty too + # (e.g. if the package has no physical location). + return spec.submodule_search_locations[0] + + if spec.origin is None: + # This is only the case for namespace packages + return None + + return os.path.dirname(spec.origin) + + +@dataclasses.dataclass(frozen=True) +class CollectionArgument: + """A resolved collection argument.""" + + path: Path + parts: Sequence[str] + parametrization: str | None + module_name: str | None + original_index: int + + +def resolve_collection_argument( + invocation_path: Path, + arg: str, + arg_index: int, + *, + as_pypath: bool = False, + consider_namespace_packages: bool = False, +) -> CollectionArgument: + """Parse path arguments optionally containing selection parts and return (fspath, names). + + Command-line arguments can point to files and/or directories, and optionally contain + parts for specific tests selection, for example: + + "pkg/tests/test_foo.py::TestClass::test_foo" + + This function ensures the path exists, and returns a resolved `CollectionArgument`: + + CollectionArgument( + path=Path("/full/path/to/pkg/tests/test_foo.py"), + parts=["TestClass", "test_foo"], + module_name=None, + ) + + When as_pypath is True, expects that the command-line argument actually contains + module paths instead of file-system paths: + + "pkg.tests.test_foo::TestClass::test_foo[a,b]" + + In which case we search sys.path for a matching module, and then return the *path* to the + found module, which may look like this: + + CollectionArgument( + path=Path("/home/u/myvenv/lib/site-packages/pkg/tests/test_foo.py"), + parts=["TestClass", "test_foo"], + parametrization="[a,b]", + module_name="pkg.tests.test_foo", + ) + + If the path doesn't exist, raise UsageError. + If the path is a directory and selection parts are present, raise UsageError. + """ + base, squacket, rest = arg.partition("[") + strpath, *parts = base.split("::") + if squacket and not parts: + raise UsageError(f"path cannot contain [] parametrization: {arg}") + parametrization = f"{squacket}{rest}" if squacket else None + module_name = None + if as_pypath: + pyarg_strpath = search_pypath( + strpath, consider_namespace_packages=consider_namespace_packages + ) + if pyarg_strpath is not None: + module_name = strpath + strpath = pyarg_strpath + fspath = invocation_path / strpath + fspath = absolutepath(fspath) + if not safe_exists(fspath): + msg = ( + "module or package not found: {arg} (missing __init__.py?)" + if as_pypath + else "file or directory not found: {arg}" + ) + raise UsageError(msg.format(arg=arg)) + if parts and fspath.is_dir(): + msg = ( + "package argument cannot contain :: selection parts: {arg}" + if as_pypath + else "directory argument cannot contain :: selection parts: {arg}" + ) + raise UsageError(msg.format(arg=arg)) + return CollectionArgument( + path=fspath, + parts=parts, + parametrization=parametrization, + module_name=module_name, + original_index=arg_index, + ) + + +def is_collection_argument_subsumed_by( + arg: CollectionArgument, by: CollectionArgument +) -> bool: + """Check if `arg` is subsumed (contained) by `by`.""" + # First check path subsumption. + if by.path != arg.path: + # `by` subsumes `arg` if `by` is a parent directory of `arg` and has no + # parts (collects everything in that directory). + if not by.parts: + return arg.path.is_relative_to(by.path) + return False + # Paths are equal, check parts. + # For example: ("TestClass",) is a prefix of ("TestClass", "test_method"). + if len(by.parts) > len(arg.parts) or arg.parts[: len(by.parts)] != by.parts: + return False + # Paths and parts are equal, check parametrization. + # A `by` without parametrization (None) matches everything, e.g. + # `pytest x.py::test_it` matches `x.py::test_it[0]`. Otherwise must be + # exactly equal. + if by.parametrization is not None and by.parametrization != arg.parametrization: + return False + return True + + +def normalize_collection_arguments( + collection_args: Sequence[CollectionArgument], +) -> list[CollectionArgument]: + """Normalize collection arguments to eliminate overlapping paths and parts. + + Detects when collection arguments overlap in either paths or parts and only + keeps the shorter prefix, or the earliest argument if duplicate, preserving + order. The result is prefix-free. + """ + # A quadratic algorithm is not acceptable since large inputs are possible. + # So this uses an O(n*log(n)) algorithm which takes advantage of the + # property that after sorting, a collection argument will immediately + # precede collection arguments it subsumes. An O(n) algorithm is not worth + # it. + collection_args_sorted = sorted( + collection_args, + key=lambda arg: (arg.path, arg.parts, arg.parametrization or ""), + ) + normalized: list[CollectionArgument] = [] + last_kept = None + for arg in collection_args_sorted: + if last_kept is None or not is_collection_argument_subsumed_by(arg, last_kept): + normalized.append(arg) + last_kept = arg + normalized.sort(key=lambda arg: arg.original_index) + return normalized diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/mark/__init__.py b/Backend/venv/lib/python3.12/site-packages/_pytest/mark/__init__.py new file mode 100644 index 00000000..841d7811 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/mark/__init__.py @@ -0,0 +1,301 @@ +"""Generic mechanism for marking and selecting python functions.""" + +from __future__ import annotations + +import collections +from collections.abc import Collection +from collections.abc import Iterable +from collections.abc import Set as AbstractSet +import dataclasses +from typing import TYPE_CHECKING + +from .expression import Expression +from .structures import _HiddenParam +from .structures import EMPTY_PARAMETERSET_OPTION +from .structures import get_empty_parameterset_mark +from .structures import HIDDEN_PARAM +from .structures import Mark +from .structures import MARK_GEN +from .structures import MarkDecorator +from .structures import MarkGenerator +from .structures import ParameterSet +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config import hookimpl +from _pytest.config import UsageError +from _pytest.config.argparsing import NOT_SET +from _pytest.config.argparsing import Parser +from _pytest.stash import StashKey + + +if TYPE_CHECKING: + from _pytest.nodes import Item + + +__all__ = [ + "HIDDEN_PARAM", + "MARK_GEN", + "Mark", + "MarkDecorator", + "MarkGenerator", + "ParameterSet", + "get_empty_parameterset_mark", +] + + +old_mark_config_key = StashKey[Config | None]() + + +def param( + *values: object, + marks: MarkDecorator | Collection[MarkDecorator | Mark] = (), + id: str | _HiddenParam | None = None, +) -> ParameterSet: + """Specify a parameter in `pytest.mark.parametrize`_ calls or + :ref:`parametrized fixtures `. + + .. code-block:: python + + @pytest.mark.parametrize( + "test_input,expected", + [ + ("3+5", 8), + pytest.param("6*9", 42, marks=pytest.mark.xfail), + ], + ) + def test_eval(test_input, expected): + assert eval(test_input) == expected + + :param values: Variable args of the values of the parameter set, in order. + + :param marks: + A single mark or a list of marks to be applied to this parameter set. + + :ref:`pytest.mark.usefixtures ` cannot be added via this parameter. + + :type id: str | Literal[pytest.HIDDEN_PARAM] | None + :param id: + The id to attribute to this parameter set. + + .. versionadded:: 8.4 + :ref:`hidden-param` means to hide the parameter set + from the test name. Can only be used at most 1 time, as + test names need to be unique. + """ + return ParameterSet.param(*values, marks=marks, id=id) + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("general") + group._addoption( # private to use reserved lower-case short option + "-k", + action="store", + dest="keyword", + default="", + metavar="EXPRESSION", + help="Only run tests which match the given substring expression. " + "An expression is a Python evaluable expression " + "where all names are substring-matched against test names " + "and their parent classes. Example: -k 'test_method or test_" + "other' matches all test functions and classes whose name " + "contains 'test_method' or 'test_other', while -k 'not test_method' " + "matches those that don't contain 'test_method' in their names. " + "-k 'not test_method and not test_other' will eliminate the matches. " + "Additionally keywords are matched to classes and functions " + "containing extra names in their 'extra_keyword_matches' set, " + "as well as functions which have names assigned directly to them. " + "The matching is case-insensitive.", + ) + + group._addoption( # private to use reserved lower-case short option + "-m", + action="store", + dest="markexpr", + default="", + metavar="MARKEXPR", + help="Only run tests matching given mark expression. " + "For example: -m 'mark1 and not mark2'.", + ) + + group.addoption( + "--markers", + action="store_true", + help="show markers (builtin, plugin and per-project ones).", + ) + + parser.addini("markers", "Register new markers for test functions", "linelist") + parser.addini(EMPTY_PARAMETERSET_OPTION, "Default marker for empty parametersets") + + +@hookimpl(tryfirst=True) +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: + import _pytest.config + + if config.option.markers: + config._do_configure() + tw = _pytest.config.create_terminal_writer(config) + for line in config.getini("markers"): + parts = line.split(":", 1) + name = parts[0] + rest = parts[1] if len(parts) == 2 else "" + tw.write(f"@pytest.mark.{name}:", bold=True) + tw.line(rest) + tw.line() + config._ensure_unconfigure() + return 0 + + return None + + +@dataclasses.dataclass +class KeywordMatcher: + """A matcher for keywords. + + Given a list of names, matches any substring of one of these names. The + string inclusion check is case-insensitive. + + Will match on the name of colitem, including the names of its parents. + Only matches names of items which are either a :class:`Class` or a + :class:`Function`. + + Additionally, matches on names in the 'extra_keyword_matches' set of + any item, as well as names directly assigned to test functions. + """ + + __slots__ = ("_names",) + + _names: AbstractSet[str] + + @classmethod + def from_item(cls, item: Item) -> KeywordMatcher: + mapped_names = set() + + # Add the names of the current item and any parent items, + # except the Session and root Directory's which are not + # interesting for matching. + import pytest + + for node in item.listchain(): + if isinstance(node, pytest.Session): + continue + if isinstance(node, pytest.Directory) and isinstance( + node.parent, pytest.Session + ): + continue + mapped_names.add(node.name) + + # Add the names added as extra keywords to current or parent items. + mapped_names.update(item.listextrakeywords()) + + # Add the names attached to the current function through direct assignment. + function_obj = getattr(item, "function", None) + if function_obj: + mapped_names.update(function_obj.__dict__) + + # Add the markers to the keywords as we no longer handle them correctly. + mapped_names.update(mark.name for mark in item.iter_markers()) + + return cls(mapped_names) + + def __call__(self, subname: str, /, **kwargs: str | int | bool | None) -> bool: + if kwargs: + raise UsageError("Keyword expressions do not support call parameters.") + subname = subname.lower() + return any(subname in name.lower() for name in self._names) + + +def deselect_by_keyword(items: list[Item], config: Config) -> None: + keywordexpr = config.option.keyword.lstrip() + if not keywordexpr: + return + + expr = _parse_expression(keywordexpr, "Wrong expression passed to '-k'") + + remaining = [] + deselected = [] + for colitem in items: + if not expr.evaluate(KeywordMatcher.from_item(colitem)): + deselected.append(colitem) + else: + remaining.append(colitem) + + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = remaining + + +@dataclasses.dataclass +class MarkMatcher: + """A matcher for markers which are present. + + Tries to match on any marker names, attached to the given colitem. + """ + + __slots__ = ("own_mark_name_mapping",) + + own_mark_name_mapping: dict[str, list[Mark]] + + @classmethod + def from_markers(cls, markers: Iterable[Mark]) -> MarkMatcher: + mark_name_mapping = collections.defaultdict(list) + for mark in markers: + mark_name_mapping[mark.name].append(mark) + return cls(mark_name_mapping) + + def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool: + if not (matches := self.own_mark_name_mapping.get(name, [])): + return False + + for mark in matches: # pylint: disable=consider-using-any-or-all + if all(mark.kwargs.get(k, NOT_SET) == v for k, v in kwargs.items()): + return True + return False + + +def deselect_by_mark(items: list[Item], config: Config) -> None: + matchexpr = config.option.markexpr + if not matchexpr: + return + + expr = _parse_expression(matchexpr, "Wrong expression passed to '-m'") + remaining: list[Item] = [] + deselected: list[Item] = [] + for item in items: + if expr.evaluate(MarkMatcher.from_markers(item.iter_markers())): + remaining.append(item) + else: + deselected.append(item) + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = remaining + + +def _parse_expression(expr: str, exc_message: str) -> Expression: + try: + return Expression.compile(expr) + except SyntaxError as e: + raise UsageError( + f"{exc_message}: {e.text}: at column {e.offset}: {e.msg}" + ) from None + + +def pytest_collection_modifyitems(items: list[Item], config: Config) -> None: + deselect_by_keyword(items, config) + deselect_by_mark(items, config) + + +def pytest_configure(config: Config) -> None: + config.stash[old_mark_config_key] = MARK_GEN._config + MARK_GEN._config = config + + empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION) + + if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""): + raise UsageError( + f"{EMPTY_PARAMETERSET_OPTION!s} must be one of skip, xfail or fail_at_collect" + f" but it is {empty_parameterset!r}" + ) + + +def pytest_unconfigure(config: Config) -> None: + MARK_GEN._config = config.stash.get(old_mark_config_key, None) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ce9174e3e93756e823d68224c500ef748c2abae GIT binary patch literal 13083 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 OBYN&NiQ<5%ss0zBC5)T^ literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/expression.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/expression.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ad38629c2862d714145ddb3657038deff0f21c3 GIT binary patch literal 17367 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/structures.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/structures.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db9d7f6c298d1ba2ef055b372f3a1875f68cc125 GIT binary patch literal 27505 zcmcJ2d30M>df$84Hv%NUouc%(iG)OvmaN657Hgxd#iAFyYe}E=E#9-)@lgzZ4)1(IyIWrmc zboqVvEm#P7Jjv;kdEbZo?tZ`f-EX_s|D~+V!QuJQBhBIF0LT3e{m>qp0`M!Z8#(SK zCvqYm;fDAC&l7Bj7y?GTjS<6;DPUr8Ccx%^nZahjmVkx97QohkmBCiPwt$VnHo*3P zoxyg%j(~%~hKMuZWOcX#F7_@9l;Q1&xQ9Fe4@=_&TplQAunTZSpn}0=fV}}PgWZ6A z0Uv`sfGY!)3^qin0##zUSP`instME}u3Yp+YKQ6qbqw|at`F3+w3U&Dp>=_E46cea z4y_NYXK*#(4S@{|u89akO@Sr`*G8I$S^_N$u8Xt|wFTN3Tp!su)E;Q(ImstmP+vo& zW5^%yYOrl@fK47TBcWFn`%5fiU(((j$mad7Fz8R$pT1vgtU1F=)`4*qoo5SLkYnH%c zT8!8$cA9E4Bly~b(LKmY-HKATNxSs^?q;!%B6fRT>>d`o1F<{vV)wGx zU5I@wFLob`eH^hpd9hEh*xiWTlNY<6Mi+Cs7xDYn^z8sf>;Ojj32l@E2i5fZk^X>W zmkvoDWhAxs>*I2G)Zl;e$213iKI05Uqp?IN5spRUefVuV6pBPb=OYqe$DvpxBK6ZZ z`f@BG$?6vcPQ+xyTTh0Dhr`i9fbNqc2}+TRv7M5BZA6OpQ*p)v(NVn21L0^W0>IH1 z8j{4*Bg3pRTkNtVM`9rnU&>Cu_)Kr`(9zyQCyw-(S#IF;G6s8f#**2 z9u6Kk_A95Kf36oXrh!nH+T}iQKAw<6{fXy7;kXn>j3aoeKQ=5;d-1FG8CJ&g(M!?T zmFUTkeCaD8IjW3+sV^oA&oQBg^c^T9>p;9i68^it~8Z>+s z4VQLt+$qj)?2`qQk+kiJgoe(Gp?!Ws#@4T*amET%2_+JQsiuLEXn&k466B2tGmdba zuz{lG&4}2Jzc_Vdf?IO>rVf8|->1%J-x{0|XOG`?%&$N6adK|`vnkWFtV@|Pn)<_1 ze@qTxCG_jfM1JZ%>)fxRHf(%oHf?<4Mi6V`Me2IdGsbJ39ycW@H+#kmTI@@h{&DlT zMKq0>-sD8{+s1M07T8m!e6E#|zFw`sfP9t;j5oCA_fAK#@Fv0!A!kL?wz127UH~iB_=e=zcCw}N{oEGQ3 zEw7%KI5b(E_H@pknLT;8B2~6~;z+ux;pW*JXBS%b%vbH5=u2CxQgxf>teclSHE)bx zAD=lgIX>^%JZZ{$Iam3_iEIVusGZ*RslAyJymI}OnHTOTY-!T7u5GqF2?k1gM+_{4E{EaiG?#c1$2vYf&0ShaDU`suED zw?Ad`vo+t3?&yz6f%ugSfE@jP9?KnktpVJOk!YJ}0%T^81(1~>$t>DLyXbh!pz$Kg z8=Q#qnnmBKEt1*Ji50mtqE~cXGwa{2k}dZQDScwuHIx3;Ui7t6bmx6_P}yt$iB+N} zFO_rD?yt_c*cL=P8TQuDRZzAAY$3%4gv+4_@P#d@kQS$}wzv?A#G-@3m2l#M5Q@iR z{oxQ$OJGPPgrcH@kSY`vV#CB_gd)PR!>p*p1&Ipla5Q%5Tb~Y(!nG zs*bIl!nRJ~Q9nyfUt5Iy7I!F(+^eVSR9djN!`~@{#l3y8sN{c0(ajg6NF>(G(lrmt z5-r)m=3iKomJJoK&T3WV7g4IUYzWzKDGQG}Lr+0>x1+O4scJO+=wf8+7d8xeb8R}A zzkzjhb#?jaTO4J2qWHno&xFWFj!DYZ1#5+q&ZXYi{;YLpfDkQDjdE?7`o6XqN~jk_)hRU zY+4I*p3VKtb>JU^TGRx39+G#v)}Y+XDAQ2THrC%{qsCC5iJiZQff2@p9Lsw)V^L5& zDdbr9GX=AZ^z{2|#!N^Pe^3W54srRcCMz;3Wej0aZUwRGyKRt<<1IghK&AOo3DT zCi2&~G5#>8(Z3IA_r)VS6J-hnTC6ivukmAsSGVQ0#E@`nsq`9}#|#N#&e=1<%k>GJ zkr9oFD(y>-O#|~{8Z*3U$Y)y&;9JLxW2jH#T*0%N`}BdNQiyj1x52OU064+%*iOc{ zv#4yGALB1lilRs4h`0J({wB|JuUDD4QT|^Vukc`eog;B+Abgdv<$d64Otdf&Z|Hg? z!-wNED}uuPKCtf@l%iN(^1ftSKDktgYS*4f3`}%2} z{@NRtuV0>S{LHg)&b{;JahlE7>KgduSpJd|`k9m|ghWwWiiCY#d2QVd$|ngI5{cn> zPj|N%>nEXtnDeffJlHKoA#IRk*)2;0l1u_+4(hs)7>cwgsUfOBSV|u5RjCG6xR8hm z=OqEVQ$off7r^|5gmY@)M~@vo+}o${Ugyw+D9F1M=@f`{Od5IwzmtT7a1^zpPMKtz zj8kO-No39#L+ATtV(0-yM>CEWXg_3(SjLD7Gp_s^GIkP7m9M4*$*87`u|E>eEid^P ziejTk^g`jZ%u1iUYv(9NiQhpm!L5SPsh*PFI=kT9oUW>$I(ohLhrY&X>HRYw1m6uV zwmmV|_QZm3f6BE#<=emHbYr&em>1kT7o0nnyz8z!6mNS;QvkuPXp4_*D z63)Nhvf$e|yX}u(n%jI}sd@vFRy8a&Y?v{;`PlTPnS(R(%s|%2S9KwGjTgBqy!UKe zxo>Lc^p2V4c~4u))~4`Igyh?j=aek=stFBCtOtl3tOaO#zD(jsk5%0+{zW zZKt$c9^P08H2qSL)lt~i-YEMduulRjo0dR4CIF`g(RxS&BSXM?HHET!6m(aNMcWes zn0-Nns5>+qi=%ba_VbXJE(x?mF?`e^divy(m>kGXq=J;s5p0x$cx7B{T#1Loh-RF5 zO^(ylBCmq&E=ifJAWc@Xt^n!tKTRT290~x!eSvU4GIMCb*^%~bNVztA&sm-JRo{I4 z#^cj3&HFZ`T$}E>xyqWUvDs~RS{Hn~Qm$PJPU^c93L>0@Sj?|{7eE0{awT@Nr-73t z1bI;xy=-e;)db%HWN3Fk`lVupVO5< z8-fjC+>E`(JYYBlHA4>WH}Gi1xakV#H}yT>J7pTSBYv|oNoVjQV~!6;!U?=UBZo7_ zh!pi(Nz;B2cNe4#$<~6o zd`PPIsmMzRG?I4VedB_&HNE-KZ|}Rk?{3Td=6&C-d1@x~anoP4f71RR{nPwA)?3!M z?WvllCXXUms&AaW@aC4;t#kFAlP9tkuDo`-A?0aJ*;=zku0fbQk@nPRF+aa==W0*# zOul`;>cB?RpL!AW=Ocx#$svTMT^ZAAP^T4J;kY4zc;zWcqCmnv;x&SXDQqy&RA7SC zNikqj3{bqXvBbJLin3%80oDy|{48D`0#vw68cU2{ZiOW`{i3nXrTrr+8_WkrJnQ0~ z;-BY#YBZEP(np?r{i#J)+nlRyCUoaa%GLIn>%e{b!ery0-{%0bo$BlBF=qM98)=!a zrumisz}P}86qOZ2y#6qTNF8aW-QveB4idrYbiZmLf6nGNYBKcfs(CEqV6%rATOFfvS_7h zlP+$(Koo6&A;PurmqGD)!LYZpNl33DJ?E^Afhx0aXy}c$o==K@gI0KkZ4% z?{UY_2;@VsV^A|e+{YeQm|LMVLdVMrke6SH826hq7NseSRf2Af6kHj*UR}nK%X$P1 zuoavhl=+fOtUDGJF<3c%%kPk*6fsOem;&~_7Es25J!&YFV9KnFg|_9WsH{&`l#}gi zj7TagbrWTOf&!TWB90kHZk@5+Fk>8w56Zpx6ek>GWK#(g;#nR!#yQ+TR ztXi&XxzAOURZp3heAQDUH^**_&HGwEGR_|Tux#G9BWp6&wSi_<)vmg@s=9P-!#j>! zj&zGZ-O};Fo_F`8n>Q_O^v`y@H?-8gX?DlA_uSqy-~L$E$c^Gyz9W-|)2`}8SL2+kajAUW(z@p9$KQ|7 zKK9=DT*LOer|%y5`0A?BP~lxQniyxAD~9diew%^`7KN8%bDMmGgj`)W4~g~{$-_*z zl}}(KNf8Y3Daxk$Hh%;g;dw+SJMt$2B^C+dXAz~gfX%$=d@L4`UqMb*kh+~d@G0`P zo*6&SGW2<{O|!3^`LY$AWyL4!8FzQ4pvX>vsy~Zb*(UNDHG`TZPf$RuN_t6EQ&87g ztcLyc{Z$Hb85vHKw5uRCF$_u(9VKgCTkcuEOBN_s69vr(GS*-aLLMwvo?tM~1c?Y| ze*~&R*v&4)M7Czwc9?NN1WSbb)%e_^9it*!DQKf$BL!>|rQm7&#kXVk`8s#cP-nIQ z3ClLkoBV0#=6TcRw5@60)U<3a&)T>{d@rBw=)7mIHg8<6sL7h~ChlQBpS2Qb z=_5oVNA-OtpcNx$smK~A?h%4)Tw`_=3MlFtY_pN}`)D&0F zn&vmRE~9KS-bht>1brdU!J&grf?QlhWwwkUH|N-P-vfv>@SXvXGN3e!WLdRFP<|c1+1bII&&B4%)ev~n^=Y%ZpuO0ZRo`9 zq9@-ApjhWER*L1YU{uOjMQI>35=mrCvNS9|a8d;z9i)TC zT438nFf7KI`17E$U{PzyD)W+&3J3N_NyJuX*X`RnJ4TEbF&IW-$2j~dkZUZt%|0jL zl#*)5@(O7irO*SLtjf4SYe%IQJxFcHX}i8eYGaP6`4XuiN&ONnF^`#ZsYfk->jQTW zh)75nmcy4Za6K|jB%3!jN%9-?^9F)Ga0}J+V;n)o86+z{S(1Z6zZF(PDKb!$HRCRv zzKk#TTc0QSTPTg$$JNbW+4y1Bhb=G>3?`Xbxl8-`zbG{^CXk1!IeX32=z^{3hc@@3 zt$xl{KfQgXdbV=G*0uD7F}1Th7i`<|(xjTU-RWDf9V&?0m3sWC&uk|Z!I%(7duZAj z0?|m3H5+`o_AT#ynY+rzYYPqb7gZ9GmrS=yLu7qPHQ;BAK=~h2T8ud@6@?e0#dMpO zZ{n42ws!|FgyOk1ko3_S>MD$MqXY34WSrpAp2}&{JI-59@E)Ifdcaz2|MugzAAj$a zyA^ZoJt6mX?b2RHy?|sJ;wK*NE7B2~Dj5^_PfE;%lBX#9ee0H4r}s_%P&yQeWQZe-o;1 zCe5gAB6WB2`_D0^N(c#2=?XJJ2v;tI`!5J#7zqU^md?W~h>UZnj6jmY zDFkiW1qc~zwsYpXLo6?BkmpAQg-ItB7ik<66`LdvQiP(lSgYt`#j}EYgwv2@hKL?R zE>W5!G23NMzttm?+O~(aL2qGS#>p1C1m#ImIt?j)8bibuIv6lUUPwtRU#Ea@I%5*U z{RxKXz;{CLjF~loWsDO}pzjJ{C{r6E2FWa#^s?R;#KCKki~lC_Fd3k{@{Q5!qtm_b z9KUt^Q_sefZDYzqoJ{-XNq5@mdE@cxk589<>TJWRSoE~ad0G}cZRw_sH{9ups>O=d zxr)|>ijC=}wkh`yJ-)Q7{EcJRkEyR|Gf4n*uI43I`Bdf2h8qoYt}Qd>4_xoM0HhoI z>FWAbr-d{G7Q2J3D(E)){BC6o*se+4m%l*)Tjt*+h>aF?K)yx66a_>jbIfW6epOKk zb|}VAPvD#1B)>;V*qE|dg4Xbv^4BQQH3S)_7)pc~ZwYx3PH0j@%+Y%JpHn(EOYalZ zNYTYrjd$DEvTnogP z@0b@_`X-*9Ix}zTTi$x`PQ&8XgL7LCrpqg*cR;M!W=WZ9Q}tadoWX3#dNEDu?T=;4 zD$IR6n79VvUIl^6?ux9L#aTGd`mB|KHm;&MYiFQ?t82|V8R+8HZ`1Qv)XKkTV9L}F<{X0V(cuJBQM4Q2F*zfndAbC=F0n0hM39%mxnSi zZ*X!5l#P1)HOyKQ`U%@gJZYz9&`YTjvNGKXaj3LNBx#~$GjPWor#6ZVM zBQyX%tJoDxk?QiXwM#fQpr%yaNi-7%n84ZTt=0~P_H#rl~NlFDB*-w7;GchYP8WDNyMNu3R9;> zDHGlba`lJcOp1+Ve-d1co09B2XrnQUztRr%_`w zq9|pESs(^tO;K7MnyFSn5^85m4ub)JvT;qGcHkcRMdq)OVeAg-GS-SJ>?Gkb5-^RW zinMC;;eP>+;54d|;f#wGo`)+l#0xlsZiq`lews9w`PhyGcv{M(JUL?+lwBZxn4{g;m9OjZ_Rgp%(98WSg<z11LDSEWR(=+#EKWvgA=8j{qQd3pjsff|P^g_dMrW6lsJB*!yuERYdP$bF@} z$)T8`3B<$7QqdoMHYWd-F3}YXwL*BAr9OX)z(EF4rI*AXJRgmOFM)f)E}HxLWtIIkxPzUCx*$If_8W5A7q+Wa@dCA#)Wk}b@=A78^@*-GcVjp zEO_=znwC84rcb}~;;k1K8@JCjZohMSzOm<6-AD#1mt@$dlI@Fc|U5QxfBKT!pg* z+#mAr-I&nvJz>+zpaW)_#Z`9UltKwT2TCc^(&yZbMCNQEwZCl(hW`d(2 z?K%t^9Qabh3yzk6=(yzHO#B6ak)&odr;q z$R8sXN{yl=vWevn$AhfkjC&-i`Z2K*<23lR_*B&VLj3r3jANXPIuqOvoSs{jw6A8G zzp-ob@KVkCn_s>0)tSV6P3PoM?2YT2C;OH>b$2dlr!yVoJ`REI-N*)x@IoV zdAd@zE{2CmrFqqeqsmPrr z2==Xq#|e5mK~Gnw$JMRatmdcqY%ME+Hg{dLn2bI{%0|;YK9D6U;Df93;Oo16*Ntpfq!iJdT-j2e!19%3k=F=>#j+%^c~1 z+esVJ=%ni*X?B^oXiLr$nruTD50#XSTk=|ADbWh!m}T4)MTn6z<%nyZH?@A`OB*%a zS;|PxQX!n)=AJ%J;wYnnBT+h@2+n{wDiYY?f=QYgRT_mxS@-x<84zd^V5wI%&tz;? z)IE8)MBi|_5t64OcB*A-LViL~BrqXK5zv@^8e|#DYf?kRN3aaHuul*O(a@96Jq>9N zn5qc*c~vo92GtF~*BbRPX|QCaVpsIJx@crzVV&TTb+ybo95~_&F;DDov+`Ms@r@0_^O55eTe_~(ZAUJ z$?o}{r{Tg@?@sU7ImO>}+;H4<-*8Wl%|7>8`6F5SFnJt4Z1rx?m#Ugcd%AM{ea>WW z#}>nO6BB8~%j*`)g}HKJrt-7$jmrhU?%9zTJY9X$jBh4#=iq$Zu1WWjvuY~(zDVBJ zwHvP=Tdr$Q)pkrCTXK39%3EfR&6ju0JG)b+?w{Y^fSTEk_=&CMP?Jd~Tj(x3P|-p{ za}qzaa{yT0+pxU(yoCjhC})m^^+#QN@a8fkbb_KS{J1A~qLd7?Z^AnBw$3ZjK?wC6 z!FHS@FtKw4=H$;AUiN7?Kx+td%FcfilAi=hka~jNy8Nm+D3b<=oRrpx{$cmNmXdSw`_5;Q-Sa zY0V*tLG>gC)u0||4QJWdo}Vm4pk}V zUjULRN+4+90Ki_E?seHSA|MaZ%1$}glYD> z@k2SVP0Bx6A*rf5i#9&kKgvOs)FU*6WRl#+hV_X2pXj?iH}x5tcG5=v9G@~aI7X9- zG@daLB-bF4(KGokDW^$Zpa{lN{(FKbQ=wof{~5(o@Et-D(kOrg;xI<_l=#-+1!q&r z)IPI)-qfCQHvP!7`3KgTY2&_d$UA~+qFKs# zgQswanx;r6209jM)i5lisz`dxiIVc4ASz#QRiuN4wIxEWrwFcpg|S&7#D;BuR|)Gq z)%cLwz?w7B<w7k(dX}LdY4t~+NUAg2Gf9;rV17HDL-X*?@-!3MR2jCX~>p5{+XIHgbMt?NnkIXmB%ju;q$eg{mW zX1_h-3;qh-UQjTv`i!zhAb7}fdC1~iSE5LLb^bj@P5vRuWIfl{1|$@6tz}oLWGt95 zu%&u4RK!&dla@d%tFmfowv~CM(&YOTtWZF+zJ|S22+0?=HJgWAwK?RXWi5Xd0VHl# zCqt-GsKwebLCL8x$(P7Y0jNk$B4-Z5bk%#cUt(w5$vs%{6)f>h@f}2p!kcG`N>IAW zD${9<{NJhWRtnlE*hoQE3;vF}H-TRzs0m^8Cb*lb7!CJsV0QPmG?2Rrkqo)%qk`N@ zGD2>G9Gtg0>mYf3R(l}~Y!6HznonIk6X>JjK zRx#G&En^O~^C3_(BODBfZdo#Q!Dp0fcF6EufaG1c-@_ILC8AAUehXFLd=uT{(+_NC zPu{yEJyZ2u`;l0_NWsr2ppxW&Ku}t*)%@@VX*BKs=Am7sj&^Wf*-u5kN&&0xHG-(l zWF-+dAV`5fC~HabS>113MLvZZ4&W{&IvoyP93k@t$D4GAq!vGOhP+ZCZsrb=M)Hw&s;*17Z@TAm0I{n5Su@^< ztUV5gWC9&r&E~9=AeX`G%a#%3lGb z2iKi(^8cn@5+$Rd!Lh|JH8jwlb20@UD&qfWMX;!XF;Uk$zj6U-a+spr*dm$a^|-YK zw1u{O+}dK+Z*4Ivx3-8j#M#+b+@2yj8RP;~#vnKOnZuo_9JI*-$0YJ5w~l)KRmx3K zPb!|;5I%4fAa?}!i^b$oW-;zzilUx#1;c+%6H0|GkXF@Ww~7=U5{ev+R9Bb;WM@rU zR#jc<)hYQ-JR~8-*p(5hbozOaYhgai2FSB?NE6o@}m)6s2<5Ja_HT+WcBu~zu1;u5C`Gr-qslnBKd zxLsU2hvWaGaAR66sN6Kc<6B5mutG@FuGq?S1i_L}e8Ibb&+&RBz$40hP2NPw}tXVMi{h7{AQR zF?>NC#ZO6c;&VA=cfW8~Bj5J$Jo6Aiya zPFH?Txjfe*w8tQpy=@TPA9*xQ3q15m)4`sLUJc5<#d6k*inomk+ELjfdNl<9!c=*CNyz^1zhbmLYU==%Hx2#I`sQ69F9CG2xrv|##;9SmcYcLE;KRZsUm^L$f zFu93HBwWDBp+aCsNpbTdH2opaGv@vT=Q=DlD0ZZbmB<)=Jm7mC@EvrYqUxkA5vQ2v zvK^Q5KgFffNxOQtBd&96X=m!iPC>*?BBSt>8j_$2ZO`-fZhs=T=?tusbF*~xBy z(Ea_Gq7%(IM?Pnu4XM~>1?2!GVbB!O?S0AF^*mHf)oU;Xz5!|)6zq9w&-Z@W4 zdY$mjQ@5U4T-QCfuKSLCeqGOf&Ro$obr{LkH@_3O6__=?8JvA~uCaTuamQTajywHu z?YO&ju5s^Tw)TWcEy1s?Vr9o-H6SJ|Z8al>`x|?M; z%F@-FX1C97gneY|F&<&nF&?Issx9ftD!BIJ5NkJBWjo^j)F>> z#eK*?#_BS&?ViBds?u&@-X^5WTjy=9X;1UKtvOxQHE-)u!`kihw(ZNETgjT;vFg}l zewJ5wXjUrWbU*&|2YV?!{K^2r9Mi}y`Ex-RimR)|G6qWWW|i8e@i+8oEq_y$-y5C18KlW;MUBwol3V8Mb{rtNdFjR_fX;MX<2!Y? z>Shk7YW<}d9cnS_^XmX^;>;E{%>3r7@ECI*I>oi*omy;#k&|uBC3-!h#y96{!(AQ!fU7X?b4J8_d~dvb{pFOWIbB{w ziFV)Ez36M7^R>?!=X{-?x;nE~Bzq92Rri{^g1gauz-au$z$5I}XCB`OW~1E<0Bq$$ zj<${QBnznuet1SQ+)&Oq$t^ShX61SYDo!(vsb?!6(q*vG$`^9nh?gTD(510?(nR)- zss2xG^=o-xq*NL!10Wu_G-|LDiyaT+(L`>9#}&jl(a7>snMF@2B)JSkUERGG;fWP>k3wud0O{^g%hZ^%vrkUhU^)v=%kjs^I)i+g=PxfdwLKM-JT*T`74A*6O`-XU}(8jLMg#+vPeMSp{aicF-; zjt*9d2;ibkNMY{>>$dy)?v%3`yv{qjZ|z=e=$>on21P7up0Y0c>StO%*z)d{Pkr6# z`lfH4yk|u`t~znGAh0(*h@*4dYvl*3_}}6W_)Op01?Ur}9btjC(1cVBZAj~0h&I1Oxpyjlzu0h3A&95S=sB5tbo0xy)BQuC6xqZnOy8f`cAQHbB*h<6O@Z*vK2DFhhNDpYp#-jGX5Cw zpK0+GixQ+y<(7%F@|K5Jb4IU*apa1>YO;)#1yv;7RdE(bMLau><3|(npHpkfidb}9 zG7*GEFM$n9IWyZzHR69t!pWN`ATlj?Q?QMKofJGyK`T{Hnq9?4)j)9+(5@8!3gWME z%jI1Y$5u>6^QIMt#k^&uoHw_uxQw{Y#LeyJ5At-~Nz1Zl4|~IYfV)d@?FmU|INWLn zL^+;Vu~^N`D_(|-5%cLpFxYhqZ zM3GN$0n4aQ$1kF?MXB7N31*E4|^wU2jh-sy<(<(x$Of0cN92g!JE`*w4?76NGboZAHW`<(atob&sf`*TWA`#IP6IoI$x zCw$J;f1mUGh}*z^B5eAc+rs_>;Ae&O;Ox4o;&anFETYC;DXwnS!Sn8^?Q~)0DhCi2 zx$AZSz9Gvo_2Y&MtZN`%-+jp?}{0}SG!&?@U2rls~o+vKth8x z%~ww}Eb`TJeD$=rz_)aCA%it*qTlzPGH@>6RL{(>%=X{eIeTfY zt>^CPxwZof&Vv&+G}2I}6!bGgkmtLz(9QGRl)WaaL>xDA_3Q6h*Yl60?bTT$-pJ&u z$x;l+gQa}MPS8Q#&DW$oTe3#Hm(9KvGv3)|Zs)F_aBluMe{Um(Wz}RhALggeu5$EV zbz98B^!O@=*J^n=;GtCxuhmwkS(rJw%Hg$IZ!nh=jMu7-V5G+jqiC@H%y5+FpM@%g xy_7yHQTJXzr|V|6{L`oc)r{{scbJfZ*q literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/mark/expression.py b/Backend/venv/lib/python3.12/site-packages/_pytest/mark/expression.py new file mode 100644 index 00000000..3bdbd03c --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/mark/expression.py @@ -0,0 +1,353 @@ +r"""Evaluate match expressions, as used by `-k` and `-m`. + +The grammar is: + +expression: expr? EOF +expr: and_expr ('or' and_expr)* +and_expr: not_expr ('and' not_expr)* +not_expr: 'not' not_expr | '(' expr ')' | ident kwargs? + +ident: (\w|:|\+|-|\.|\[|\]|\\|/)+ +kwargs: ('(' name '=' value ( ', ' name '=' value )* ')') +name: a valid ident, but not a reserved keyword +value: (unescaped) string literal | (-)?[0-9]+ | 'False' | 'True' | 'None' + +The semantics are: + +- Empty expression evaluates to False. +- ident evaluates to True or False according to a provided matcher function. +- ident with parentheses and keyword arguments evaluates to True or False according to a provided matcher function. +- or/and/not evaluate according to the usual boolean semantics. +""" + +from __future__ import annotations + +import ast +from collections.abc import Iterator +from collections.abc import Mapping +from collections.abc import Sequence +import dataclasses +import enum +import keyword +import re +import types +from typing import Final +from typing import final +from typing import Literal +from typing import NoReturn +from typing import overload +from typing import Protocol + + +__all__ = [ + "Expression", + "ExpressionMatcher", +] + + +FILE_NAME: Final = "" + + +class TokenType(enum.Enum): + LPAREN = "left parenthesis" + RPAREN = "right parenthesis" + OR = "or" + AND = "and" + NOT = "not" + IDENT = "identifier" + EOF = "end of input" + EQUAL = "=" + STRING = "string literal" + COMMA = "," + + +@dataclasses.dataclass(frozen=True) +class Token: + __slots__ = ("pos", "type", "value") + type: TokenType + value: str + pos: int + + +class Scanner: + __slots__ = ("current", "input", "tokens") + + def __init__(self, input: str) -> None: + self.input = input + self.tokens = self.lex(input) + self.current = next(self.tokens) + + def lex(self, input: str) -> Iterator[Token]: + pos = 0 + while pos < len(input): + if input[pos] in (" ", "\t"): + pos += 1 + elif input[pos] == "(": + yield Token(TokenType.LPAREN, "(", pos) + pos += 1 + elif input[pos] == ")": + yield Token(TokenType.RPAREN, ")", pos) + pos += 1 + elif input[pos] == "=": + yield Token(TokenType.EQUAL, "=", pos) + pos += 1 + elif input[pos] == ",": + yield Token(TokenType.COMMA, ",", pos) + pos += 1 + elif (quote_char := input[pos]) in ("'", '"'): + end_quote_pos = input.find(quote_char, pos + 1) + if end_quote_pos == -1: + raise SyntaxError( + f'closing quote "{quote_char}" is missing', + (FILE_NAME, 1, pos + 1, input), + ) + value = input[pos : end_quote_pos + 1] + if (backslash_pos := input.find("\\")) != -1: + raise SyntaxError( + r'escaping with "\" not supported in marker expression', + (FILE_NAME, 1, backslash_pos + 1, input), + ) + yield Token(TokenType.STRING, value, pos) + pos += len(value) + else: + match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|/)+", input[pos:]) + if match: + value = match.group(0) + if value == "or": + yield Token(TokenType.OR, value, pos) + elif value == "and": + yield Token(TokenType.AND, value, pos) + elif value == "not": + yield Token(TokenType.NOT, value, pos) + else: + yield Token(TokenType.IDENT, value, pos) + pos += len(value) + else: + raise SyntaxError( + f'unexpected character "{input[pos]}"', + (FILE_NAME, 1, pos + 1, input), + ) + yield Token(TokenType.EOF, "", pos) + + @overload + def accept(self, type: TokenType, *, reject: Literal[True]) -> Token: ... + + @overload + def accept( + self, type: TokenType, *, reject: Literal[False] = False + ) -> Token | None: ... + + def accept(self, type: TokenType, *, reject: bool = False) -> Token | None: + if self.current.type is type: + token = self.current + if token.type is not TokenType.EOF: + self.current = next(self.tokens) + return token + if reject: + self.reject((type,)) + return None + + def reject(self, expected: Sequence[TokenType]) -> NoReturn: + raise SyntaxError( + "expected {}; got {}".format( + " OR ".join(type.value for type in expected), + self.current.type.value, + ), + (FILE_NAME, 1, self.current.pos + 1, self.input), + ) + + +# True, False and None are legal match expression identifiers, +# but illegal as Python identifiers. To fix this, this prefix +# is added to identifiers in the conversion to Python AST. +IDENT_PREFIX = "$" + + +def expression(s: Scanner) -> ast.Expression: + if s.accept(TokenType.EOF): + ret: ast.expr = ast.Constant(False) + else: + ret = expr(s) + s.accept(TokenType.EOF, reject=True) + return ast.fix_missing_locations(ast.Expression(ret)) + + +def expr(s: Scanner) -> ast.expr: + ret = and_expr(s) + while s.accept(TokenType.OR): + rhs = and_expr(s) + ret = ast.BoolOp(ast.Or(), [ret, rhs]) + return ret + + +def and_expr(s: Scanner) -> ast.expr: + ret = not_expr(s) + while s.accept(TokenType.AND): + rhs = not_expr(s) + ret = ast.BoolOp(ast.And(), [ret, rhs]) + return ret + + +def not_expr(s: Scanner) -> ast.expr: + if s.accept(TokenType.NOT): + return ast.UnaryOp(ast.Not(), not_expr(s)) + if s.accept(TokenType.LPAREN): + ret = expr(s) + s.accept(TokenType.RPAREN, reject=True) + return ret + ident = s.accept(TokenType.IDENT) + if ident: + name = ast.Name(IDENT_PREFIX + ident.value, ast.Load()) + if s.accept(TokenType.LPAREN): + ret = ast.Call(func=name, args=[], keywords=all_kwargs(s)) + s.accept(TokenType.RPAREN, reject=True) + else: + ret = name + return ret + + s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT)) + + +BUILTIN_MATCHERS = {"True": True, "False": False, "None": None} + + +def single_kwarg(s: Scanner) -> ast.keyword: + keyword_name = s.accept(TokenType.IDENT, reject=True) + if not keyword_name.value.isidentifier(): + raise SyntaxError( + f"not a valid python identifier {keyword_name.value}", + (FILE_NAME, 1, keyword_name.pos + 1, s.input), + ) + if keyword.iskeyword(keyword_name.value): + raise SyntaxError( + f"unexpected reserved python keyword `{keyword_name.value}`", + (FILE_NAME, 1, keyword_name.pos + 1, s.input), + ) + s.accept(TokenType.EQUAL, reject=True) + + if value_token := s.accept(TokenType.STRING): + value: str | int | bool | None = value_token.value[1:-1] # strip quotes + else: + value_token = s.accept(TokenType.IDENT, reject=True) + if (number := value_token.value).isdigit() or ( + number.startswith("-") and number[1:].isdigit() + ): + value = int(number) + elif value_token.value in BUILTIN_MATCHERS: + value = BUILTIN_MATCHERS[value_token.value] + else: + raise SyntaxError( + f'unexpected character/s "{value_token.value}"', + (FILE_NAME, 1, value_token.pos + 1, s.input), + ) + + ret = ast.keyword(keyword_name.value, ast.Constant(value)) + return ret + + +def all_kwargs(s: Scanner) -> list[ast.keyword]: + ret = [single_kwarg(s)] + while s.accept(TokenType.COMMA): + ret.append(single_kwarg(s)) + return ret + + +class ExpressionMatcher(Protocol): + """A callable which, given an identifier and optional kwargs, should return + whether it matches in an :class:`Expression` evaluation. + + Should be prepared to handle arbitrary strings as input. + + If no kwargs are provided, the expression of the form `foo`. + If kwargs are provided, the expression is of the form `foo(1, b=True, "s")`. + + If the expression is not supported (e.g. don't want to accept the kwargs + syntax variant), should raise :class:`~pytest.UsageError`. + + Example:: + + def matcher(name: str, /, **kwargs: str | int | bool | None) -> bool: + # Match `cat`. + if name == "cat" and not kwargs: + return True + # Match `dog(barks=True)`. + if name == "dog" and kwargs == {"barks": False}: + return True + return False + """ + + def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool: ... + + +@dataclasses.dataclass +class MatcherNameAdapter: + matcher: ExpressionMatcher + name: str + + def __bool__(self) -> bool: + return self.matcher(self.name) + + def __call__(self, **kwargs: str | int | bool | None) -> bool: + return self.matcher(self.name, **kwargs) + + +class MatcherAdapter(Mapping[str, MatcherNameAdapter]): + """Adapts a matcher function to a locals mapping as required by eval().""" + + def __init__(self, matcher: ExpressionMatcher) -> None: + self.matcher = matcher + + def __getitem__(self, key: str) -> MatcherNameAdapter: + return MatcherNameAdapter(matcher=self.matcher, name=key[len(IDENT_PREFIX) :]) + + def __iter__(self) -> Iterator[str]: + raise NotImplementedError() + + def __len__(self) -> int: + raise NotImplementedError() + + +@final +class Expression: + """A compiled match expression as used by -k and -m. + + The expression can be evaluated against different matchers. + """ + + __slots__ = ("_code", "input") + + def __init__(self, input: str, code: types.CodeType) -> None: + #: The original input line, as a string. + self.input: Final = input + self._code: Final = code + + @classmethod + def compile(cls, input: str) -> Expression: + """Compile a match expression. + + :param input: The input expression - one line. + + :raises SyntaxError: If the expression is malformed. + """ + astexpr = expression(Scanner(input)) + code = compile( + astexpr, + filename="", + mode="eval", + ) + return Expression(input, code) + + def evaluate(self, matcher: ExpressionMatcher) -> bool: + """Evaluate the match expression. + + :param matcher: + A callback which determines whether an identifier matches or not. + See the :class:`ExpressionMatcher` protocol for details and example. + + :returns: Whether the expression matches or not. + + :raises UsageError: + If the matcher doesn't support the expression. Cannot happen if the + matcher supports all expressions. + """ + return bool(eval(self._code, {"__builtins__": {}}, MatcherAdapter(matcher))) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/mark/structures.py b/Backend/venv/lib/python3.12/site-packages/_pytest/mark/structures.py new file mode 100644 index 00000000..16bb6d81 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/mark/structures.py @@ -0,0 +1,664 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import collections.abc +from collections.abc import Callable +from collections.abc import Collection +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Mapping +from collections.abc import MutableMapping +from collections.abc import Sequence +import dataclasses +import enum +import inspect +from typing import Any +from typing import final +from typing import NamedTuple +from typing import overload +from typing import TYPE_CHECKING +from typing import TypeVar +import warnings + +from .._code import getfslineno +from ..compat import NOTSET +from ..compat import NotSetType +from _pytest.config import Config +from _pytest.deprecated import check_ispytest +from _pytest.deprecated import MARKED_FIXTURE +from _pytest.outcomes import fail +from _pytest.raises import AbstractRaises +from _pytest.scope import _ScopeName +from _pytest.warning_types import PytestUnknownMarkWarning + + +if TYPE_CHECKING: + from ..nodes import Node + + +EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" + + +# Singleton type for HIDDEN_PARAM, as described in: +# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions +class _HiddenParam(enum.Enum): + token = 0 + + +#: Can be used as a parameter set id to hide it from the test name. +HIDDEN_PARAM = _HiddenParam.token + + +def istestfunc(func) -> bool: + return callable(func) and getattr(func, "__name__", "") != "" + + +def get_empty_parameterset_mark( + config: Config, argnames: Sequence[str], func +) -> MarkDecorator: + from ..nodes import Collector + + argslisting = ", ".join(argnames) + + _fs, lineno = getfslineno(func) + reason = f"got empty parameter set for ({argslisting})" + requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) + if requested_mark in ("", None, "skip"): + mark = MARK_GEN.skip(reason=reason) + elif requested_mark == "xfail": + mark = MARK_GEN.xfail(reason=reason, run=False) + elif requested_mark == "fail_at_collect": + raise Collector.CollectError( + f"Empty parameter set in '{func.__name__}' at line {lineno + 1}" + ) + else: + raise LookupError(requested_mark) + return mark + + +class ParameterSet(NamedTuple): + """A set of values for a set of parameters along with associated marks and + an optional ID for the set. + + Examples:: + + pytest.param(1, 2, 3) + # ParameterSet(values=(1, 2, 3), marks=(), id=None) + + pytest.param("hello", id="greeting") + # ParameterSet(values=("hello",), marks=(), id="greeting") + + # Parameter set with marks + pytest.param(42, marks=pytest.mark.xfail) + # ParameterSet(values=(42,), marks=(MarkDecorator(...),), id=None) + + # From parametrize mark (parameter names + list of parameter sets) + pytest.mark.parametrize( + ("a", "b", "expected"), + [ + (1, 2, 3), + pytest.param(40, 2, 42, id="everything"), + ], + ) + # ParameterSet(values=(1, 2, 3), marks=(), id=None) + # ParameterSet(values=(40, 2, 42), marks=(), id="everything") + """ + + values: Sequence[object | NotSetType] + marks: Collection[MarkDecorator | Mark] + id: str | _HiddenParam | None + + @classmethod + def param( + cls, + *values: object, + marks: MarkDecorator | Collection[MarkDecorator | Mark] = (), + id: str | _HiddenParam | None = None, + ) -> ParameterSet: + if isinstance(marks, MarkDecorator): + marks = (marks,) + else: + assert isinstance(marks, collections.abc.Collection) + if any(i.name == "usefixtures" for i in marks): + raise ValueError( + "pytest.param cannot add pytest.mark.usefixtures; see " + "https://docs.pytest.org/en/stable/reference/reference.html#pytest-param" + ) + + if id is not None: + if not isinstance(id, str) and id is not HIDDEN_PARAM: + raise TypeError( + "Expected id to be a string or a `pytest.HIDDEN_PARAM` sentinel, " + f"got {type(id)}: {id!r}", + ) + return cls(values, marks, id) + + @classmethod + def extract_from( + cls, + parameterset: ParameterSet | Sequence[object] | object, + force_tuple: bool = False, + ) -> ParameterSet: + """Extract from an object or objects. + + :param parameterset: + A legacy style parameterset that may or may not be a tuple, + and may or may not be wrapped into a mess of mark objects. + + :param force_tuple: + Enforce tuple wrapping so single argument tuple values + don't get decomposed and break tests. + """ + if isinstance(parameterset, cls): + return parameterset + if force_tuple: + return cls.param(parameterset) + else: + # TODO: Refactor to fix this type-ignore. Currently the following + # passes type-checking but crashes: + # + # @pytest.mark.parametrize(('x', 'y'), [1, 2]) + # def test_foo(x, y): pass + return cls(parameterset, marks=[], id=None) # type: ignore[arg-type] + + @staticmethod + def _parse_parametrize_args( + argnames: str | Sequence[str], + argvalues: Iterable[ParameterSet | Sequence[object] | object], + *args, + **kwargs, + ) -> tuple[Sequence[str], bool]: + if isinstance(argnames, str): + argnames = [x.strip() for x in argnames.split(",") if x.strip()] + force_tuple = len(argnames) == 1 + else: + force_tuple = False + return argnames, force_tuple + + @staticmethod + def _parse_parametrize_parameters( + argvalues: Iterable[ParameterSet | Sequence[object] | object], + force_tuple: bool, + ) -> list[ParameterSet]: + return [ + ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues + ] + + @classmethod + def _for_parametrize( + cls, + argnames: str | Sequence[str], + argvalues: Iterable[ParameterSet | Sequence[object] | object], + func, + config: Config, + nodeid: str, + ) -> tuple[Sequence[str], list[ParameterSet]]: + argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues) + parameters = cls._parse_parametrize_parameters(argvalues, force_tuple) + del argvalues + + if parameters: + # Check all parameter sets have the correct number of values. + for param in parameters: + if len(param.values) != len(argnames): + msg = ( + '{nodeid}: in "parametrize" the number of names ({names_len}):\n' + " {names}\n" + "must be equal to the number of values ({values_len}):\n" + " {values}" + ) + fail( + msg.format( + nodeid=nodeid, + values=param.values, + names=argnames, + names_len=len(argnames), + values_len=len(param.values), + ), + pytrace=False, + ) + else: + # Empty parameter set (likely computed at runtime): create a single + # parameter set with NOTSET values, with the "empty parameter set" mark applied to it. + mark = get_empty_parameterset_mark(config, argnames, func) + parameters.append( + ParameterSet( + values=(NOTSET,) * len(argnames), marks=[mark], id="NOTSET" + ) + ) + return argnames, parameters + + +@final +@dataclasses.dataclass(frozen=True) +class Mark: + """A pytest mark.""" + + #: Name of the mark. + name: str + #: Positional arguments of the mark decorator. + args: tuple[Any, ...] + #: Keyword arguments of the mark decorator. + kwargs: Mapping[str, Any] + + #: Source Mark for ids with parametrize Marks. + _param_ids_from: Mark | None = dataclasses.field(default=None, repr=False) + #: Resolved/generated ids with parametrize Marks. + _param_ids_generated: Sequence[str] | None = dataclasses.field( + default=None, repr=False + ) + + def __init__( + self, + name: str, + args: tuple[Any, ...], + kwargs: Mapping[str, Any], + param_ids_from: Mark | None = None, + param_ids_generated: Sequence[str] | None = None, + *, + _ispytest: bool = False, + ) -> None: + """:meta private:""" + check_ispytest(_ispytest) + # Weirdness to bypass frozen=True. + object.__setattr__(self, "name", name) + object.__setattr__(self, "args", args) + object.__setattr__(self, "kwargs", kwargs) + object.__setattr__(self, "_param_ids_from", param_ids_from) + object.__setattr__(self, "_param_ids_generated", param_ids_generated) + + def _has_param_ids(self) -> bool: + return "ids" in self.kwargs or len(self.args) >= 4 + + def combined_with(self, other: Mark) -> Mark: + """Return a new Mark which is a combination of this + Mark and another Mark. + + Combines by appending args and merging kwargs. + + :param Mark other: The mark to combine with. + :rtype: Mark + """ + assert self.name == other.name + + # Remember source of ids with parametrize Marks. + param_ids_from: Mark | None = None + if self.name == "parametrize": + if other._has_param_ids(): + param_ids_from = other + elif self._has_param_ids(): + param_ids_from = self + + return Mark( + self.name, + self.args + other.args, + dict(self.kwargs, **other.kwargs), + param_ids_from=param_ids_from, + _ispytest=True, + ) + + +# A generic parameter designating an object to which a Mark may +# be applied -- a test function (callable) or class. +# Note: a lambda is not allowed, but this can't be represented. +Markable = TypeVar("Markable", bound=Callable[..., object] | type) + + +@dataclasses.dataclass +class MarkDecorator: + """A decorator for applying a mark on test functions and classes. + + ``MarkDecorators`` are created with ``pytest.mark``:: + + mark1 = pytest.mark.NAME # Simple MarkDecorator + mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator + + and can then be applied as decorators to test functions:: + + @mark2 + def test_function(): + pass + + When a ``MarkDecorator`` is called, it does the following: + + 1. If called with a single class as its only positional argument and no + additional keyword arguments, it attaches the mark to the class so it + gets applied automatically to all test cases found in that class. + + 2. If called with a single function as its only positional argument and + no additional keyword arguments, it attaches the mark to the function, + containing all the arguments already stored internally in the + ``MarkDecorator``. + + 3. When called in any other case, it returns a new ``MarkDecorator`` + instance with the original ``MarkDecorator``'s content updated with + the arguments passed to this call. + + Note: The rules above prevent a ``MarkDecorator`` from storing only a + single function or class reference as its positional argument with no + additional keyword or positional arguments. You can work around this by + using `with_args()`. + """ + + mark: Mark + + def __init__(self, mark: Mark, *, _ispytest: bool = False) -> None: + """:meta private:""" + check_ispytest(_ispytest) + self.mark = mark + + @property + def name(self) -> str: + """Alias for mark.name.""" + return self.mark.name + + @property + def args(self) -> tuple[Any, ...]: + """Alias for mark.args.""" + return self.mark.args + + @property + def kwargs(self) -> Mapping[str, Any]: + """Alias for mark.kwargs.""" + return self.mark.kwargs + + @property + def markname(self) -> str: + """:meta private:""" + return self.name # for backward-compat (2.4.1 had this attr) + + def with_args(self, *args: object, **kwargs: object) -> MarkDecorator: + """Return a MarkDecorator with extra arguments added. + + Unlike calling the MarkDecorator, with_args() can be used even + if the sole argument is a callable/class. + """ + mark = Mark(self.name, args, kwargs, _ispytest=True) + return MarkDecorator(self.mark.combined_with(mark), _ispytest=True) + + # Type ignored because the overloads overlap with an incompatible + # return type. Not much we can do about that. Thankfully mypy picks + # the first match so it works out even if we break the rules. + @overload + def __call__(self, arg: Markable) -> Markable: # type: ignore[overload-overlap] + pass + + @overload + def __call__(self, *args: object, **kwargs: object) -> MarkDecorator: + pass + + def __call__(self, *args: object, **kwargs: object): + """Call the MarkDecorator.""" + if args and not kwargs: + func = args[0] + is_class = inspect.isclass(func) + # For staticmethods/classmethods, the marks are eventually fetched from the + # function object, not the descriptor, so unwrap. + unwrapped_func = func + if isinstance(func, staticmethod | classmethod): + unwrapped_func = func.__func__ + if len(args) == 1 and (istestfunc(unwrapped_func) or is_class): + store_mark(unwrapped_func, self.mark, stacklevel=3) + return func + return self.with_args(*args, **kwargs) + + +def get_unpacked_marks( + obj: object | type, + *, + consider_mro: bool = True, +) -> list[Mark]: + """Obtain the unpacked marks that are stored on an object. + + If obj is a class and consider_mro is true, return marks applied to + this class and all of its super-classes in MRO order. If consider_mro + is false, only return marks applied directly to this class. + """ + if isinstance(obj, type): + if not consider_mro: + mark_lists = [obj.__dict__.get("pytestmark", [])] + else: + mark_lists = [ + x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__) + ] + mark_list = [] + for item in mark_lists: + if isinstance(item, list): + mark_list.extend(item) + else: + mark_list.append(item) + else: + mark_attribute = getattr(obj, "pytestmark", []) + if isinstance(mark_attribute, list): + mark_list = mark_attribute + else: + mark_list = [mark_attribute] + return list(normalize_mark_list(mark_list)) + + +def normalize_mark_list( + mark_list: Iterable[Mark | MarkDecorator], +) -> Iterable[Mark]: + """ + Normalize an iterable of Mark or MarkDecorator objects into a list of marks + by retrieving the `mark` attribute on MarkDecorator instances. + + :param mark_list: marks to normalize + :returns: A new list of the extracted Mark objects + """ + for mark in mark_list: + mark_obj = getattr(mark, "mark", mark) + if not isinstance(mark_obj, Mark): + raise TypeError(f"got {mark_obj!r} instead of Mark") + yield mark_obj + + +def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None: + """Store a Mark on an object. + + This is used to implement the Mark declarations/decorators correctly. + """ + assert isinstance(mark, Mark), mark + + from ..fixtures import getfixturemarker + + if getfixturemarker(obj) is not None: + warnings.warn(MARKED_FIXTURE, stacklevel=stacklevel) + + # Always reassign name to avoid updating pytestmark in a reference that + # was only borrowed. + obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark] + + +# Typing for builtin pytest marks. This is cheating; it gives builtin marks +# special privilege, and breaks modularity. But practicality beats purity... +if TYPE_CHECKING: + + class _SkipMarkDecorator(MarkDecorator): + @overload # type: ignore[override,no-overload-impl] + def __call__(self, arg: Markable) -> Markable: ... + + @overload + def __call__(self, reason: str = ...) -> MarkDecorator: ... + + class _SkipifMarkDecorator(MarkDecorator): + def __call__( # type: ignore[override] + self, + condition: str | bool = ..., + *conditions: str | bool, + reason: str = ..., + ) -> MarkDecorator: ... + + class _XfailMarkDecorator(MarkDecorator): + @overload # type: ignore[override,no-overload-impl] + def __call__(self, arg: Markable) -> Markable: ... + + @overload + def __call__( + self, + condition: str | bool = False, + *conditions: str | bool, + reason: str = ..., + run: bool = ..., + raises: None + | type[BaseException] + | tuple[type[BaseException], ...] + | AbstractRaises[BaseException] = ..., + strict: bool = ..., + ) -> MarkDecorator: ... + + class _ParametrizeMarkDecorator(MarkDecorator): + def __call__( # type: ignore[override] + self, + argnames: str | Sequence[str], + argvalues: Iterable[ParameterSet | Sequence[object] | object], + *, + indirect: bool | Sequence[str] = ..., + ids: Iterable[None | str | float | int | bool] + | Callable[[Any], object | None] + | None = ..., + scope: _ScopeName | None = ..., + ) -> MarkDecorator: ... + + class _UsefixturesMarkDecorator(MarkDecorator): + def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override] + ... + + class _FilterwarningsMarkDecorator(MarkDecorator): + def __call__(self, *filters: str) -> MarkDecorator: # type: ignore[override] + ... + + +@final +class MarkGenerator: + """Factory for :class:`MarkDecorator` objects - exposed as + a ``pytest.mark`` singleton instance. + + Example:: + + import pytest + + + @pytest.mark.slowtest + def test_function(): + pass + + applies a 'slowtest' :class:`Mark` on ``test_function``. + """ + + # See TYPE_CHECKING above. + if TYPE_CHECKING: + skip: _SkipMarkDecorator + skipif: _SkipifMarkDecorator + xfail: _XfailMarkDecorator + parametrize: _ParametrizeMarkDecorator + usefixtures: _UsefixturesMarkDecorator + filterwarnings: _FilterwarningsMarkDecorator + + def __init__(self, *, _ispytest: bool = False) -> None: + check_ispytest(_ispytest) + self._config: Config | None = None + self._markers: set[str] = set() + + def __getattr__(self, name: str) -> MarkDecorator: + """Generate a new :class:`MarkDecorator` with the given name.""" + if name[0] == "_": + raise AttributeError("Marker name must NOT start with underscore") + + if self._config is not None: + # We store a set of markers as a performance optimisation - if a mark + # name is in the set we definitely know it, but a mark may be known and + # not in the set. We therefore start by updating the set! + if name not in self._markers: + for line in self._config.getini("markers"): + # example lines: "skipif(condition): skip the given test if..." + # or "hypothesis: tests which use Hypothesis", so to get the + # marker name we split on both `:` and `(`. + marker = line.split(":")[0].split("(")[0].strip() + self._markers.add(marker) + + # If the name is not in the set of known marks after updating, + # then it really is time to issue a warning or an error. + if name not in self._markers: + # Raise a specific error for common misspellings of "parametrize". + if name in ["parameterize", "parametrise", "parameterise"]: + __tracebackhide__ = True + fail(f"Unknown '{name}' mark, did you mean 'parametrize'?") + + strict_markers = self._config.getini("strict_markers") + if strict_markers is None: + strict_markers = self._config.getini("strict") + if strict_markers: + fail( + f"{name!r} not found in `markers` configuration option", + pytrace=False, + ) + + warnings.warn( + f"Unknown pytest.mark.{name} - is this a typo? You can register " + "custom marks to avoid this warning - for details, see " + "https://docs.pytest.org/en/stable/how-to/mark.html", + PytestUnknownMarkWarning, + 2, + ) + + return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True) + + +MARK_GEN = MarkGenerator(_ispytest=True) + + +@final +class NodeKeywords(MutableMapping[str, Any]): + __slots__ = ("_markers", "node", "parent") + + def __init__(self, node: Node) -> None: + self.node = node + self.parent = node.parent + self._markers = {node.name: True} + + def __getitem__(self, key: str) -> Any: + try: + return self._markers[key] + except KeyError: + if self.parent is None: + raise + return self.parent.keywords[key] + + def __setitem__(self, key: str, value: Any) -> None: + self._markers[key] = value + + # Note: we could've avoided explicitly implementing some of the methods + # below and use the collections.abc fallback, but that would be slow. + + def __contains__(self, key: object) -> bool: + return key in self._markers or ( + self.parent is not None and key in self.parent.keywords + ) + + def update( # type: ignore[override] + self, + other: Mapping[str, Any] | Iterable[tuple[str, Any]] = (), + **kwds: Any, + ) -> None: + self._markers.update(other) + self._markers.update(kwds) + + def __delitem__(self, key: str) -> None: + raise ValueError("cannot delete key in keywords dict") + + def __iter__(self) -> Iterator[str]: + # Doesn't need to be fast. + yield from self._markers + if self.parent is not None: + for keyword in self.parent.keywords: + # self._marks and self.parent.keywords can have duplicates. + if keyword not in self._markers: + yield keyword + + def __len__(self) -> int: + # Doesn't need to be fast. + return sum(1 for keyword in self) + + def __repr__(self) -> str: + return f"" diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/monkeypatch.py b/Backend/venv/lib/python3.12/site-packages/_pytest/monkeypatch.py new file mode 100644 index 00000000..07cc3fc4 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/monkeypatch.py @@ -0,0 +1,435 @@ +# mypy: allow-untyped-defs +"""Monkeypatching and mocking functionality.""" + +from __future__ import annotations + +from collections.abc import Generator +from collections.abc import Mapping +from collections.abc import MutableMapping +from contextlib import contextmanager +import os +from pathlib import Path +import re +import sys +from typing import Any +from typing import final +from typing import overload +from typing import TypeVar +import warnings + +from _pytest.deprecated import MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES +from _pytest.fixtures import fixture +from _pytest.warning_types import PytestWarning + + +RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$") + + +K = TypeVar("K") +V = TypeVar("V") + + +@fixture +def monkeypatch() -> Generator[MonkeyPatch]: + """A convenient fixture for monkey-patching. + + The fixture provides these methods to modify objects, dictionaries, or + :data:`os.environ`: + + * :meth:`monkeypatch.setattr(obj, name, value, raising=True) ` + * :meth:`monkeypatch.delattr(obj, name, raising=True) ` + * :meth:`monkeypatch.setitem(mapping, name, value) ` + * :meth:`monkeypatch.delitem(obj, name, raising=True) ` + * :meth:`monkeypatch.setenv(name, value, prepend=None) ` + * :meth:`monkeypatch.delenv(name, raising=True) ` + * :meth:`monkeypatch.syspath_prepend(path) ` + * :meth:`monkeypatch.chdir(path) ` + * :meth:`monkeypatch.context() ` + + All modifications will be undone after the requesting test function or + fixture has finished. The ``raising`` parameter determines if a :class:`KeyError` + or :class:`AttributeError` will be raised if the set/deletion operation does not have the + specified target. + + To undo modifications done by the fixture in a contained scope, + use :meth:`context() `. + """ + mpatch = MonkeyPatch() + yield mpatch + mpatch.undo() + + +def resolve(name: str) -> object: + # Simplified from zope.dottedname. + parts = name.split(".") + + used = parts.pop(0) + found: object = __import__(used) + for part in parts: + used += "." + part + try: + found = getattr(found, part) + except AttributeError: + pass + else: + continue + # We use explicit un-nesting of the handling block in order + # to avoid nested exceptions. + try: + __import__(used) + except ImportError as ex: + expected = str(ex).split()[-1] + if expected == used: + raise + else: + raise ImportError(f"import error in {used}: {ex}") from ex + found = annotated_getattr(found, part, used) + return found + + +def annotated_getattr(obj: object, name: str, ann: str) -> object: + try: + obj = getattr(obj, name) + except AttributeError as e: + raise AttributeError( + f"{type(obj).__name__!r} object at {ann} has no attribute {name!r}" + ) from e + return obj + + +def derive_importpath(import_path: str, raising: bool) -> tuple[str, object]: + if not isinstance(import_path, str) or "." not in import_path: + raise TypeError(f"must be absolute import path string, not {import_path!r}") + module, attr = import_path.rsplit(".", 1) + target = resolve(module) + if raising: + annotated_getattr(target, attr, ann=module) + return attr, target + + +class Notset: + def __repr__(self) -> str: + return "" + + +notset = Notset() + + +@final +class MonkeyPatch: + """Helper to conveniently monkeypatch attributes/items/environment + variables/syspath. + + Returned by the :fixture:`monkeypatch` fixture. + + .. versionchanged:: 6.2 + Can now also be used directly as `pytest.MonkeyPatch()`, for when + the fixture is not available. In this case, use + :meth:`with MonkeyPatch.context() as mp: ` or remember to call + :meth:`undo` explicitly. + """ + + def __init__(self) -> None: + self._setattr: list[tuple[object, str, object]] = [] + self._setitem: list[tuple[Mapping[Any, Any], object, object]] = [] + self._cwd: str | None = None + self._savesyspath: list[str] | None = None + + @classmethod + @contextmanager + def context(cls) -> Generator[MonkeyPatch]: + """Context manager that returns a new :class:`MonkeyPatch` object + which undoes any patching done inside the ``with`` block upon exit. + + Example: + + .. code-block:: python + + import functools + + + def test_partial(monkeypatch): + with monkeypatch.context() as m: + m.setattr(functools, "partial", 3) + + Useful in situations where it is desired to undo some patches before the test ends, + such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples + of this see :issue:`3290`). + """ + m = cls() + try: + yield m + finally: + m.undo() + + @overload + def setattr( + self, + target: str, + name: object, + value: Notset = ..., + raising: bool = ..., + ) -> None: ... + + @overload + def setattr( + self, + target: object, + name: str, + value: object, + raising: bool = ..., + ) -> None: ... + + def setattr( + self, + target: str | object, + name: object | str, + value: object = notset, + raising: bool = True, + ) -> None: + """ + Set attribute value on target, memorizing the old value. + + For example: + + .. code-block:: python + + import os + + monkeypatch.setattr(os, "getcwd", lambda: "/") + + The code above replaces the :func:`os.getcwd` function by a ``lambda`` which + always returns ``"/"``. + + For convenience, you can specify a string as ``target`` which + will be interpreted as a dotted import path, with the last part + being the attribute name: + + .. code-block:: python + + monkeypatch.setattr("os.getcwd", lambda: "/") + + Raises :class:`AttributeError` if the attribute does not exist, unless + ``raising`` is set to False. + + **Where to patch** + + ``monkeypatch.setattr`` works by (temporarily) changing the object that a name points to with another one. + There can be many names pointing to any individual object, so for patching to work you must ensure + that you patch the name used by the system under test. + + See the section :ref:`Where to patch ` in the :mod:`unittest.mock` + docs for a complete explanation, which is meant for :func:`unittest.mock.patch` but + applies to ``monkeypatch.setattr`` as well. + """ + __tracebackhide__ = True + import inspect + + if isinstance(value, Notset): + if not isinstance(target, str): + raise TypeError( + "use setattr(target, name, value) or " + "setattr(target, value) with target being a dotted " + "import string" + ) + value = name + name, target = derive_importpath(target, raising) + else: + if not isinstance(name, str): + raise TypeError( + "use setattr(target, name, value) with name being a string or " + "setattr(target, value) with target being a dotted " + "import string" + ) + + oldval = getattr(target, name, notset) + if raising and oldval is notset: + raise AttributeError(f"{target!r} has no attribute {name!r}") + + # avoid class descriptors like staticmethod/classmethod + if inspect.isclass(target): + oldval = target.__dict__.get(name, notset) + self._setattr.append((target, name, oldval)) + setattr(target, name, value) + + def delattr( + self, + target: object | str, + name: str | Notset = notset, + raising: bool = True, + ) -> None: + """Delete attribute ``name`` from ``target``. + + If no ``name`` is specified and ``target`` is a string + it will be interpreted as a dotted import path with the + last part being the attribute name. + + Raises AttributeError it the attribute does not exist, unless + ``raising`` is set to False. + """ + __tracebackhide__ = True + import inspect + + if isinstance(name, Notset): + if not isinstance(target, str): + raise TypeError( + "use delattr(target, name) or " + "delattr(target) with target being a dotted " + "import string" + ) + name, target = derive_importpath(target, raising) + + if not hasattr(target, name): + if raising: + raise AttributeError(name) + else: + oldval = getattr(target, name, notset) + # Avoid class descriptors like staticmethod/classmethod. + if inspect.isclass(target): + oldval = target.__dict__.get(name, notset) + self._setattr.append((target, name, oldval)) + delattr(target, name) + + def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None: + """Set dictionary entry ``name`` to value.""" + self._setitem.append((dic, name, dic.get(name, notset))) + # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict + dic[name] = value # type: ignore[index] + + def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None: + """Delete ``name`` from dict. + + Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to + False. + """ + if name not in dic: + if raising: + raise KeyError(name) + else: + self._setitem.append((dic, name, dic.get(name, notset))) + # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict + del dic[name] # type: ignore[attr-defined] + + def setenv(self, name: str, value: str, prepend: str | None = None) -> None: + """Set environment variable ``name`` to ``value``. + + If ``prepend`` is a character, read the current environment variable + value and prepend the ``value`` adjoined with the ``prepend`` + character. + """ + if not isinstance(value, str): + warnings.warn( # type: ignore[unreachable] + PytestWarning( + f"Value of environment variable {name} type should be str, but got " + f"{value!r} (type: {type(value).__name__}); converted to str implicitly" + ), + stacklevel=2, + ) + value = str(value) + if prepend and name in os.environ: + value = value + prepend + os.environ[name] + self.setitem(os.environ, name, value) + + def delenv(self, name: str, raising: bool = True) -> None: + """Delete ``name`` from the environment. + + Raises ``KeyError`` if it does not exist, unless ``raising`` is set to + False. + """ + environ: MutableMapping[str, str] = os.environ + self.delitem(environ, name, raising=raising) + + def syspath_prepend(self, path) -> None: + """Prepend ``path`` to ``sys.path`` list of import locations.""" + if self._savesyspath is None: + self._savesyspath = sys.path[:] + sys.path.insert(0, str(path)) + + # https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171 + # this is only needed when pkg_resources was already loaded by the namespace package + if "pkg_resources" in sys.modules: + import pkg_resources + from pkg_resources import fixup_namespace_packages + + # Only issue deprecation warning if this call would actually have an + # effect for this specific path. + if ( + hasattr(pkg_resources, "_namespace_packages") + and pkg_resources._namespace_packages + ): + path_obj = Path(str(path)) + for ns_pkg in pkg_resources._namespace_packages: + if ns_pkg is None: + continue + ns_pkg_path = path_obj / ns_pkg.replace(".", os.sep) + if ns_pkg_path.is_dir(): + warnings.warn( + MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES, stacklevel=2 + ) + break + + fixup_namespace_packages(str(path)) + + # A call to syspathinsert() usually means that the caller wants to + # import some dynamically created files, thus with python3 we + # invalidate its import caches. + # This is especially important when any namespace package is in use, + # since then the mtime based FileFinder cache (that gets created in + # this case already) gets not invalidated when writing the new files + # quickly afterwards. + from importlib import invalidate_caches + + invalidate_caches() + + def chdir(self, path: str | os.PathLike[str]) -> None: + """Change the current working directory to the specified path. + + :param path: + The path to change into. + """ + if self._cwd is None: + self._cwd = os.getcwd() + os.chdir(path) + + def undo(self) -> None: + """Undo previous changes. + + This call consumes the undo stack. Calling it a second time has no + effect unless you do more monkeypatching after the undo call. + + There is generally no need to call `undo()`, since it is + called automatically during tear-down. + + .. note:: + The same `monkeypatch` fixture is used across a + single test function invocation. If `monkeypatch` is used both by + the test function itself and one of the test fixtures, + calling `undo()` will undo all of the changes made in + both functions. + + Prefer to use :meth:`context() ` instead. + """ + for obj, name, value in reversed(self._setattr): + if value is not notset: + setattr(obj, name, value) + else: + delattr(obj, name) + self._setattr[:] = [] + for dictionary, key, value in reversed(self._setitem): + if value is notset: + try: + # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict + del dictionary[key] # type: ignore[attr-defined] + except KeyError: + pass # Was already deleted, so we have the desired state. + else: + # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict + dictionary[key] = value # type: ignore[index] + self._setitem[:] = [] + if self._savesyspath is not None: + sys.path[:] = self._savesyspath + self._savesyspath = None + + if self._cwd is not None: + os.chdir(self._cwd) + self._cwd = None diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/nodes.py b/Backend/venv/lib/python3.12/site-packages/_pytest/nodes.py new file mode 100644 index 00000000..6690f6ab --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/nodes.py @@ -0,0 +1,772 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import abc +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import MutableMapping +from functools import cached_property +from functools import lru_cache +import os +import pathlib +from pathlib import Path +from typing import Any +from typing import cast +from typing import NoReturn +from typing import overload +from typing import TYPE_CHECKING +from typing import TypeVar +import warnings + +import pluggy + +import _pytest._code +from _pytest._code import getfslineno +from _pytest._code.code import ExceptionInfo +from _pytest._code.code import TerminalRepr +from _pytest._code.code import Traceback +from _pytest._code.code import TracebackStyle +from _pytest.compat import LEGACY_PATH +from _pytest.compat import signature +from _pytest.config import Config +from _pytest.config import ConftestImportFailure +from _pytest.config.compat import _check_path +from _pytest.deprecated import NODE_CTOR_FSPATH_ARG +from _pytest.mark.structures import Mark +from _pytest.mark.structures import MarkDecorator +from _pytest.mark.structures import NodeKeywords +from _pytest.outcomes import fail +from _pytest.pathlib import absolutepath +from _pytest.stash import Stash +from _pytest.warning_types import PytestWarning + + +if TYPE_CHECKING: + from typing_extensions import Self + + # Imported here due to circular import. + from _pytest.main import Session + + +SEP = "/" + +tracebackcutdir = Path(_pytest.__file__).parent + + +_T = TypeVar("_T") + + +def _imply_path( + node_type: type[Node], + path: Path | None, + fspath: LEGACY_PATH | None, +) -> Path: + if fspath is not None: + warnings.warn( + NODE_CTOR_FSPATH_ARG.format( + node_type_name=node_type.__name__, + ), + stacklevel=6, + ) + if path is not None: + if fspath is not None: + _check_path(path, fspath) + return path + else: + assert fspath is not None + return Path(fspath) + + +_NodeType = TypeVar("_NodeType", bound="Node") + + +class NodeMeta(abc.ABCMeta): + """Metaclass used by :class:`Node` to enforce that direct construction raises + :class:`Failed`. + + This behaviour supports the indirection introduced with :meth:`Node.from_parent`, + the named constructor to be used instead of direct construction. The design + decision to enforce indirection with :class:`NodeMeta` was made as a + temporary aid for refactoring the collection tree, which was diagnosed to + have :class:`Node` objects whose creational patterns were overly entangled. + Once the refactoring is complete, this metaclass can be removed. + + See https://github.com/pytest-dev/pytest/projects/3 for an overview of the + progress on detangling the :class:`Node` classes. + """ + + def __call__(cls, *k, **kw) -> NoReturn: + msg = ( + "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n" + "See " + "https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent" + " for more details." + ).format(name=f"{cls.__module__}.{cls.__name__}") + fail(msg, pytrace=False) + + def _create(cls: type[_T], *k, **kw) -> _T: + try: + return super().__call__(*k, **kw) # type: ignore[no-any-return,misc] + except TypeError: + sig = signature(getattr(cls, "__init__")) + known_kw = {k: v for k, v in kw.items() if k in sig.parameters} + from .warning_types import PytestDeprecationWarning + + warnings.warn( + PytestDeprecationWarning( + f"{cls} is not using a cooperative constructor and only takes {set(known_kw)}.\n" + "See https://docs.pytest.org/en/stable/deprecations.html" + "#constructors-of-custom-pytest-node-subclasses-should-take-kwargs " + "for more details." + ) + ) + + return super().__call__(*k, **known_kw) # type: ignore[no-any-return,misc] + + +class Node(abc.ABC, metaclass=NodeMeta): + r"""Base class of :class:`Collector` and :class:`Item`, the components of + the test collection tree. + + ``Collector``\'s are the internal nodes of the tree, and ``Item``\'s are the + leaf nodes. + """ + + # Implemented in the legacypath plugin. + #: A ``LEGACY_PATH`` copy of the :attr:`path` attribute. Intended for usage + #: for methods not migrated to ``pathlib.Path`` yet, such as + #: :meth:`Item.reportinfo `. Will be deprecated in + #: a future release, prefer using :attr:`path` instead. + fspath: LEGACY_PATH + + # Use __slots__ to make attribute access faster. + # Note that __dict__ is still available. + __slots__ = ( + "__dict__", + "_nodeid", + "_store", + "config", + "name", + "parent", + "path", + "session", + ) + + def __init__( + self, + name: str, + parent: Node | None = None, + config: Config | None = None, + session: Session | None = None, + fspath: LEGACY_PATH | None = None, + path: Path | None = None, + nodeid: str | None = None, + ) -> None: + #: A unique name within the scope of the parent node. + self.name: str = name + + #: The parent collector node. + self.parent = parent + + if config: + #: The pytest config object. + self.config: Config = config + else: + if not parent: + raise TypeError("config or parent must be provided") + self.config = parent.config + + if session: + #: The pytest session this node is part of. + self.session: Session = session + else: + if not parent: + raise TypeError("session or parent must be provided") + self.session = parent.session + + if path is None and fspath is None: + path = getattr(parent, "path", None) + #: Filesystem path where this node was collected from (can be None). + self.path: pathlib.Path = _imply_path(type(self), path, fspath=fspath) + + # The explicit annotation is to avoid publicly exposing NodeKeywords. + #: Keywords/markers collected from all scopes. + self.keywords: MutableMapping[str, Any] = NodeKeywords(self) + + #: The marker objects belonging to this node. + self.own_markers: list[Mark] = [] + + #: Allow adding of extra keywords to use for matching. + self.extra_keyword_matches: set[str] = set() + + if nodeid is not None: + assert "::()" not in nodeid + self._nodeid = nodeid + else: + if not self.parent: + raise TypeError("nodeid or parent must be provided") + self._nodeid = self.parent.nodeid + "::" + self.name + + #: A place where plugins can store information on the node for their + #: own use. + self.stash: Stash = Stash() + # Deprecated alias. Was never public. Can be removed in a few releases. + self._store = self.stash + + @classmethod + def from_parent(cls, parent: Node, **kw) -> Self: + """Public constructor for Nodes. + + This indirection got introduced in order to enable removing + the fragile logic from the node constructors. + + Subclasses can use ``super().from_parent(...)`` when overriding the + construction. + + :param parent: The parent node of this Node. + """ + if "config" in kw: + raise TypeError("config is not a valid argument for from_parent") + if "session" in kw: + raise TypeError("session is not a valid argument for from_parent") + return cls._create(parent=parent, **kw) + + @property + def ihook(self) -> pluggy.HookRelay: + """fspath-sensitive hook proxy used to call pytest hooks.""" + return self.session.gethookproxy(self.path) + + def __repr__(self) -> str: + return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None)) + + def warn(self, warning: Warning) -> None: + """Issue a warning for this Node. + + Warnings will be displayed after the test session, unless explicitly suppressed. + + :param Warning warning: + The warning instance to issue. + + :raises ValueError: If ``warning`` instance is not a subclass of Warning. + + Example usage: + + .. code-block:: python + + node.warn(PytestWarning("some message")) + node.warn(UserWarning("some message")) + + .. versionchanged:: 6.2 + Any subclass of :class:`Warning` is now accepted, rather than only + :class:`PytestWarning ` subclasses. + """ + # enforce type checks here to avoid getting a generic type error later otherwise. + if not isinstance(warning, Warning): + raise ValueError( + f"warning must be an instance of Warning or subclass, got {warning!r}" + ) + path, lineno = get_fslocation_from_item(self) + assert lineno is not None + warnings.warn_explicit( + warning, + category=None, + filename=str(path), + lineno=lineno + 1, + ) + + # Methods for ordering nodes. + + @property + def nodeid(self) -> str: + """A ::-separated string denoting its collection tree address.""" + return self._nodeid + + def __hash__(self) -> int: + return hash(self._nodeid) + + def setup(self) -> None: + pass + + def teardown(self) -> None: + pass + + def iter_parents(self) -> Iterator[Node]: + """Iterate over all parent collectors starting from and including self + up to the root of the collection tree. + + .. versionadded:: 8.1 + """ + parent: Node | None = self + while parent is not None: + yield parent + parent = parent.parent + + def listchain(self) -> list[Node]: + """Return a list of all parent collectors starting from the root of the + collection tree down to and including self.""" + chain = [] + item: Node | None = self + while item is not None: + chain.append(item) + item = item.parent + chain.reverse() + return chain + + def add_marker(self, marker: str | MarkDecorator, append: bool = True) -> None: + """Dynamically add a marker object to the node. + + :param marker: + The marker. + :param append: + Whether to append the marker, or prepend it. + """ + from _pytest.mark import MARK_GEN + + if isinstance(marker, MarkDecorator): + marker_ = marker + elif isinstance(marker, str): + marker_ = getattr(MARK_GEN, marker) + else: + raise ValueError("is not a string or pytest.mark.* Marker") + self.keywords[marker_.name] = marker_ + if append: + self.own_markers.append(marker_.mark) + else: + self.own_markers.insert(0, marker_.mark) + + def iter_markers(self, name: str | None = None) -> Iterator[Mark]: + """Iterate over all markers of the node. + + :param name: If given, filter the results by the name attribute. + :returns: An iterator of the markers of the node. + """ + return (x[1] for x in self.iter_markers_with_node(name=name)) + + def iter_markers_with_node( + self, name: str | None = None + ) -> Iterator[tuple[Node, Mark]]: + """Iterate over all markers of the node. + + :param name: If given, filter the results by the name attribute. + :returns: An iterator of (node, mark) tuples. + """ + for node in self.iter_parents(): + for mark in node.own_markers: + if name is None or getattr(mark, "name", None) == name: + yield node, mark + + @overload + def get_closest_marker(self, name: str) -> Mark | None: ... + + @overload + def get_closest_marker(self, name: str, default: Mark) -> Mark: ... + + def get_closest_marker(self, name: str, default: Mark | None = None) -> Mark | None: + """Return the first marker matching the name, from closest (for + example function) to farther level (for example module level). + + :param default: Fallback return value if no marker was found. + :param name: Name to filter by. + """ + return next(self.iter_markers(name=name), default) + + def listextrakeywords(self) -> set[str]: + """Return a set of all extra keywords in self and any parents.""" + extra_keywords: set[str] = set() + for item in self.listchain(): + extra_keywords.update(item.extra_keyword_matches) + return extra_keywords + + def listnames(self) -> list[str]: + return [x.name for x in self.listchain()] + + def addfinalizer(self, fin: Callable[[], object]) -> None: + """Register a function to be called without arguments when this node is + finalized. + + This method can only be called when this node is active + in a setup chain, for example during self.setup(). + """ + self.session._setupstate.addfinalizer(fin, self) + + def getparent(self, cls: type[_NodeType]) -> _NodeType | None: + """Get the closest parent node (including self) which is an instance of + the given class. + + :param cls: The node class to search for. + :returns: The node, if found. + """ + for node in self.iter_parents(): + if isinstance(node, cls): + return node + return None + + def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: + return excinfo.traceback + + def _repr_failure_py( + self, + excinfo: ExceptionInfo[BaseException], + style: TracebackStyle | None = None, + ) -> TerminalRepr: + from _pytest.fixtures import FixtureLookupError + + if isinstance(excinfo.value, ConftestImportFailure): + excinfo = ExceptionInfo.from_exception(excinfo.value.cause) + if isinstance(excinfo.value, fail.Exception): + if not excinfo.value.pytrace: + style = "value" + if isinstance(excinfo.value, FixtureLookupError): + return excinfo.value.formatrepr() + + tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] + if self.config.getoption("fulltrace", False): + style = "long" + tbfilter = False + else: + tbfilter = self._traceback_filter + if style == "auto": + style = "long" + # XXX should excinfo.getrepr record all data and toterminal() process it? + if style is None: + if self.config.getoption("tbstyle", "auto") == "short": + style = "short" + else: + style = "long" + + if self.config.get_verbosity() > 1: + truncate_locals = False + else: + truncate_locals = True + + truncate_args = False if self.config.get_verbosity() > 2 else True + + # excinfo.getrepr() formats paths relative to the CWD if `abspath` is False. + # It is possible for a fixture/test to change the CWD while this code runs, which + # would then result in the user seeing confusing paths in the failure message. + # To fix this, if the CWD changed, always display the full absolute path. + # It will be better to just always display paths relative to invocation_dir, but + # this requires a lot of plumbing (#6428). + try: + abspath = Path(os.getcwd()) != self.config.invocation_params.dir + except OSError: + abspath = True + + return excinfo.getrepr( + funcargs=True, + abspath=abspath, + showlocals=self.config.getoption("showlocals", False), + style=style, + tbfilter=tbfilter, + truncate_locals=truncate_locals, + truncate_args=truncate_args, + ) + + def repr_failure( + self, + excinfo: ExceptionInfo[BaseException], + style: TracebackStyle | None = None, + ) -> str | TerminalRepr: + """Return a representation of a collection or test failure. + + .. seealso:: :ref:`non-python tests` + + :param excinfo: Exception information for the failure. + """ + return self._repr_failure_py(excinfo, style) + + +def get_fslocation_from_item(node: Node) -> tuple[str | Path, int | None]: + """Try to extract the actual location from a node, depending on available attributes: + + * "location": a pair (path, lineno) + * "obj": a Python object that the node wraps. + * "path": just a path + + :rtype: A tuple of (str|Path, int) with filename and 0-based line number. + """ + # See Item.location. + location: tuple[str, int | None, str] | None = getattr(node, "location", None) + if location is not None: + return location[:2] + obj = getattr(node, "obj", None) + if obj is not None: + return getfslineno(obj) + return getattr(node, "path", "unknown location"), -1 + + +class Collector(Node, abc.ABC): + """Base class of all collectors. + + Collector create children through `collect()` and thus iteratively build + the collection tree. + """ + + class CollectError(Exception): + """An error during collection, contains a custom message.""" + + @abc.abstractmethod + def collect(self) -> Iterable[Item | Collector]: + """Collect children (items and collectors) for this collector.""" + raise NotImplementedError("abstract") + + # TODO: This omits the style= parameter which breaks Liskov Substitution. + def repr_failure( # type: ignore[override] + self, excinfo: ExceptionInfo[BaseException] + ) -> str | TerminalRepr: + """Return a representation of a collection failure. + + :param excinfo: Exception information for the failure. + """ + if isinstance(excinfo.value, self.CollectError) and not self.config.getoption( + "fulltrace", False + ): + exc = excinfo.value + return str(exc.args[0]) + + # Respect explicit tbstyle option, but default to "short" + # (_repr_failure_py uses "long" with "fulltrace" option always). + tbstyle = self.config.getoption("tbstyle", "auto") + if tbstyle == "auto": + tbstyle = "short" + + return self._repr_failure_py(excinfo, style=tbstyle) + + def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: + if hasattr(self, "path"): + traceback = excinfo.traceback + ntraceback = traceback.cut(path=self.path) + if ntraceback == traceback: + ntraceback = ntraceback.cut(excludepath=tracebackcutdir) + return ntraceback.filter(excinfo) + return excinfo.traceback + + +@lru_cache(maxsize=1000) +def _check_initialpaths_for_relpath( + initial_paths: frozenset[Path], path: Path +) -> str | None: + if path in initial_paths: + return "" + + for parent in path.parents: + if parent in initial_paths: + return str(path.relative_to(parent)) + + return None + + +class FSCollector(Collector, abc.ABC): + """Base class for filesystem collectors.""" + + def __init__( + self, + fspath: LEGACY_PATH | None = None, + path_or_parent: Path | Node | None = None, + path: Path | None = None, + name: str | None = None, + parent: Node | None = None, + config: Config | None = None, + session: Session | None = None, + nodeid: str | None = None, + ) -> None: + if path_or_parent: + if isinstance(path_or_parent, Node): + assert parent is None + parent = cast(FSCollector, path_or_parent) + elif isinstance(path_or_parent, Path): + assert path is None + path = path_or_parent + + path = _imply_path(type(self), path, fspath=fspath) + if name is None: + name = path.name + if parent is not None and parent.path != path: + try: + rel = path.relative_to(parent.path) + except ValueError: + pass + else: + name = str(rel) + name = name.replace(os.sep, SEP) + self.path = path + + if session is None: + assert parent is not None + session = parent.session + + if nodeid is None: + try: + nodeid = str(self.path.relative_to(session.config.rootpath)) + except ValueError: + nodeid = _check_initialpaths_for_relpath(session._initialpaths, path) + + if nodeid and os.sep != SEP: + nodeid = nodeid.replace(os.sep, SEP) + + super().__init__( + name=name, + parent=parent, + config=config, + session=session, + nodeid=nodeid, + path=path, + ) + + @classmethod + def from_parent( + cls, + parent, + *, + fspath: LEGACY_PATH | None = None, + path: Path | None = None, + **kw, + ) -> Self: + """The public constructor.""" + return super().from_parent(parent=parent, fspath=fspath, path=path, **kw) + + +class File(FSCollector, abc.ABC): + """Base class for collecting tests from a file. + + :ref:`non-python tests`. + """ + + +class Directory(FSCollector, abc.ABC): + """Base class for collecting files from a directory. + + A basic directory collector does the following: goes over the files and + sub-directories in the directory and creates collectors for them by calling + the hooks :hook:`pytest_collect_directory` and :hook:`pytest_collect_file`, + after checking that they are not ignored using + :hook:`pytest_ignore_collect`. + + The default directory collectors are :class:`~pytest.Dir` and + :class:`~pytest.Package`. + + .. versionadded:: 8.0 + + :ref:`custom directory collectors`. + """ + + +class Item(Node, abc.ABC): + """Base class of all test invocation items. + + Note that for a single function there might be multiple test invocation items. + """ + + nextitem = None + + def __init__( + self, + name, + parent=None, + config: Config | None = None, + session: Session | None = None, + nodeid: str | None = None, + **kw, + ) -> None: + # The first two arguments are intentionally passed positionally, + # to keep plugins who define a node type which inherits from + # (pytest.Item, pytest.File) working (see issue #8435). + # They can be made kwargs when the deprecation above is done. + super().__init__( + name, + parent, + config=config, + session=session, + nodeid=nodeid, + **kw, + ) + self._report_sections: list[tuple[str, str, str]] = [] + + #: A list of tuples (name, value) that holds user defined properties + #: for this test. + self.user_properties: list[tuple[str, object]] = [] + + self._check_item_and_collector_diamond_inheritance() + + def _check_item_and_collector_diamond_inheritance(self) -> None: + """ + Check if the current type inherits from both File and Collector + at the same time, emitting a warning accordingly (#8447). + """ + cls = type(self) + + # We inject an attribute in the type to avoid issuing this warning + # for the same class more than once, which is not helpful. + # It is a hack, but was deemed acceptable in order to avoid + # flooding the user in the common case. + attr_name = "_pytest_diamond_inheritance_warning_shown" + if getattr(cls, attr_name, False): + return + setattr(cls, attr_name, True) + + problems = ", ".join( + base.__name__ for base in cls.__bases__ if issubclass(base, Collector) + ) + if problems: + warnings.warn( + f"{cls.__name__} is an Item subclass and should not be a collector, " + f"however its bases {problems} are collectors.\n" + "Please split the Collectors and the Item into separate node types.\n" + "Pytest Doc example: https://docs.pytest.org/en/latest/example/nonpython.html\n" + "example pull request on a plugin: https://github.com/asmeurer/pytest-flakes/pull/40/", + PytestWarning, + ) + + @abc.abstractmethod + def runtest(self) -> None: + """Run the test case for this item. + + Must be implemented by subclasses. + + .. seealso:: :ref:`non-python tests` + """ + raise NotImplementedError("runtest must be implemented by Item subclass") + + def add_report_section(self, when: str, key: str, content: str) -> None: + """Add a new report section, similar to what's done internally to add + stdout and stderr captured output:: + + item.add_report_section("call", "stdout", "report section contents") + + :param str when: + One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``. + :param str key: + Name of the section, can be customized at will. Pytest uses ``"stdout"`` and + ``"stderr"`` internally. + :param str content: + The full contents as a string. + """ + if content: + self._report_sections.append((when, key, content)) + + def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]: + """Get location information for this item for test reports. + + Returns a tuple with three elements: + + - The path of the test (default ``self.path``) + - The 0-based line number of the test (default ``None``) + - A name of the test to be shown (default ``""``) + + .. seealso:: :ref:`non-python tests` + """ + return self.path, None, "" + + @cached_property + def location(self) -> tuple[str, int | None, str]: + """ + Returns a tuple of ``(relfspath, lineno, testname)`` for this item + where ``relfspath`` is file path relative to ``config.rootpath`` + and lineno is a 0-based line number. + """ + location = self.reportinfo() + path = absolutepath(location[0]) + relfspath = self.session._node_location_to_relpath(path) + assert type(location[2]) is str + return (relfspath, location[1], location[2]) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/outcomes.py b/Backend/venv/lib/python3.12/site-packages/_pytest/outcomes.py new file mode 100644 index 00000000..766be95c --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/outcomes.py @@ -0,0 +1,308 @@ +"""Exception classes and constants handling test outcomes as well as +functions creating them.""" + +from __future__ import annotations + +import sys +from typing import Any +from typing import ClassVar +from typing import NoReturn + +from .warning_types import PytestDeprecationWarning + + +class OutcomeException(BaseException): + """OutcomeException and its subclass instances indicate and contain info + about test and collection outcomes.""" + + def __init__(self, msg: str | None = None, pytrace: bool = True) -> None: + if msg is not None and not isinstance(msg, str): + error_msg = ( # type: ignore[unreachable] + "{} expected string as 'msg' parameter, got '{}' instead.\n" + "Perhaps you meant to use a mark?" + ) + raise TypeError(error_msg.format(type(self).__name__, type(msg).__name__)) + super().__init__(msg) + self.msg = msg + self.pytrace = pytrace + + def __repr__(self) -> str: + if self.msg is not None: + return self.msg + return f"<{self.__class__.__name__} instance>" + + __str__ = __repr__ + + +TEST_OUTCOME = (OutcomeException, Exception) + + +class Skipped(OutcomeException): + # XXX hackish: on 3k we fake to live in the builtins + # in order to have Skipped exception printing shorter/nicer + __module__ = "builtins" + + def __init__( + self, + msg: str | None = None, + pytrace: bool = True, + allow_module_level: bool = False, + *, + _use_item_location: bool = False, + ) -> None: + super().__init__(msg=msg, pytrace=pytrace) + self.allow_module_level = allow_module_level + # If true, the skip location is reported as the item's location, + # instead of the place that raises the exception/calls skip(). + self._use_item_location = _use_item_location + + +class Failed(OutcomeException): + """Raised from an explicit call to pytest.fail().""" + + __module__ = "builtins" + + +class Exit(Exception): + """Raised for immediate program exits (no tracebacks/summaries).""" + + def __init__( + self, msg: str = "unknown reason", returncode: int | None = None + ) -> None: + self.msg = msg + self.returncode = returncode + super().__init__(msg) + + +class XFailed(Failed): + """Raised from an explicit call to pytest.xfail().""" + + +class _Exit: + """Exit testing process. + + :param reason: + The message to show as the reason for exiting pytest. reason has a default value + only because `msg` is deprecated. + + :param returncode: + Return code to be used when exiting pytest. None means the same as ``0`` (no error), + same as :func:`sys.exit`. + + :raises pytest.exit.Exception: + The exception that is raised. + """ + + Exception: ClassVar[type[Exit]] = Exit + + def __call__(self, reason: str = "", returncode: int | None = None) -> NoReturn: + __tracebackhide__ = True + raise Exit(msg=reason, returncode=returncode) + + +exit: _Exit = _Exit() + + +class _Skip: + """Skip an executing test with the given message. + + This function should be called only during testing (setup, call or teardown) or + during collection by using the ``allow_module_level`` flag. This function can + be called in doctests as well. + + :param reason: + The message to show the user as reason for the skip. + + :param allow_module_level: + Allows this function to be called at module level. + Raising the skip exception at module level will stop + the execution of the module and prevent the collection of all tests in the module, + even those defined before the `skip` call. + + Defaults to False. + + :raises pytest.skip.Exception: + The exception that is raised. + + .. note:: + It is better to use the :ref:`pytest.mark.skipif ref` marker when + possible to declare a test to be skipped under certain conditions + like mismatching platforms or dependencies. + Similarly, use the ``# doctest: +SKIP`` directive (see :py:data:`doctest.SKIP`) + to skip a doctest statically. + """ + + Exception: ClassVar[type[Skipped]] = Skipped + + def __call__(self, reason: str = "", allow_module_level: bool = False) -> NoReturn: + __tracebackhide__ = True + raise Skipped(msg=reason, allow_module_level=allow_module_level) + + +skip: _Skip = _Skip() + + +class _Fail: + """Explicitly fail an executing test with the given message. + + :param reason: + The message to show the user as reason for the failure. + + :param pytrace: + If False, msg represents the full failure information and no + python traceback will be reported. + + :raises pytest.fail.Exception: + The exception that is raised. + """ + + Exception: ClassVar[type[Failed]] = Failed + + def __call__(self, reason: str = "", pytrace: bool = True) -> NoReturn: + __tracebackhide__ = True + raise Failed(msg=reason, pytrace=pytrace) + + +fail: _Fail = _Fail() + + +class _XFail: + """Imperatively xfail an executing test or setup function with the given reason. + + This function should be called only during testing (setup, call or teardown). + + No other code is executed after using ``xfail()`` (it is implemented + internally by raising an exception). + + :param reason: + The message to show the user as reason for the xfail. + + .. note:: + It is better to use the :ref:`pytest.mark.xfail ref` marker when + possible to declare a test to be xfailed under certain conditions + like known bugs or missing features. + + :raises pytest.xfail.Exception: + The exception that is raised. + """ + + Exception: ClassVar[type[XFailed]] = XFailed + + def __call__(self, reason: str = "") -> NoReturn: + __tracebackhide__ = True + raise XFailed(msg=reason) + + +xfail: _XFail = _XFail() + + +def importorskip( + modname: str, + minversion: str | None = None, + reason: str | None = None, + *, + exc_type: type[ImportError] | None = None, +) -> Any: + """Import and return the requested module ``modname``, or skip the + current test if the module cannot be imported. + + :param modname: + The name of the module to import. + :param minversion: + If given, the imported module's ``__version__`` attribute must be at + least this minimal version, otherwise the test is still skipped. + :param reason: + If given, this reason is shown as the message when the module cannot + be imported. + :param exc_type: + The exception that should be captured in order to skip modules. + Must be :py:class:`ImportError` or a subclass. + + If the module can be imported but raises :class:`ImportError`, pytest will + issue a warning to the user, as often users expect the module not to be + found (which would raise :class:`ModuleNotFoundError` instead). + + This warning can be suppressed by passing ``exc_type=ImportError`` explicitly. + + See :ref:`import-or-skip-import-error` for details. + + + :returns: + The imported module. This should be assigned to its canonical name. + + :raises pytest.skip.Exception: + If the module cannot be imported. + + Example:: + + docutils = pytest.importorskip("docutils") + + .. versionadded:: 8.2 + + The ``exc_type`` parameter. + """ + import warnings + + __tracebackhide__ = True + compile(modname, "", "eval") # to catch syntaxerrors + + # Until pytest 9.1, we will warn the user if we catch ImportError (instead of ModuleNotFoundError), + # as this might be hiding an installation/environment problem, which is not usually what is intended + # when using importorskip() (#11523). + # In 9.1, to keep the function signature compatible, we just change the code below to: + # 1. Use `exc_type = ModuleNotFoundError` if `exc_type` is not given. + # 2. Remove `warn_on_import` and the warning handling. + if exc_type is None: + exc_type = ImportError + warn_on_import_error = True + else: + warn_on_import_error = False + + skipped: Skipped | None = None + warning: Warning | None = None + + with warnings.catch_warnings(): + # Make sure to ignore ImportWarnings that might happen because + # of existing directories with the same name we're trying to + # import but without a __init__.py file. + warnings.simplefilter("ignore") + + try: + __import__(modname) + except exc_type as exc: + # Do not raise or issue warnings inside the catch_warnings() block. + if reason is None: + reason = f"could not import {modname!r}: {exc}" + skipped = Skipped(reason, allow_module_level=True) + + if warn_on_import_error and not isinstance(exc, ModuleNotFoundError): + lines = [ + "", + f"Module '{modname}' was found, but when imported by pytest it raised:", + f" {exc!r}", + "In pytest 9.1 this warning will become an error by default.", + "You can fix the underlying problem, or alternatively overwrite this behavior and silence this " + "warning by passing exc_type=ImportError explicitly.", + "See https://docs.pytest.org/en/stable/deprecations.html#pytest-importorskip-default-behavior-regarding-importerror", + ] + warning = PytestDeprecationWarning("\n".join(lines)) + + if warning: + warnings.warn(warning, stacklevel=2) + if skipped: + raise skipped + + mod = sys.modules[modname] + if minversion is None: + return mod + verattr = getattr(mod, "__version__", None) + if minversion is not None: + # Imported lazily to improve start-up time. + from packaging.version import Version + + if verattr is None or Version(verattr) < Version(minversion): + raise Skipped( + f"module {modname!r} has __version__ {verattr!r}, required is: {minversion!r}", + allow_module_level=True, + ) + return mod diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/pastebin.py b/Backend/venv/lib/python3.12/site-packages/_pytest/pastebin.py new file mode 100644 index 00000000..c7b39d96 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/pastebin.py @@ -0,0 +1,117 @@ +# mypy: allow-untyped-defs +"""Submit failure or test session information to a pastebin service.""" + +from __future__ import annotations + +from io import StringIO +import tempfile +from typing import IO + +from _pytest.config import Config +from _pytest.config import create_terminal_writer +from _pytest.config.argparsing import Parser +from _pytest.stash import StashKey +from _pytest.terminal import TerminalReporter +import pytest + + +pastebinfile_key = StashKey[IO[bytes]]() + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("terminal reporting") + group.addoption( + "--pastebin", + metavar="mode", + action="store", + dest="pastebin", + default=None, + choices=["failed", "all"], + help="Send failed|all info to bpaste.net pastebin service", + ) + + +@pytest.hookimpl(trylast=True) +def pytest_configure(config: Config) -> None: + if config.option.pastebin == "all": + tr = config.pluginmanager.getplugin("terminalreporter") + # If no terminal reporter plugin is present, nothing we can do here; + # this can happen when this function executes in a worker node + # when using pytest-xdist, for example. + if tr is not None: + # pastebin file will be UTF-8 encoded binary file. + config.stash[pastebinfile_key] = tempfile.TemporaryFile("w+b") + oldwrite = tr._tw.write + + def tee_write(s, **kwargs): + oldwrite(s, **kwargs) + if isinstance(s, str): + s = s.encode("utf-8") + config.stash[pastebinfile_key].write(s) + + tr._tw.write = tee_write + + +def pytest_unconfigure(config: Config) -> None: + if pastebinfile_key in config.stash: + pastebinfile = config.stash[pastebinfile_key] + # Get terminal contents and delete file. + pastebinfile.seek(0) + sessionlog = pastebinfile.read() + pastebinfile.close() + del config.stash[pastebinfile_key] + # Undo our patching in the terminal reporter. + tr = config.pluginmanager.getplugin("terminalreporter") + del tr._tw.__dict__["write"] + # Write summary. + tr.write_sep("=", "Sending information to Paste Service") + pastebinurl = create_new_paste(sessionlog) + tr.write_line(f"pastebin session-log: {pastebinurl}\n") + + +def create_new_paste(contents: str | bytes) -> str: + """Create a new paste using the bpaste.net service. + + :contents: Paste contents string. + :returns: URL to the pasted contents, or an error message. + """ + import re + from urllib.error import HTTPError + from urllib.parse import urlencode + from urllib.request import urlopen + + params = {"code": contents, "lexer": "text", "expiry": "1week"} + url = "https://bpa.st" + try: + response: str = ( + urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8") + ) + except HTTPError as e: + with e: # HTTPErrors are also http responses that must be closed! + return f"bad response: {e}" + except OSError as e: # eg urllib.error.URLError + return f"bad response: {e}" + m = re.search(r'href="/raw/(\w+)"', response) + if m: + return f"{url}/show/{m.group(1)}" + else: + return "bad response: invalid format ('" + response + "')" + + +def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None: + if terminalreporter.config.option.pastebin != "failed": + return + if "failed" in terminalreporter.stats: + terminalreporter.write_sep("=", "Sending information to Paste Service") + for rep in terminalreporter.stats["failed"]: + try: + msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc + except AttributeError: + msg = terminalreporter._getfailureheadline(rep) + file = StringIO() + tw = create_terminal_writer(terminalreporter.config, file) + rep.toterminal(tw) + s = file.getvalue() + assert len(s) + pastebinurl = create_new_paste(s) + terminalreporter.write_line(f"{msg} --> {pastebinurl}") diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/pathlib.py b/Backend/venv/lib/python3.12/site-packages/_pytest/pathlib.py new file mode 100644 index 00000000..cd154346 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/pathlib.py @@ -0,0 +1,1063 @@ +from __future__ import annotations + +import atexit +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Iterator +import contextlib +from enum import Enum +from errno import EBADF +from errno import ELOOP +from errno import ENOENT +from errno import ENOTDIR +import fnmatch +from functools import partial +from importlib.machinery import ModuleSpec +from importlib.machinery import PathFinder +import importlib.util +import itertools +import os +from os.path import expanduser +from os.path import expandvars +from os.path import isabs +from os.path import sep +from pathlib import Path +from pathlib import PurePath +from posixpath import sep as posix_sep +import shutil +import sys +import types +from types import ModuleType +from typing import Any +from typing import TypeVar +import uuid +import warnings + +from _pytest.compat import assert_never +from _pytest.outcomes import skip +from _pytest.warning_types import PytestWarning + + +if sys.version_info < (3, 11): + from importlib._bootstrap_external import _NamespaceLoader as NamespaceLoader +else: + from importlib.machinery import NamespaceLoader + +LOCK_TIMEOUT = 60 * 60 * 24 * 3 + +_AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath) + +# The following function, variables and comments were +# copied from cpython 3.9 Lib/pathlib.py file. + +# EBADF - guard against macOS `stat` throwing EBADF +_IGNORED_ERRORS = (ENOENT, ENOTDIR, EBADF, ELOOP) + +_IGNORED_WINERRORS = ( + 21, # ERROR_NOT_READY - drive exists but is not accessible + 1921, # ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself +) + + +def _ignore_error(exception: Exception) -> bool: + return ( + getattr(exception, "errno", None) in _IGNORED_ERRORS + or getattr(exception, "winerror", None) in _IGNORED_WINERRORS + ) + + +def get_lock_path(path: _AnyPurePath) -> _AnyPurePath: + return path.joinpath(".lock") + + +def on_rm_rf_error( + func: Callable[..., Any] | None, + path: str, + excinfo: BaseException + | tuple[type[BaseException], BaseException, types.TracebackType | None], + *, + start_path: Path, +) -> bool: + """Handle known read-only errors during rmtree. + + The returned value is used only by our own tests. + """ + if isinstance(excinfo, BaseException): + exc = excinfo + else: + exc = excinfo[1] + + # Another process removed the file in the middle of the "rm_rf" (xdist for example). + # More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018 + if isinstance(exc, FileNotFoundError): + return False + + if not isinstance(exc, PermissionError): + warnings.warn( + PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}") + ) + return False + + if func not in (os.rmdir, os.remove, os.unlink): + if func not in (os.open,): + warnings.warn( + PytestWarning( + f"(rm_rf) unknown function {func} when removing {path}:\n{type(exc)}: {exc}" + ) + ) + return False + + # Chmod + retry. + import stat + + def chmod_rw(p: str) -> None: + mode = os.stat(p).st_mode + os.chmod(p, mode | stat.S_IRUSR | stat.S_IWUSR) + + # For files, we need to recursively go upwards in the directories to + # ensure they all are also writable. + p = Path(path) + if p.is_file(): + for parent in p.parents: + chmod_rw(str(parent)) + # Stop when we reach the original path passed to rm_rf. + if parent == start_path: + break + chmod_rw(str(path)) + + func(path) + return True + + +def ensure_extended_length_path(path: Path) -> Path: + """Get the extended-length version of a path (Windows). + + On Windows, by default, the maximum length of a path (MAX_PATH) is 260 + characters, and operations on paths longer than that fail. But it is possible + to overcome this by converting the path to "extended-length" form before + performing the operation: + https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation + + On Windows, this function returns the extended-length absolute version of path. + On other platforms it returns path unchanged. + """ + if sys.platform.startswith("win32"): + path = path.resolve() + path = Path(get_extended_length_path_str(str(path))) + return path + + +def get_extended_length_path_str(path: str) -> str: + """Convert a path to a Windows extended length path.""" + long_path_prefix = "\\\\?\\" + unc_long_path_prefix = "\\\\?\\UNC\\" + if path.startswith((long_path_prefix, unc_long_path_prefix)): + return path + # UNC + if path.startswith("\\\\"): + return unc_long_path_prefix + path[2:] + return long_path_prefix + path + + +def rm_rf(path: Path) -> None: + """Remove the path contents recursively, even if some elements + are read-only.""" + path = ensure_extended_length_path(path) + onerror = partial(on_rm_rf_error, start_path=path) + if sys.version_info >= (3, 12): + shutil.rmtree(str(path), onexc=onerror) + else: + shutil.rmtree(str(path), onerror=onerror) + + +def find_prefixed(root: Path, prefix: str) -> Iterator[os.DirEntry[str]]: + """Find all elements in root that begin with the prefix, case-insensitive.""" + l_prefix = prefix.lower() + for x in os.scandir(root): + if x.name.lower().startswith(l_prefix): + yield x + + +def extract_suffixes(iter: Iterable[os.DirEntry[str]], prefix: str) -> Iterator[str]: + """Return the parts of the paths following the prefix. + + :param iter: Iterator over path names. + :param prefix: Expected prefix of the path names. + """ + p_len = len(prefix) + for entry in iter: + yield entry.name[p_len:] + + +def find_suffixes(root: Path, prefix: str) -> Iterator[str]: + """Combine find_prefixes and extract_suffixes.""" + return extract_suffixes(find_prefixed(root, prefix), prefix) + + +def parse_num(maybe_num: str) -> int: + """Parse number path suffixes, returns -1 on error.""" + try: + return int(maybe_num) + except ValueError: + return -1 + + +def _force_symlink(root: Path, target: str | PurePath, link_to: str | Path) -> None: + """Helper to create the current symlink. + + It's full of race conditions that are reasonably OK to ignore + for the context of best effort linking to the latest test run. + + The presumption being that in case of much parallelism + the inaccuracy is going to be acceptable. + """ + current_symlink = root.joinpath(target) + try: + current_symlink.unlink() + except OSError: + pass + try: + current_symlink.symlink_to(link_to) + except Exception: + pass + + +def make_numbered_dir(root: Path, prefix: str, mode: int = 0o700) -> Path: + """Create a directory with an increased number as suffix for the given prefix.""" + for i in range(10): + # try up to 10 times to create the folder + max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1) + new_number = max_existing + 1 + new_path = root.joinpath(f"{prefix}{new_number}") + try: + new_path.mkdir(mode=mode) + except Exception: + pass + else: + _force_symlink(root, prefix + "current", new_path) + return new_path + else: + raise OSError( + "could not create numbered dir with prefix " + f"{prefix} in {root} after 10 tries" + ) + + +def create_cleanup_lock(p: Path) -> Path: + """Create a lock to prevent premature folder cleanup.""" + lock_path = get_lock_path(p) + try: + fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) + except FileExistsError as e: + raise OSError(f"cannot create lockfile in {p}") from e + else: + pid = os.getpid() + spid = str(pid).encode() + os.write(fd, spid) + os.close(fd) + if not lock_path.is_file(): + raise OSError("lock path got renamed after successful creation") + return lock_path + + +def register_cleanup_lock_removal( + lock_path: Path, register: Any = atexit.register +) -> Any: + """Register a cleanup function for removing a lock, by default on atexit.""" + pid = os.getpid() + + def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> None: + current_pid = os.getpid() + if current_pid != original_pid: + # fork + return + try: + lock_path.unlink() + except OSError: + pass + + return register(cleanup_on_exit) + + +def maybe_delete_a_numbered_dir(path: Path) -> None: + """Remove a numbered directory if its lock can be obtained and it does + not seem to be in use.""" + path = ensure_extended_length_path(path) + lock_path = None + try: + lock_path = create_cleanup_lock(path) + parent = path.parent + + garbage = parent.joinpath(f"garbage-{uuid.uuid4()}") + path.rename(garbage) + rm_rf(garbage) + except OSError: + # known races: + # * other process did a cleanup at the same time + # * deletable folder was found + # * process cwd (Windows) + return + finally: + # If we created the lock, ensure we remove it even if we failed + # to properly remove the numbered dir. + if lock_path is not None: + try: + lock_path.unlink() + except OSError: + pass + + +def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> bool: + """Check if `path` is deletable based on whether the lock file is expired.""" + if path.is_symlink(): + return False + lock = get_lock_path(path) + try: + if not lock.is_file(): + return True + except OSError: + # we might not have access to the lock file at all, in this case assume + # we don't have access to the entire directory (#7491). + return False + try: + lock_time = lock.stat().st_mtime + except Exception: + return False + else: + if lock_time < consider_lock_dead_if_created_before: + # We want to ignore any errors while trying to remove the lock such as: + # - PermissionDenied, like the file permissions have changed since the lock creation; + # - FileNotFoundError, in case another pytest process got here first; + # and any other cause of failure. + with contextlib.suppress(OSError): + lock.unlink() + return True + return False + + +def try_cleanup(path: Path, consider_lock_dead_if_created_before: float) -> None: + """Try to cleanup a folder if we can ensure it's deletable.""" + if ensure_deletable(path, consider_lock_dead_if_created_before): + maybe_delete_a_numbered_dir(path) + + +def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]: + """List candidates for numbered directories to be removed - follows py.path.""" + max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1) + max_delete = max_existing - keep + entries = find_prefixed(root, prefix) + entries, entries2 = itertools.tee(entries) + numbers = map(parse_num, extract_suffixes(entries2, prefix)) + for entry, number in zip(entries, numbers, strict=True): + if number <= max_delete: + yield Path(entry) + + +def cleanup_dead_symlinks(root: Path) -> None: + for left_dir in root.iterdir(): + if left_dir.is_symlink(): + if not left_dir.resolve().exists(): + left_dir.unlink() + + +def cleanup_numbered_dir( + root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float +) -> None: + """Cleanup for lock driven numbered directories.""" + if not root.exists(): + return + for path in cleanup_candidates(root, prefix, keep): + try_cleanup(path, consider_lock_dead_if_created_before) + for path in root.glob("garbage-*"): + try_cleanup(path, consider_lock_dead_if_created_before) + + cleanup_dead_symlinks(root) + + +def make_numbered_dir_with_cleanup( + root: Path, + prefix: str, + keep: int, + lock_timeout: float, + mode: int, +) -> Path: + """Create a numbered dir with a cleanup lock and remove old ones.""" + e = None + for i in range(10): + try: + p = make_numbered_dir(root, prefix, mode) + # Only lock the current dir when keep is not 0 + if keep != 0: + lock_path = create_cleanup_lock(p) + register_cleanup_lock_removal(lock_path) + except Exception as exc: + e = exc + else: + consider_lock_dead_if_created_before = p.stat().st_mtime - lock_timeout + # Register a cleanup for program exit + atexit.register( + cleanup_numbered_dir, + root, + prefix, + keep, + consider_lock_dead_if_created_before, + ) + return p + assert e is not None + raise e + + +def resolve_from_str(input: str, rootpath: Path) -> Path: + input = expanduser(input) + input = expandvars(input) + if isabs(input): + return Path(input) + else: + return rootpath.joinpath(input) + + +def fnmatch_ex(pattern: str, path: str | os.PathLike[str]) -> bool: + """A port of FNMatcher from py.path.common which works with PurePath() instances. + + The difference between this algorithm and PurePath.match() is that the + latter matches "**" glob expressions for each part of the path, while + this algorithm uses the whole path instead. + + For example: + "tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py" + with this algorithm, but not with PurePath.match(). + + This algorithm was ported to keep backward-compatibility with existing + settings which assume paths match according this logic. + + References: + * https://bugs.python.org/issue29249 + * https://bugs.python.org/issue34731 + """ + path = PurePath(path) + iswin32 = sys.platform.startswith("win") + + if iswin32 and sep not in pattern and posix_sep in pattern: + # Running on Windows, the pattern has no Windows path separators, + # and the pattern has one or more Posix path separators. Replace + # the Posix path separators with the Windows path separator. + pattern = pattern.replace(posix_sep, sep) + + if sep not in pattern: + name = path.name + else: + name = str(path) + if path.is_absolute() and not os.path.isabs(pattern): + pattern = f"*{os.sep}{pattern}" + return fnmatch.fnmatch(name, pattern) + + +def parts(s: str) -> set[str]: + parts = s.split(sep) + return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))} + + +def symlink_or_skip( + src: os.PathLike[str] | str, + dst: os.PathLike[str] | str, + **kwargs: Any, +) -> None: + """Make a symlink, or skip the test in case symlinks are not supported.""" + try: + os.symlink(src, dst, **kwargs) + except OSError as e: + skip(f"symlinks not supported: {e}") + + +class ImportMode(Enum): + """Possible values for `mode` parameter of `import_path`.""" + + prepend = "prepend" + append = "append" + importlib = "importlib" + + +class ImportPathMismatchError(ImportError): + """Raised on import_path() if there is a mismatch of __file__'s. + + This can happen when `import_path` is called multiple times with different filenames that has + the same basename but reside in packages + (for example "/tests1/test_foo.py" and "/tests2/test_foo.py"). + """ + + +def import_path( + path: str | os.PathLike[str], + *, + mode: str | ImportMode = ImportMode.prepend, + root: Path, + consider_namespace_packages: bool, +) -> ModuleType: + """ + Import and return a module from the given path, which can be a file (a module) or + a directory (a package). + + :param path: + Path to the file to import. + + :param mode: + Controls the underlying import mechanism that will be used: + + * ImportMode.prepend: the directory containing the module (or package, taking + `__init__.py` files into account) will be put at the *start* of `sys.path` before + being imported with `importlib.import_module`. + + * ImportMode.append: same as `prepend`, but the directory will be appended + to the end of `sys.path`, if not already in `sys.path`. + + * ImportMode.importlib: uses more fine control mechanisms provided by `importlib` + to import the module, which avoids having to muck with `sys.path` at all. It effectively + allows having same-named test modules in different places. + + :param root: + Used as an anchor when mode == ImportMode.importlib to obtain + a unique name for the module being imported so it can safely be stored + into ``sys.modules``. + + :param consider_namespace_packages: + If True, consider namespace packages when resolving module names. + + :raises ImportPathMismatchError: + If after importing the given `path` and the module `__file__` + are different. Only raised in `prepend` and `append` modes. + """ + path = Path(path) + mode = ImportMode(mode) + + if not path.exists(): + raise ImportError(path) + + if mode is ImportMode.importlib: + # Try to import this module using the standard import mechanisms, but + # without touching sys.path. + try: + pkg_root, module_name = resolve_pkg_root_and_module_name( + path, consider_namespace_packages=consider_namespace_packages + ) + except CouldNotResolvePathError: + pass + else: + # If the given module name is already in sys.modules, do not import it again. + with contextlib.suppress(KeyError): + return sys.modules[module_name] + + mod = _import_module_using_spec( + module_name, path, pkg_root, insert_modules=False + ) + if mod is not None: + return mod + + # Could not import the module with the current sys.path, so we fall back + # to importing the file as a single module, not being a part of a package. + module_name = module_name_from_path(path, root) + with contextlib.suppress(KeyError): + return sys.modules[module_name] + + mod = _import_module_using_spec( + module_name, path, path.parent, insert_modules=True + ) + if mod is None: + raise ImportError(f"Can't find module {module_name} at location {path}") + return mod + + try: + pkg_root, module_name = resolve_pkg_root_and_module_name( + path, consider_namespace_packages=consider_namespace_packages + ) + except CouldNotResolvePathError: + pkg_root, module_name = path.parent, path.stem + + # Change sys.path permanently: restoring it at the end of this function would cause surprising + # problems because of delayed imports: for example, a conftest.py file imported by this function + # might have local imports, which would fail at runtime if we restored sys.path. + if mode is ImportMode.append: + if str(pkg_root) not in sys.path: + sys.path.append(str(pkg_root)) + elif mode is ImportMode.prepend: + if str(pkg_root) != sys.path[0]: + sys.path.insert(0, str(pkg_root)) + else: + assert_never(mode) + + importlib.import_module(module_name) + + mod = sys.modules[module_name] + if path.name == "__init__.py": + return mod + + ignore = os.environ.get("PY_IGNORE_IMPORTMISMATCH", "") + if ignore != "1": + module_file = mod.__file__ + if module_file is None: + raise ImportPathMismatchError(module_name, module_file, path) + + if module_file.endswith((".pyc", ".pyo")): + module_file = module_file[:-1] + if module_file.endswith(os.sep + "__init__.py"): + module_file = module_file[: -(len(os.sep + "__init__.py"))] + + try: + is_same = _is_same(str(path), module_file) + except FileNotFoundError: + is_same = False + + if not is_same: + raise ImportPathMismatchError(module_name, module_file, path) + + return mod + + +def _import_module_using_spec( + module_name: str, module_path: Path, module_location: Path, *, insert_modules: bool +) -> ModuleType | None: + """ + Tries to import a module by its canonical name, path, and its parent location. + + :param module_name: + The expected module name, will become the key of `sys.modules`. + + :param module_path: + The file path of the module, for example `/foo/bar/test_demo.py`. + If module is a package, pass the path to the `__init__.py` of the package. + If module is a namespace package, pass directory path. + + :param module_location: + The parent location of the module. + If module is a package, pass the directory containing the `__init__.py` file. + + :param insert_modules: + If True, will call `insert_missing_modules` to create empty intermediate modules + with made-up module names (when importing test files not reachable from `sys.path`). + + Example 1 of parent_module_*: + + module_name: "a.b.c.demo" + module_path: Path("a/b/c/demo.py") + module_location: Path("a/b/c/") + if "a.b.c" is package ("a/b/c/__init__.py" exists), then + parent_module_name: "a.b.c" + parent_module_path: Path("a/b/c/__init__.py") + parent_module_location: Path("a/b/c/") + else: + parent_module_name: "a.b.c" + parent_module_path: Path("a/b/c") + parent_module_location: Path("a/b/") + + Example 2 of parent_module_*: + + module_name: "a.b.c" + module_path: Path("a/b/c/__init__.py") + module_location: Path("a/b/c/") + if "a.b" is package ("a/b/__init__.py" exists), then + parent_module_name: "a.b" + parent_module_path: Path("a/b/__init__.py") + parent_module_location: Path("a/b/") + else: + parent_module_name: "a.b" + parent_module_path: Path("a/b/") + parent_module_location: Path("a/") + """ + # Attempt to import the parent module, seems is our responsibility: + # https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311 + parent_module_name, _, name = module_name.rpartition(".") + parent_module: ModuleType | None = None + if parent_module_name: + parent_module = sys.modules.get(parent_module_name) + # If the parent_module lacks the `__path__` attribute, AttributeError when finding a submodule's spec, + # requiring re-import according to the path. + need_reimport = not hasattr(parent_module, "__path__") + if parent_module is None or need_reimport: + # Get parent_location based on location, get parent_path based on path. + if module_path.name == "__init__.py": + # If the current module is in a package, + # need to leave the package first and then enter the parent module. + parent_module_path = module_path.parent.parent + else: + parent_module_path = module_path.parent + + if (parent_module_path / "__init__.py").is_file(): + # If the parent module is a package, loading by __init__.py file. + parent_module_path = parent_module_path / "__init__.py" + + parent_module = _import_module_using_spec( + parent_module_name, + parent_module_path, + parent_module_path.parent, + insert_modules=insert_modules, + ) + + # Checking with sys.meta_path first in case one of its hooks can import this module, + # such as our own assertion-rewrite hook. + for meta_importer in sys.meta_path: + module_name_of_meta = getattr(meta_importer.__class__, "__module__", "") + if module_name_of_meta == "_pytest.assertion.rewrite" and module_path.is_file(): + # Import modules in subdirectories by module_path + # to ensure assertion rewrites are not missed (#12659). + find_spec_path = [str(module_location), str(module_path)] + else: + find_spec_path = [str(module_location)] + + spec = meta_importer.find_spec(module_name, find_spec_path) + + if spec_matches_module_path(spec, module_path): + break + else: + loader = None + if module_path.is_dir(): + # The `spec_from_file_location` matches a loader based on the file extension by default. + # For a namespace package, need to manually specify a loader. + loader = NamespaceLoader(name, module_path, PathFinder()) # type: ignore[arg-type] + + spec = importlib.util.spec_from_file_location( + module_name, str(module_path), loader=loader + ) + + if spec_matches_module_path(spec, module_path): + assert spec is not None + # Find spec and import this module. + mod = importlib.util.module_from_spec(spec) + sys.modules[module_name] = mod + spec.loader.exec_module(mod) # type: ignore[union-attr] + + # Set this module as an attribute of the parent module (#12194). + if parent_module is not None: + setattr(parent_module, name, mod) + + if insert_modules: + insert_missing_modules(sys.modules, module_name) + return mod + + return None + + +def spec_matches_module_path(module_spec: ModuleSpec | None, module_path: Path) -> bool: + """Return true if the given ModuleSpec can be used to import the given module path.""" + if module_spec is None: + return False + + if module_spec.origin: + return Path(module_spec.origin) == module_path + + # Compare the path with the `module_spec.submodule_Search_Locations` in case + # the module is part of a namespace package. + # https://docs.python.org/3/library/importlib.html#importlib.machinery.ModuleSpec.submodule_search_locations + if module_spec.submodule_search_locations: # can be None. + for path in module_spec.submodule_search_locations: + if Path(path) == module_path: + return True + + return False + + +# Implement a special _is_same function on Windows which returns True if the two filenames +# compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678). +if sys.platform.startswith("win"): + + def _is_same(f1: str, f2: str) -> bool: + return Path(f1) == Path(f2) or os.path.samefile(f1, f2) + +else: + + def _is_same(f1: str, f2: str) -> bool: + return os.path.samefile(f1, f2) + + +def module_name_from_path(path: Path, root: Path) -> str: + """ + Return a dotted module name based on the given path, anchored on root. + + For example: path="projects/src/tests/test_foo.py" and root="/projects", the + resulting module name will be "src.tests.test_foo". + """ + path = path.with_suffix("") + try: + relative_path = path.relative_to(root) + except ValueError: + # If we can't get a relative path to root, use the full path, except + # for the first part ("d:\\" or "/" depending on the platform, for example). + path_parts = path.parts[1:] + else: + # Use the parts for the relative path to the root path. + path_parts = relative_path.parts + + # Module name for packages do not contain the __init__ file, unless + # the `__init__.py` file is at the root. + if len(path_parts) >= 2 and path_parts[-1] == "__init__": + path_parts = path_parts[:-1] + + # Module names cannot contain ".", normalize them to "_". This prevents + # a directory having a "." in the name (".env.310" for example) causing extra intermediate modules. + # Also, important to replace "." at the start of paths, as those are considered relative imports. + path_parts = tuple(x.replace(".", "_") for x in path_parts) + + return ".".join(path_parts) + + +def insert_missing_modules(modules: dict[str, ModuleType], module_name: str) -> None: + """ + Used by ``import_path`` to create intermediate modules when using mode=importlib. + + When we want to import a module as "src.tests.test_foo" for example, we need + to create empty modules "src" and "src.tests" after inserting "src.tests.test_foo", + otherwise "src.tests.test_foo" is not importable by ``__import__``. + """ + module_parts = module_name.split(".") + while module_name: + parent_module_name, _, child_name = module_name.rpartition(".") + if parent_module_name: + parent_module = modules.get(parent_module_name) + if parent_module is None: + try: + # If sys.meta_path is empty, calling import_module will issue + # a warning and raise ModuleNotFoundError. To avoid the + # warning, we check sys.meta_path explicitly and raise the error + # ourselves to fall back to creating a dummy module. + if not sys.meta_path: + raise ModuleNotFoundError + parent_module = importlib.import_module(parent_module_name) + except ModuleNotFoundError: + parent_module = ModuleType( + module_name, + doc="Empty module created by pytest's importmode=importlib.", + ) + modules[parent_module_name] = parent_module + + # Add child attribute to the parent that can reference the child + # modules. + if not hasattr(parent_module, child_name): + setattr(parent_module, child_name, modules[module_name]) + + module_parts.pop(-1) + module_name = ".".join(module_parts) + + +def resolve_package_path(path: Path) -> Path | None: + """Return the Python package path by looking for the last + directory upwards which still contains an __init__.py. + + Returns None if it cannot be determined. + """ + result = None + for parent in itertools.chain((path,), path.parents): + if parent.is_dir(): + if not (parent / "__init__.py").is_file(): + break + if not parent.name.isidentifier(): + break + result = parent + return result + + +def resolve_pkg_root_and_module_name( + path: Path, *, consider_namespace_packages: bool = False +) -> tuple[Path, str]: + """ + Return the path to the directory of the root package that contains the + given Python file, and its module name: + + src/ + app/ + __init__.py + core/ + __init__.py + models.py + + Passing the full path to `models.py` will yield Path("src") and "app.core.models". + + If consider_namespace_packages is True, then we additionally check upwards in the hierarchy + for namespace packages: + + https://packaging.python.org/en/latest/guides/packaging-namespace-packages + + Raises CouldNotResolvePathError if the given path does not belong to a package (missing any __init__.py files). + """ + pkg_root: Path | None = None + pkg_path = resolve_package_path(path) + if pkg_path is not None: + pkg_root = pkg_path.parent + if consider_namespace_packages: + start = pkg_root if pkg_root is not None else path.parent + for candidate in (start, *start.parents): + module_name = compute_module_name(candidate, path) + if module_name and is_importable(module_name, path): + # Point the pkg_root to the root of the namespace package. + pkg_root = candidate + break + + if pkg_root is not None: + module_name = compute_module_name(pkg_root, path) + if module_name: + return pkg_root, module_name + + raise CouldNotResolvePathError(f"Could not resolve for {path}") + + +def is_importable(module_name: str, module_path: Path) -> bool: + """ + Return if the given module path could be imported normally by Python, akin to the user + entering the REPL and importing the corresponding module name directly, and corresponds + to the module_path specified. + + :param module_name: + Full module name that we want to check if is importable. + For example, "app.models". + + :param module_path: + Full path to the python module/package we want to check if is importable. + For example, "/projects/src/app/models.py". + """ + try: + # Note this is different from what we do in ``_import_module_using_spec``, where we explicitly search through + # sys.meta_path to be able to pass the path of the module that we want to import (``meta_importer.find_spec``). + # Using importlib.util.find_spec() is different, it gives the same results as trying to import + # the module normally in the REPL. + spec = importlib.util.find_spec(module_name) + except (ImportError, ValueError, ImportWarning): + return False + else: + return spec_matches_module_path(spec, module_path) + + +def compute_module_name(root: Path, module_path: Path) -> str | None: + """Compute a module name based on a path and a root anchor.""" + try: + path_without_suffix = module_path.with_suffix("") + except ValueError: + # Empty paths (such as Path.cwd()) might break meta_path hooks (like our own assertion rewriter). + return None + + try: + relative = path_without_suffix.relative_to(root) + except ValueError: # pragma: no cover + return None + names = list(relative.parts) + if not names: + return None + if names[-1] == "__init__": + names.pop() + return ".".join(names) + + +class CouldNotResolvePathError(Exception): + """Custom exception raised by resolve_pkg_root_and_module_name.""" + + +def scandir( + path: str | os.PathLike[str], + sort_key: Callable[[os.DirEntry[str]], object] = lambda entry: entry.name, +) -> list[os.DirEntry[str]]: + """Scan a directory recursively, in breadth-first order. + + The returned entries are sorted according to the given key. + The default is to sort by name. + If the directory does not exist, return an empty list. + """ + entries = [] + # Attempt to create a scandir iterator for the given path. + try: + scandir_iter = os.scandir(path) + except FileNotFoundError: + # If the directory does not exist, return an empty list. + return [] + # Use the scandir iterator in a context manager to ensure it is properly closed. + with scandir_iter as s: + for entry in s: + try: + entry.is_file() + except OSError as err: + if _ignore_error(err): + continue + # Reraise non-ignorable errors to avoid hiding issues. + raise + entries.append(entry) + entries.sort(key=sort_key) # type: ignore[arg-type] + return entries + + +def visit( + path: str | os.PathLike[str], recurse: Callable[[os.DirEntry[str]], bool] +) -> Iterator[os.DirEntry[str]]: + """Walk a directory recursively, in breadth-first order. + + The `recurse` predicate determines whether a directory is recursed. + + Entries at each directory level are sorted. + """ + entries = scandir(path) + yield from entries + for entry in entries: + if entry.is_dir() and recurse(entry): + yield from visit(entry.path, recurse) + + +def absolutepath(path: str | os.PathLike[str]) -> Path: + """Convert a path to an absolute path using os.path.abspath. + + Prefer this over Path.resolve() (see #6523). + Prefer this over Path.absolute() (not public, doesn't normalize). + """ + return Path(os.path.abspath(path)) + + +def commonpath(path1: Path, path2: Path) -> Path | None: + """Return the common part shared with the other path, or None if there is + no common part. + + If one path is relative and one is absolute, returns None. + """ + try: + return Path(os.path.commonpath((str(path1), str(path2)))) + except ValueError: + return None + + +def bestrelpath(directory: Path, dest: Path) -> str: + """Return a string which is a relative path from directory to dest such + that directory/bestrelpath == dest. + + The paths must be either both absolute or both relative. + + If no such path can be determined, returns dest. + """ + assert isinstance(directory, Path) + assert isinstance(dest, Path) + if dest == directory: + return os.curdir + # Find the longest common directory. + base = commonpath(directory, dest) + # Can be the case on Windows for two absolute paths on different drives. + # Can be the case for two relative paths without common prefix. + # Can be the case for a relative path and an absolute path. + if not base: + return str(dest) + reldirectory = directory.relative_to(base) + reldest = dest.relative_to(base) + return os.path.join( + # Back from directory to base. + *([os.pardir] * len(reldirectory.parts)), + # Forward from base to dest. + *reldest.parts, + ) + + +def safe_exists(p: Path) -> bool: + """Like Path.exists(), but account for input arguments that might be too long (#11394).""" + try: + return p.exists() + except (ValueError, OSError): + # ValueError: stat: path too long for Windows + # OSError: [WinError 123] The filename, directory name, or volume label syntax is incorrect + return False + + +def samefile_nofollow(p1: Path, p2: Path) -> bool: + """Test whether two paths reference the same actual file or directory. + + Unlike Path.samefile(), does not resolve symlinks. + """ + return os.path.samestat(p1.lstat(), p2.lstat()) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/py.typed b/Backend/venv/lib/python3.12/site-packages/_pytest/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/pytester.py b/Backend/venv/lib/python3.12/site-packages/_pytest/pytester.py new file mode 100644 index 00000000..1cd5f05d --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/pytester.py @@ -0,0 +1,1791 @@ +# mypy: allow-untyped-defs +"""(Disabled by default) support for testing pytest and pytest plugins. + +PYTEST_DONT_REWRITE +""" + +from __future__ import annotations + +import collections.abc +from collections.abc import Callable +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import Sequence +import contextlib +from fnmatch import fnmatch +import gc +import importlib +from io import StringIO +import locale +import os +from pathlib import Path +import platform +import re +import shutil +import subprocess +import sys +import traceback +from typing import Any +from typing import Final +from typing import final +from typing import IO +from typing import Literal +from typing import overload +from typing import TextIO +from typing import TYPE_CHECKING +from weakref import WeakKeyDictionary + +from iniconfig import IniConfig +from iniconfig import SectionWrapper + +from _pytest import timing +from _pytest._code import Source +from _pytest.capture import _get_multicapture +from _pytest.compat import NOTSET +from _pytest.compat import NotSetType +from _pytest.config import _PluggyPlugin +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config import hookimpl +from _pytest.config import main +from _pytest.config import PytestPluginManager +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import fixture +from _pytest.fixtures import FixtureRequest +from _pytest.main import Session +from _pytest.monkeypatch import MonkeyPatch +from _pytest.nodes import Collector +from _pytest.nodes import Item +from _pytest.outcomes import fail +from _pytest.outcomes import importorskip +from _pytest.outcomes import skip +from _pytest.pathlib import bestrelpath +from _pytest.pathlib import make_numbered_dir +from _pytest.reports import CollectReport +from _pytest.reports import TestReport +from _pytest.tmpdir import TempPathFactory +from _pytest.warning_types import PytestFDWarning + + +if TYPE_CHECKING: + import pexpect + + +pytest_plugins = ["pytester_assertions"] + + +IGNORE_PAM = [ # filenames added when obtaining details about the current user + "/var/lib/sss/mc/passwd" +] + + +def pytest_addoption(parser: Parser) -> None: + parser.addoption( + "--lsof", + action="store_true", + dest="lsof", + default=False, + help="Run FD checks if lsof is available", + ) + + parser.addoption( + "--runpytest", + default="inprocess", + dest="runpytest", + choices=("inprocess", "subprocess"), + help=( + "Run pytest sub runs in tests using an 'inprocess' " + "or 'subprocess' (python -m main) method" + ), + ) + + parser.addini( + "pytester_example_dir", help="Directory to take the pytester example files from" + ) + + +def pytest_configure(config: Config) -> None: + if config.getvalue("lsof"): + checker = LsofFdLeakChecker() + if checker.matching_platform(): + config.pluginmanager.register(checker) + + config.addinivalue_line( + "markers", + "pytester_example_path(*path_segments): join the given path " + "segments to `pytester_example_dir` for this test.", + ) + + +class LsofFdLeakChecker: + def get_open_files(self) -> list[tuple[str, str]]: + if sys.version_info >= (3, 11): + # New in Python 3.11, ignores utf-8 mode + encoding = locale.getencoding() + else: + encoding = locale.getpreferredencoding(False) + out = subprocess.run( + ("lsof", "-Ffn0", "-p", str(os.getpid())), + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=True, + text=True, + encoding=encoding, + ).stdout + + def isopen(line: str) -> bool: + return line.startswith("f") and ( + "deleted" not in line + and "mem" not in line + and "txt" not in line + and "cwd" not in line + ) + + open_files = [] + + for line in out.split("\n"): + if isopen(line): + fields = line.split("\0") + fd = fields[0][1:] + filename = fields[1][1:] + if filename in IGNORE_PAM: + continue + if filename.startswith("/"): + open_files.append((fd, filename)) + + return open_files + + def matching_platform(self) -> bool: + try: + subprocess.run(("lsof", "-v"), check=True) + except (OSError, subprocess.CalledProcessError): + return False + else: + return True + + @hookimpl(wrapper=True, tryfirst=True) + def pytest_runtest_protocol(self, item: Item) -> Generator[None, object, object]: + lines1 = self.get_open_files() + try: + return (yield) + finally: + if hasattr(sys, "pypy_version_info"): + gc.collect() + lines2 = self.get_open_files() + + new_fds = {t[0] for t in lines2} - {t[0] for t in lines1} + leaked_files = [t for t in lines2 if t[0] in new_fds] + if leaked_files: + error = [ + f"***** {len(leaked_files)} FD leakage detected", + *(str(f) for f in leaked_files), + "*** Before:", + *(str(f) for f in lines1), + "*** After:", + *(str(f) for f in lines2), + f"***** {len(leaked_files)} FD leakage detected", + "*** function {}:{}: {} ".format(*item.location), + "See issue #2366", + ] + item.warn(PytestFDWarning("\n".join(error))) + + +# used at least by pytest-xdist plugin + + +@fixture +def _pytest(request: FixtureRequest) -> PytestArg: + """Return a helper which offers a gethookrecorder(hook) method which + returns a HookRecorder instance which helps to make assertions about called + hooks.""" + return PytestArg(request) + + +class PytestArg: + def __init__(self, request: FixtureRequest) -> None: + self._request = request + + def gethookrecorder(self, hook) -> HookRecorder: + hookrecorder = HookRecorder(hook._pm) + self._request.addfinalizer(hookrecorder.finish_recording) + return hookrecorder + + +def get_public_names(values: Iterable[str]) -> list[str]: + """Only return names from iterator values without a leading underscore.""" + return [x for x in values if x[0] != "_"] + + +@final +class RecordedHookCall: + """A recorded call to a hook. + + The arguments to the hook call are set as attributes. + For example: + + .. code-block:: python + + calls = hook_recorder.getcalls("pytest_runtest_setup") + # Suppose pytest_runtest_setup was called once with `item=an_item`. + assert calls[0].item is an_item + """ + + def __init__(self, name: str, kwargs) -> None: + self.__dict__.update(kwargs) + self._name = name + + def __repr__(self) -> str: + d = self.__dict__.copy() + del d["_name"] + return f"" + + if TYPE_CHECKING: + # The class has undetermined attributes, this tells mypy about it. + def __getattr__(self, key: str): ... + + +@final +class HookRecorder: + """Record all hooks called in a plugin manager. + + Hook recorders are created by :class:`Pytester`. + + This wraps all the hook calls in the plugin manager, recording each call + before propagating the normal calls. + """ + + def __init__( + self, pluginmanager: PytestPluginManager, *, _ispytest: bool = False + ) -> None: + check_ispytest(_ispytest) + + self._pluginmanager = pluginmanager + self.calls: list[RecordedHookCall] = [] + self.ret: int | ExitCode | None = None + + def before(hook_name: str, hook_impls, kwargs) -> None: + self.calls.append(RecordedHookCall(hook_name, kwargs)) + + def after(outcome, hook_name: str, hook_impls, kwargs) -> None: + pass + + self._undo_wrapping = pluginmanager.add_hookcall_monitoring(before, after) + + def finish_recording(self) -> None: + self._undo_wrapping() + + def getcalls(self, names: str | Iterable[str]) -> list[RecordedHookCall]: + """Get all recorded calls to hooks with the given names (or name).""" + if isinstance(names, str): + names = names.split() + return [call for call in self.calls if call._name in names] + + def assert_contains(self, entries: Sequence[tuple[str, str]]) -> None: + __tracebackhide__ = True + i = 0 + entries = list(entries) + # Since Python 3.13, f_locals is not a dict, but eval requires a dict. + backlocals = dict(sys._getframe(1).f_locals) + while entries: + name, check = entries.pop(0) + for ind, call in enumerate(self.calls[i:]): + if call._name == name: + print("NAMEMATCH", name, call) + if eval(check, backlocals, call.__dict__): + print("CHECKERMATCH", repr(check), "->", call) + else: + print("NOCHECKERMATCH", repr(check), "-", call) + continue + i += ind + 1 + break + print("NONAMEMATCH", name, "with", call) + else: + fail(f"could not find {name!r} check {check!r}") + + def popcall(self, name: str) -> RecordedHookCall: + __tracebackhide__ = True + for i, call in enumerate(self.calls): + if call._name == name: + del self.calls[i] + return call + lines = [f"could not find call {name!r}, in:"] + lines.extend([f" {x}" for x in self.calls]) + fail("\n".join(lines)) + + def getcall(self, name: str) -> RecordedHookCall: + values = self.getcalls(name) + assert len(values) == 1, (name, values) + return values[0] + + # functionality for test reports + + @overload + def getreports( + self, + names: Literal["pytest_collectreport"], + ) -> Sequence[CollectReport]: ... + + @overload + def getreports( + self, + names: Literal["pytest_runtest_logreport"], + ) -> Sequence[TestReport]: ... + + @overload + def getreports( + self, + names: str | Iterable[str] = ( + "pytest_collectreport", + "pytest_runtest_logreport", + ), + ) -> Sequence[CollectReport | TestReport]: ... + + def getreports( + self, + names: str | Iterable[str] = ( + "pytest_collectreport", + "pytest_runtest_logreport", + ), + ) -> Sequence[CollectReport | TestReport]: + return [x.report for x in self.getcalls(names)] + + def matchreport( + self, + inamepart: str = "", + names: str | Iterable[str] = ( + "pytest_runtest_logreport", + "pytest_collectreport", + ), + when: str | None = None, + ) -> CollectReport | TestReport: + """Return a testreport whose dotted import path matches.""" + values = [] + for rep in self.getreports(names=names): + if not when and rep.when != "call" and rep.passed: + # setup/teardown passing reports - let's ignore those + continue + if when and rep.when != when: + continue + if not inamepart or inamepart in rep.nodeid.split("::"): + values.append(rep) + if not values: + raise ValueError( + f"could not find test report matching {inamepart!r}: " + "no test reports at all!" + ) + if len(values) > 1: + raise ValueError( + f"found 2 or more testreports matching {inamepart!r}: {values}" + ) + return values[0] + + @overload + def getfailures( + self, + names: Literal["pytest_collectreport"], + ) -> Sequence[CollectReport]: ... + + @overload + def getfailures( + self, + names: Literal["pytest_runtest_logreport"], + ) -> Sequence[TestReport]: ... + + @overload + def getfailures( + self, + names: str | Iterable[str] = ( + "pytest_collectreport", + "pytest_runtest_logreport", + ), + ) -> Sequence[CollectReport | TestReport]: ... + + def getfailures( + self, + names: str | Iterable[str] = ( + "pytest_collectreport", + "pytest_runtest_logreport", + ), + ) -> Sequence[CollectReport | TestReport]: + return [rep for rep in self.getreports(names) if rep.failed] + + def getfailedcollections(self) -> Sequence[CollectReport]: + return self.getfailures("pytest_collectreport") + + def listoutcomes( + self, + ) -> tuple[ + Sequence[TestReport], + Sequence[CollectReport | TestReport], + Sequence[CollectReport | TestReport], + ]: + passed = [] + skipped = [] + failed = [] + for rep in self.getreports( + ("pytest_collectreport", "pytest_runtest_logreport") + ): + if rep.passed: + if rep.when == "call": + assert isinstance(rep, TestReport) + passed.append(rep) + elif rep.skipped: + skipped.append(rep) + else: + assert rep.failed, f"Unexpected outcome: {rep!r}" + failed.append(rep) + return passed, skipped, failed + + def countoutcomes(self) -> list[int]: + return [len(x) for x in self.listoutcomes()] + + def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None: + __tracebackhide__ = True + from _pytest.pytester_assertions import assertoutcome + + outcomes = self.listoutcomes() + assertoutcome( + outcomes, + passed=passed, + skipped=skipped, + failed=failed, + ) + + def clear(self) -> None: + self.calls[:] = [] + + +@fixture +def linecomp() -> LineComp: + """A :class: `LineComp` instance for checking that an input linearly + contains a sequence of strings.""" + return LineComp() + + +@fixture(name="LineMatcher") +def LineMatcher_fixture(request: FixtureRequest) -> type[LineMatcher]: + """A reference to the :class: `LineMatcher`. + + This is instantiable with a list of lines (without their trailing newlines). + This is useful for testing large texts, such as the output of commands. + """ + return LineMatcher + + +@fixture +def pytester( + request: FixtureRequest, tmp_path_factory: TempPathFactory, monkeypatch: MonkeyPatch +) -> Pytester: + """ + Facilities to write tests/configuration files, execute pytest in isolation, and match + against expected output, perfect for black-box testing of pytest plugins. + + It attempts to isolate the test run from external factors as much as possible, modifying + the current working directory to ``path`` and environment variables during initialization. + + It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path` + fixture but provides methods which aid in testing pytest itself. + """ + return Pytester(request, tmp_path_factory, monkeypatch, _ispytest=True) + + +@fixture +def _sys_snapshot() -> Generator[None]: + snappaths = SysPathsSnapshot() + snapmods = SysModulesSnapshot() + yield + snapmods.restore() + snappaths.restore() + + +@fixture +def _config_for_test() -> Generator[Config]: + from _pytest.config import get_config + + config = get_config() + yield config + config._ensure_unconfigure() # cleanup, e.g. capman closing tmpfiles. + + +# Regex to match the session duration string in the summary: "74.34s". +rex_session_duration = re.compile(r"\d+\.\d\ds") +# Regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped". +rex_outcome = re.compile(r"(\d+) (\w+)") + + +@final +class RunResult: + """The result of running a command from :class:`~pytest.Pytester`.""" + + def __init__( + self, + ret: int | ExitCode, + outlines: list[str], + errlines: list[str], + duration: float, + ) -> None: + try: + self.ret: int | ExitCode = ExitCode(ret) + """The return value.""" + except ValueError: + self.ret = ret + self.outlines = outlines + """List of lines captured from stdout.""" + self.errlines = errlines + """List of lines captured from stderr.""" + self.stdout = LineMatcher(outlines) + """:class:`~pytest.LineMatcher` of stdout. + + Use e.g. :func:`str(stdout) ` to reconstruct stdout, or the commonly used + :func:`stdout.fnmatch_lines() ` method. + """ + self.stderr = LineMatcher(errlines) + """:class:`~pytest.LineMatcher` of stderr.""" + self.duration = duration + """Duration in seconds.""" + + def __repr__(self) -> str: + return ( + f"" + ) + + def parseoutcomes(self) -> dict[str, int]: + """Return a dictionary of outcome noun -> count from parsing the terminal + output that the test process produced. + + The returned nouns will always be in plural form:: + + ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ==== + + Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``. + """ + return self.parse_summary_nouns(self.outlines) + + @classmethod + def parse_summary_nouns(cls, lines) -> dict[str, int]: + """Extract the nouns from a pytest terminal summary line. + + It always returns the plural noun for consistency:: + + ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ==== + + Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``. + """ + for line in reversed(lines): + if rex_session_duration.search(line): + outcomes = rex_outcome.findall(line) + ret = {noun: int(count) for (count, noun) in outcomes} + break + else: + raise ValueError("Pytest terminal summary report not found") + + to_plural = { + "warning": "warnings", + "error": "errors", + } + return {to_plural.get(k, k): v for k, v in ret.items()} + + def assert_outcomes( + self, + passed: int = 0, + skipped: int = 0, + failed: int = 0, + errors: int = 0, + xpassed: int = 0, + xfailed: int = 0, + warnings: int | None = None, + deselected: int | None = None, + ) -> None: + """ + Assert that the specified outcomes appear with the respective + numbers (0 means it didn't occur) in the text output from a test run. + + ``warnings`` and ``deselected`` are only checked if not None. + """ + __tracebackhide__ = True + from _pytest.pytester_assertions import assert_outcomes + + outcomes = self.parseoutcomes() + assert_outcomes( + outcomes, + passed=passed, + skipped=skipped, + failed=failed, + errors=errors, + xpassed=xpassed, + xfailed=xfailed, + warnings=warnings, + deselected=deselected, + ) + + +class SysModulesSnapshot: + def __init__(self, preserve: Callable[[str], bool] | None = None) -> None: + self.__preserve = preserve + self.__saved = dict(sys.modules) + + def restore(self) -> None: + if self.__preserve: + self.__saved.update( + (k, m) for k, m in sys.modules.items() if self.__preserve(k) + ) + sys.modules.clear() + sys.modules.update(self.__saved) + + +class SysPathsSnapshot: + def __init__(self) -> None: + self.__saved = list(sys.path), list(sys.meta_path) + + def restore(self) -> None: + sys.path[:], sys.meta_path[:] = self.__saved + + +@final +class Pytester: + """ + Facilities to write tests/configuration files, execute pytest in isolation, and match + against expected output, perfect for black-box testing of pytest plugins. + + It attempts to isolate the test run from external factors as much as possible, modifying + the current working directory to :attr:`path` and environment variables during initialization. + """ + + __test__ = False + + CLOSE_STDIN: Final = NOTSET + + class TimeoutExpired(Exception): + pass + + def __init__( + self, + request: FixtureRequest, + tmp_path_factory: TempPathFactory, + monkeypatch: MonkeyPatch, + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + self._request = request + self._mod_collections: WeakKeyDictionary[Collector, list[Item | Collector]] = ( + WeakKeyDictionary() + ) + if request.function: + name: str = request.function.__name__ + else: + name = request.node.name + self._name = name + self._path: Path = tmp_path_factory.mktemp(name, numbered=True) + #: A list of plugins to use with :py:meth:`parseconfig` and + #: :py:meth:`runpytest`. Initially this is an empty list but plugins can + #: be added to the list. + #: + #: When running in subprocess mode, specify plugins by name (str) - adding + #: plugin objects directly is not supported. + self.plugins: list[str | _PluggyPlugin] = [] + self._sys_path_snapshot = SysPathsSnapshot() + self._sys_modules_snapshot = self.__take_sys_modules_snapshot() + self._request.addfinalizer(self._finalize) + self._method = self._request.config.getoption("--runpytest") + self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True) + + self._monkeypatch = mp = monkeypatch + self.chdir() + mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot)) + # Ensure no unexpected caching via tox. + mp.delenv("TOX_ENV_DIR", raising=False) + # Discard outer pytest options. + mp.delenv("PYTEST_ADDOPTS", raising=False) + # Ensure no user config is used. + tmphome = str(self.path) + mp.setenv("HOME", tmphome) + mp.setenv("USERPROFILE", tmphome) + # Do not use colors for inner runs by default. + mp.setenv("PY_COLORS", "0") + + @property + def path(self) -> Path: + """Temporary directory path used to create files/run tests from, etc.""" + return self._path + + def __repr__(self) -> str: + return f"" + + def _finalize(self) -> None: + """ + Clean up global state artifacts. + + Some methods modify the global interpreter state and this tries to + clean this up. It does not remove the temporary directory however so + it can be looked at after the test run has finished. + """ + self._sys_modules_snapshot.restore() + self._sys_path_snapshot.restore() + + def __take_sys_modules_snapshot(self) -> SysModulesSnapshot: + # Some zope modules used by twisted-related tests keep internal state + # and can't be deleted; we had some trouble in the past with + # `zope.interface` for example. + # + # Preserve readline due to https://bugs.python.org/issue41033. + # pexpect issues a SIGWINCH. + def preserve_module(name): + return name.startswith(("zope", "readline")) + + return SysModulesSnapshot(preserve=preserve_module) + + def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder: + """Create a new :class:`HookRecorder` for a :class:`PytestPluginManager`.""" + pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True) # type: ignore[attr-defined] + self._request.addfinalizer(reprec.finish_recording) + return reprec + + def chdir(self) -> None: + """Cd into the temporary directory. + + This is done automatically upon instantiation. + """ + self._monkeypatch.chdir(self.path) + + def _makefile( + self, + ext: str, + lines: Sequence[Any | bytes], + files: dict[str, str], + encoding: str = "utf-8", + ) -> Path: + items = list(files.items()) + + if ext is None: + raise TypeError("ext must not be None") + + if ext and not ext.startswith("."): + raise ValueError( + f"pytester.makefile expects a file extension, try .{ext} instead of {ext}" + ) + + def to_text(s: Any | bytes) -> str: + return s.decode(encoding) if isinstance(s, bytes) else str(s) + + if lines: + source = "\n".join(to_text(x) for x in lines) + basename = self._name + items.insert(0, (basename, source)) + + ret = None + for basename, value in items: + p = self.path.joinpath(basename).with_suffix(ext) + p.parent.mkdir(parents=True, exist_ok=True) + source_ = Source(value) + source = "\n".join(to_text(line) for line in source_.lines) + p.write_text(source.strip(), encoding=encoding) + if ret is None: + ret = p + assert ret is not None + return ret + + def makefile(self, ext: str, *args: str, **kwargs: str) -> Path: + r"""Create new text file(s) in the test directory. + + :param ext: + The extension the file(s) should use, including the dot, e.g. `.py`. + :param args: + All args are treated as strings and joined using newlines. + The result is written as contents to the file. The name of the + file is based on the test function requesting this fixture. + :param kwargs: + Each keyword is the name of a file, while the value of it will + be written as contents of the file. + :returns: + The first created file. + + Examples: + + .. code-block:: python + + pytester.makefile(".txt", "line1", "line2") + + pytester.makefile(".ini", pytest="[pytest]\naddopts=-rs\n") + + To create binary files, use :meth:`pathlib.Path.write_bytes` directly: + + .. code-block:: python + + filename = pytester.path.joinpath("foo.bin") + filename.write_bytes(b"...") + """ + return self._makefile(ext, args, kwargs) + + def makeconftest(self, source: str) -> Path: + """Write a conftest.py file. + + :param source: The contents. + :returns: The conftest.py file. + """ + return self.makepyfile(conftest=source) + + def makeini(self, source: str) -> Path: + """Write a tox.ini file. + + :param source: The contents. + :returns: The tox.ini file. + """ + return self.makefile(".ini", tox=source) + + def maketoml(self, source: str) -> Path: + """Write a pytest.toml file. + + :param source: The contents. + :returns: The pytest.toml file. + + .. versionadded:: 9.0 + """ + return self.makefile(".toml", pytest=source) + + def getinicfg(self, source: str) -> SectionWrapper: + """Return the pytest section from the tox.ini config file.""" + p = self.makeini(source) + return IniConfig(str(p))["pytest"] + + def makepyprojecttoml(self, source: str) -> Path: + """Write a pyproject.toml file. + + :param source: The contents. + :returns: The pyproject.ini file. + + .. versionadded:: 6.0 + """ + return self.makefile(".toml", pyproject=source) + + def makepyfile(self, *args, **kwargs) -> Path: + r"""Shortcut for .makefile() with a .py extension. + + Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting + existing files. + + Examples: + + .. code-block:: python + + def test_something(pytester): + # Initial file is created test_something.py. + pytester.makepyfile("foobar") + # To create multiple files, pass kwargs accordingly. + pytester.makepyfile(custom="foobar") + # At this point, both 'test_something.py' & 'custom.py' exist in the test directory. + + """ + return self._makefile(".py", args, kwargs) + + def maketxtfile(self, *args, **kwargs) -> Path: + r"""Shortcut for .makefile() with a .txt extension. + + Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting + existing files. + + Examples: + + .. code-block:: python + + def test_something(pytester): + # Initial file is created test_something.txt. + pytester.maketxtfile("foobar") + # To create multiple files, pass kwargs accordingly. + pytester.maketxtfile(custom="foobar") + # At this point, both 'test_something.txt' & 'custom.txt' exist in the test directory. + + """ + return self._makefile(".txt", args, kwargs) + + def syspathinsert(self, path: str | os.PathLike[str] | None = None) -> None: + """Prepend a directory to sys.path, defaults to :attr:`path`. + + This is undone automatically when this object dies at the end of each + test. + + :param path: + The path. + """ + if path is None: + path = self.path + + self._monkeypatch.syspath_prepend(str(path)) + + def mkdir(self, name: str | os.PathLike[str]) -> Path: + """Create a new (sub)directory. + + :param name: + The name of the directory, relative to the pytester path. + :returns: + The created directory. + :rtype: pathlib.Path + """ + p = self.path / name + p.mkdir() + return p + + def mkpydir(self, name: str | os.PathLike[str]) -> Path: + """Create a new python package. + + This creates a (sub)directory with an empty ``__init__.py`` file so it + gets recognised as a Python package. + """ + p = self.path / name + p.mkdir() + p.joinpath("__init__.py").touch() + return p + + def copy_example(self, name: str | None = None) -> Path: + """Copy file from project's directory into the testdir. + + :param name: + The name of the file to copy. + :return: + Path to the copied directory (inside ``self.path``). + :rtype: pathlib.Path + """ + example_dir_ = self._request.config.getini("pytester_example_dir") + if example_dir_ is None: + raise ValueError("pytester_example_dir is unset, can't copy examples") + example_dir: Path = self._request.config.rootpath / example_dir_ + + for extra_element in self._request.node.iter_markers("pytester_example_path"): + assert extra_element.args + example_dir = example_dir.joinpath(*extra_element.args) + + if name is None: + func_name = self._name + maybe_dir = example_dir / func_name + maybe_file = example_dir / (func_name + ".py") + + if maybe_dir.is_dir(): + example_path = maybe_dir + elif maybe_file.is_file(): + example_path = maybe_file + else: + raise LookupError( + f"{func_name} can't be found as module or package in {example_dir}" + ) + else: + example_path = example_dir.joinpath(name) + + if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file(): + shutil.copytree(example_path, self.path, symlinks=True, dirs_exist_ok=True) + return self.path + elif example_path.is_file(): + result = self.path.joinpath(example_path.name) + shutil.copy(example_path, result) + return result + else: + raise LookupError( + f'example "{example_path}" is not found as a file or directory' + ) + + def getnode(self, config: Config, arg: str | os.PathLike[str]) -> Collector | Item: + """Get the collection node of a file. + + :param config: + A pytest config. + See :py:meth:`parseconfig` and :py:meth:`parseconfigure` for creating it. + :param arg: + Path to the file. + :returns: + The node. + """ + session = Session.from_config(config) + assert "::" not in str(arg) + p = Path(os.path.abspath(arg)) + config.hook.pytest_sessionstart(session=session) + res = session.perform_collect([str(p)], genitems=False)[0] + config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) + return res + + def getpathnode(self, path: str | os.PathLike[str]) -> Collector | Item: + """Return the collection node of a file. + + This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to + create the (configured) pytest Config instance. + + :param path: + Path to the file. + :returns: + The node. + """ + path = Path(path) + config = self.parseconfigure(path) + session = Session.from_config(config) + x = bestrelpath(session.path, path) + config.hook.pytest_sessionstart(session=session) + res = session.perform_collect([x], genitems=False)[0] + config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) + return res + + def genitems(self, colitems: Sequence[Item | Collector]) -> list[Item]: + """Generate all test items from a collection node. + + This recurses into the collection node and returns a list of all the + test items contained within. + + :param colitems: + The collection nodes. + :returns: + The collected items. + """ + session = colitems[0].session + result: list[Item] = [] + for colitem in colitems: + result.extend(session.genitems(colitem)) + return result + + def runitem(self, source: str) -> Any: + """Run the "test_func" Item. + + The calling test instance (class containing the test method) must + provide a ``.getrunner()`` method which should return a runner which + can run the test protocol for a single item, e.g. + ``_pytest.runner.runtestprotocol``. + """ + # used from runner functional tests + item = self.getitem(source) + # the test class where we are called from wants to provide the runner + testclassinstance = self._request.instance + runner = testclassinstance.getrunner() + return runner(item) + + def inline_runsource(self, source: str, *cmdlineargs) -> HookRecorder: + """Run a test module in process using ``pytest.main()``. + + This run writes "source" into a temporary file and runs + ``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance + for the result. + + :param source: The source code of the test module. + :param cmdlineargs: Any extra command line arguments to use. + """ + p = self.makepyfile(source) + values = [*list(cmdlineargs), p] + return self.inline_run(*values) + + def inline_genitems(self, *args) -> tuple[list[Item], HookRecorder]: + """Run ``pytest.main(['--collect-only'])`` in-process. + + Runs the :py:func:`pytest.main` function to run all of pytest inside + the test process itself like :py:meth:`inline_run`, but returns a + tuple of the collected items and a :py:class:`HookRecorder` instance. + """ + rec = self.inline_run("--collect-only", *args) + items = [x.item for x in rec.getcalls("pytest_itemcollected")] + return items, rec + + def inline_run( + self, + *args: str | os.PathLike[str], + plugins=(), + no_reraise_ctrlc: bool = False, + ) -> HookRecorder: + """Run ``pytest.main()`` in-process, returning a HookRecorder. + + Runs the :py:func:`pytest.main` function to run all of pytest inside + the test process itself. This means it can return a + :py:class:`HookRecorder` instance which gives more detailed results + from that run than can be done by matching stdout/stderr from + :py:meth:`runpytest`. + + :param args: + Command line arguments to pass to :py:func:`pytest.main`. + :param plugins: + Extra plugin instances the ``pytest.main()`` instance should use. + :param no_reraise_ctrlc: + Typically we reraise keyboard interrupts from the child run. If + True, the KeyboardInterrupt exception is captured. + """ + from _pytest.unraisableexception import gc_collect_iterations_key + + # (maybe a cpython bug?) the importlib cache sometimes isn't updated + # properly between file creation and inline_run (especially if imports + # are interspersed with file creation) + importlib.invalidate_caches() + + plugins = list(plugins) + finalizers = [] + try: + # Any sys.module or sys.path changes done while running pytest + # inline should be reverted after the test run completes to avoid + # clashing with later inline tests run within the same pytest test, + # e.g. just because they use matching test module names. + finalizers.append(self.__take_sys_modules_snapshot().restore) + finalizers.append(SysPathsSnapshot().restore) + + # Important note: + # - our tests should not leave any other references/registrations + # laying around other than possibly loaded test modules + # referenced from sys.modules, as nothing will clean those up + # automatically + + rec = [] + + class PytesterHelperPlugin: + @staticmethod + def pytest_configure(config: Config) -> None: + rec.append(self.make_hook_recorder(config.pluginmanager)) + + # The unraisable plugin GC collect slows down inline + # pytester runs too much. + config.stash[gc_collect_iterations_key] = 0 + + plugins.append(PytesterHelperPlugin()) + ret = main([str(x) for x in args], plugins=plugins) + if len(rec) == 1: + reprec = rec.pop() + else: + + class reprec: # type: ignore + pass + + reprec.ret = ret + + # Typically we reraise keyboard interrupts from the child run + # because it's our user requesting interruption of the testing. + if ret == ExitCode.INTERRUPTED and not no_reraise_ctrlc: + calls = reprec.getcalls("pytest_keyboard_interrupt") + if calls and calls[-1].excinfo.type == KeyboardInterrupt: + raise KeyboardInterrupt() + return reprec + finally: + for finalizer in finalizers: + finalizer() + + def runpytest_inprocess( + self, *args: str | os.PathLike[str], **kwargs: Any + ) -> RunResult: + """Return result of running pytest in-process, providing a similar + interface to what self.runpytest() provides.""" + syspathinsert = kwargs.pop("syspathinsert", False) + + if syspathinsert: + self.syspathinsert() + instant = timing.Instant() + capture = _get_multicapture("sys") + capture.start_capturing() + try: + try: + reprec = self.inline_run(*args, **kwargs) + except SystemExit as e: + ret = e.args[0] + try: + ret = ExitCode(e.args[0]) + except ValueError: + pass + + class reprec: # type: ignore + ret = ret + + except Exception: + traceback.print_exc() + + class reprec: # type: ignore + ret = ExitCode(3) + + finally: + out, err = capture.readouterr() + capture.stop_capturing() + sys.stdout.write(out) + sys.stderr.write(err) + + assert reprec.ret is not None + res = RunResult( + reprec.ret, out.splitlines(), err.splitlines(), instant.elapsed().seconds + ) + res.reprec = reprec # type: ignore + return res + + def runpytest(self, *args: str | os.PathLike[str], **kwargs: Any) -> RunResult: + """Run pytest inline or in a subprocess, depending on the command line + option "--runpytest" and return a :py:class:`~pytest.RunResult`.""" + new_args = self._ensure_basetemp(args) + if self._method == "inprocess": + return self.runpytest_inprocess(*new_args, **kwargs) + elif self._method == "subprocess": + return self.runpytest_subprocess(*new_args, **kwargs) + raise RuntimeError(f"Unrecognized runpytest option: {self._method}") + + def _ensure_basetemp( + self, args: Sequence[str | os.PathLike[str]] + ) -> list[str | os.PathLike[str]]: + new_args = list(args) + for x in new_args: + if str(x).startswith("--basetemp"): + break + else: + new_args.append( + "--basetemp={}".format(self.path.parent.joinpath("basetemp")) + ) + return new_args + + def parseconfig(self, *args: str | os.PathLike[str]) -> Config: + """Return a new pytest :class:`pytest.Config` instance from given + commandline args. + + This invokes the pytest bootstrapping code in _pytest.config to create a + new :py:class:`pytest.PytestPluginManager` and call the + :hook:`pytest_cmdline_parse` hook to create a new :class:`pytest.Config` + instance. + + If :attr:`plugins` has been populated they should be plugin modules + to be registered with the plugin manager. + """ + import _pytest.config + + new_args = [str(x) for x in self._ensure_basetemp(args)] + + config = _pytest.config._prepareconfig(new_args, self.plugins) + # we don't know what the test will do with this half-setup config + # object and thus we make sure it gets unconfigured properly in any + # case (otherwise capturing could still be active, for example) + self._request.addfinalizer(config._ensure_unconfigure) + return config + + def parseconfigure(self, *args: str | os.PathLike[str]) -> Config: + """Return a new pytest configured Config instance. + + Returns a new :py:class:`pytest.Config` instance like + :py:meth:`parseconfig`, but also calls the :hook:`pytest_configure` + hook. + """ + config = self.parseconfig(*args) + config._do_configure() + return config + + def getitem( + self, source: str | os.PathLike[str], funcname: str = "test_func" + ) -> Item: + """Return the test item for a test function. + + Writes the source to a python file and runs pytest's collection on + the resulting module, returning the test item for the requested + function name. + + :param source: + The module source. + :param funcname: + The name of the test function for which to return a test item. + :returns: + The test item. + """ + items = self.getitems(source) + for item in items: + if item.name == funcname: + return item + assert 0, f"{funcname!r} item not found in module:\n{source}\nitems: {items}" + + def getitems(self, source: str | os.PathLike[str]) -> list[Item]: + """Return all test items collected from the module. + + Writes the source to a Python file and runs pytest's collection on + the resulting module, returning all test items contained within. + """ + modcol = self.getmodulecol(source) + return self.genitems([modcol]) + + def getmodulecol( + self, + source: str | os.PathLike[str], + configargs=(), + *, + withinit: bool = False, + ): + """Return the module collection node for ``source``. + + Writes ``source`` to a file using :py:meth:`makepyfile` and then + runs the pytest collection on it, returning the collection node for the + test module. + + :param source: + The source code of the module to collect. + + :param configargs: + Any extra arguments to pass to :py:meth:`parseconfigure`. + + :param withinit: + Whether to also write an ``__init__.py`` file to the same + directory to ensure it is a package. + """ + if isinstance(source, os.PathLike): + path = self.path.joinpath(source) + assert not withinit, "not supported for paths" + else: + kw = {self._name: str(source)} + path = self.makepyfile(**kw) + if withinit: + self.makepyfile(__init__="#") + self.config = config = self.parseconfigure(path, *configargs) + return self.getnode(config, path) + + def collect_by_name(self, modcol: Collector, name: str) -> Item | Collector | None: + """Return the collection node for name from the module collection. + + Searches a module collection node for a collection node matching the + given name. + + :param modcol: A module collection node; see :py:meth:`getmodulecol`. + :param name: The name of the node to return. + """ + if modcol not in self._mod_collections: + self._mod_collections[modcol] = list(modcol.collect()) + for colitem in self._mod_collections[modcol]: + if colitem.name == name: + return colitem + return None + + def popen( + self, + cmdargs: Sequence[str | os.PathLike[str]], + stdout: int | TextIO = subprocess.PIPE, + stderr: int | TextIO = subprocess.PIPE, + stdin: NotSetType | bytes | IO[Any] | int = CLOSE_STDIN, + **kw, + ): + """Invoke :py:class:`subprocess.Popen`. + + Calls :py:class:`subprocess.Popen` making sure the current working + directory is in ``PYTHONPATH``. + + You probably want to use :py:meth:`run` instead. + """ + env = os.environ.copy() + env["PYTHONPATH"] = os.pathsep.join( + filter(None, [os.getcwd(), env.get("PYTHONPATH", "")]) + ) + kw["env"] = env + + if stdin is self.CLOSE_STDIN: + kw["stdin"] = subprocess.PIPE + elif isinstance(stdin, bytes): + kw["stdin"] = subprocess.PIPE + else: + kw["stdin"] = stdin + + popen = subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) + if stdin is self.CLOSE_STDIN: + assert popen.stdin is not None + popen.stdin.close() + elif isinstance(stdin, bytes): + assert popen.stdin is not None + popen.stdin.write(stdin) + + return popen + + def run( + self, + *cmdargs: str | os.PathLike[str], + timeout: float | None = None, + stdin: NotSetType | bytes | IO[Any] | int = CLOSE_STDIN, + ) -> RunResult: + """Run a command with arguments. + + Run a process using :py:class:`subprocess.Popen` saving the stdout and + stderr. + + :param cmdargs: + The sequence of arguments to pass to :py:class:`subprocess.Popen`, + with path-like objects being converted to :py:class:`str` + automatically. + :param timeout: + The period in seconds after which to timeout and raise + :py:class:`Pytester.TimeoutExpired`. + :param stdin: + Optional standard input. + + - If it is ``CLOSE_STDIN`` (Default), then this method calls + :py:class:`subprocess.Popen` with ``stdin=subprocess.PIPE``, and + the standard input is closed immediately after the new command is + started. + + - If it is of type :py:class:`bytes`, these bytes are sent to the + standard input of the command. + + - Otherwise, it is passed through to :py:class:`subprocess.Popen`. + For further information in this case, consult the document of the + ``stdin`` parameter in :py:class:`subprocess.Popen`. + :type stdin: _pytest.compat.NotSetType | bytes | IO[Any] | int + :returns: + The result. + + """ + __tracebackhide__ = True + + cmdargs = tuple(os.fspath(arg) for arg in cmdargs) + p1 = self.path.joinpath("stdout") + p2 = self.path.joinpath("stderr") + print("running:", *cmdargs) + print(" in:", Path.cwd()) + + with p1.open("w", encoding="utf8") as f1, p2.open("w", encoding="utf8") as f2: + instant = timing.Instant() + popen = self.popen( + cmdargs, + stdin=stdin, + stdout=f1, + stderr=f2, + ) + if popen.stdin is not None: + popen.stdin.close() + + def handle_timeout() -> None: + __tracebackhide__ = True + + timeout_message = f"{timeout} second timeout expired running: {cmdargs}" + + popen.kill() + popen.wait() + raise self.TimeoutExpired(timeout_message) + + if timeout is None: + ret = popen.wait() + else: + try: + ret = popen.wait(timeout) + except subprocess.TimeoutExpired: + handle_timeout() + f1.flush() + f2.flush() + + with p1.open(encoding="utf8") as f1, p2.open(encoding="utf8") as f2: + out = f1.read().splitlines() + err = f2.read().splitlines() + + self._dump_lines(out, sys.stdout) + self._dump_lines(err, sys.stderr) + + with contextlib.suppress(ValueError): + ret = ExitCode(ret) + return RunResult(ret, out, err, instant.elapsed().seconds) + + def _dump_lines(self, lines, fp): + try: + for line in lines: + print(line, file=fp) + except UnicodeEncodeError: + print(f"couldn't print to {fp} because of encoding") + + def _getpytestargs(self) -> tuple[str, ...]: + return sys.executable, "-mpytest" + + def runpython(self, script: os.PathLike[str]) -> RunResult: + """Run a python script using sys.executable as interpreter.""" + return self.run(sys.executable, script) + + def runpython_c(self, command: str) -> RunResult: + """Run ``python -c "command"``.""" + return self.run(sys.executable, "-c", command) + + def runpytest_subprocess( + self, *args: str | os.PathLike[str], timeout: float | None = None + ) -> RunResult: + """Run pytest as a subprocess with given arguments. + + Any plugins added to the :py:attr:`plugins` list will be added using the + ``-p`` command line option. Additionally ``--basetemp`` is used to put + any temporary files and directories in a numbered directory prefixed + with "runpytest-" to not conflict with the normal numbered pytest + location for temporary files and directories. + + :param args: + The sequence of arguments to pass to the pytest subprocess. + :param timeout: + The period in seconds after which to timeout and raise + :py:class:`Pytester.TimeoutExpired`. + :returns: + The result. + """ + __tracebackhide__ = True + p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700) + args = (f"--basetemp={p}", *args) + for plugin in self.plugins: + if not isinstance(plugin, str): + raise ValueError( + f"Specifying plugins as objects is not supported in pytester subprocess mode; " + f"specify by name instead: {plugin}" + ) + args = ("-p", plugin, *args) + args = self._getpytestargs() + args + return self.run(*args, timeout=timeout) + + def spawn_pytest(self, string: str, expect_timeout: float = 10.0) -> pexpect.spawn: + """Run pytest using pexpect. + + This makes sure to use the right pytest and sets up the temporary + directory locations. + + The pexpect child is returned. + """ + basetemp = self.path / "temp-pexpect" + basetemp.mkdir(mode=0o700) + invoke = " ".join(map(str, self._getpytestargs())) + cmd = f"{invoke} --basetemp={basetemp} {string}" + return self.spawn(cmd, expect_timeout=expect_timeout) + + def spawn(self, cmd: str, expect_timeout: float = 10.0) -> pexpect.spawn: + """Run a command using pexpect. + + The pexpect child is returned. + """ + pexpect = importorskip("pexpect", "3.0") + if hasattr(sys, "pypy_version_info") and "64" in platform.machine(): + skip("pypy-64 bit not supported") + if not hasattr(pexpect, "spawn"): + skip("pexpect.spawn not available") + logfile = self.path.joinpath("spawn.out").open("wb") + + child = pexpect.spawn(cmd, logfile=logfile, timeout=expect_timeout) + self._request.addfinalizer(logfile.close) + return child + + +class LineComp: + def __init__(self) -> None: + self.stringio = StringIO() + """:class:`python:io.StringIO()` instance used for input.""" + + def assert_contains_lines(self, lines2: Sequence[str]) -> None: + """Assert that ``lines2`` are contained (linearly) in :attr:`stringio`'s value. + + Lines are matched using :func:`LineMatcher.fnmatch_lines `. + """ + __tracebackhide__ = True + val = self.stringio.getvalue() + self.stringio.truncate(0) + self.stringio.seek(0) + lines1 = val.split("\n") + LineMatcher(lines1).fnmatch_lines(lines2) + + +class LineMatcher: + """Flexible matching of text. + + This is a convenience class to test large texts like the output of + commands. + + The constructor takes a list of lines without their trailing newlines, i.e. + ``text.splitlines()``. + """ + + def __init__(self, lines: list[str]) -> None: + self.lines = lines + self._log_output: list[str] = [] + + def __str__(self) -> str: + """Return the entire original text. + + .. versionadded:: 6.2 + You can use :meth:`str` in older versions. + """ + return "\n".join(self.lines) + + def _getlines(self, lines2: str | Sequence[str] | Source) -> Sequence[str]: + if isinstance(lines2, str): + lines2 = Source(lines2) + if isinstance(lines2, Source): + lines2 = lines2.strip().lines + return lines2 + + def fnmatch_lines_random(self, lines2: Sequence[str]) -> None: + """Check lines exist in the output in any order (using :func:`python:fnmatch.fnmatch`).""" + __tracebackhide__ = True + self._match_lines_random(lines2, fnmatch) + + def re_match_lines_random(self, lines2: Sequence[str]) -> None: + """Check lines exist in the output in any order (using :func:`python:re.match`).""" + __tracebackhide__ = True + self._match_lines_random(lines2, lambda name, pat: bool(re.match(pat, name))) + + def _match_lines_random( + self, lines2: Sequence[str], match_func: Callable[[str, str], bool] + ) -> None: + __tracebackhide__ = True + lines2 = self._getlines(lines2) + for line in lines2: + for x in self.lines: + if line == x or match_func(x, line): + self._log("matched: ", repr(line)) + break + else: + msg = f"line {line!r} not found in output" + self._log(msg) + self._fail(msg) + + def get_lines_after(self, fnline: str) -> Sequence[str]: + """Return all lines following the given line in the text. + + The given line can contain glob wildcards. + """ + for i, line in enumerate(self.lines): + if fnline == line or fnmatch(line, fnline): + return self.lines[i + 1 :] + raise ValueError(f"line {fnline!r} not found in output") + + def _log(self, *args) -> None: + self._log_output.append(" ".join(str(x) for x in args)) + + @property + def _log_text(self) -> str: + return "\n".join(self._log_output) + + def fnmatch_lines( + self, lines2: Sequence[str], *, consecutive: bool = False + ) -> None: + """Check lines exist in the output (using :func:`python:fnmatch.fnmatch`). + + The argument is a list of lines which have to match and can use glob + wildcards. If they do not match a pytest.fail() is called. The + matches and non-matches are also shown as part of the error message. + + :param lines2: String patterns to match. + :param consecutive: Match lines consecutively? + """ + __tracebackhide__ = True + self._match_lines(lines2, fnmatch, "fnmatch", consecutive=consecutive) + + def re_match_lines( + self, lines2: Sequence[str], *, consecutive: bool = False + ) -> None: + """Check lines exist in the output (using :func:`python:re.match`). + + The argument is a list of lines which have to match using ``re.match``. + If they do not match a pytest.fail() is called. + + The matches and non-matches are also shown as part of the error message. + + :param lines2: string patterns to match. + :param consecutive: match lines consecutively? + """ + __tracebackhide__ = True + self._match_lines( + lines2, + lambda name, pat: bool(re.match(pat, name)), + "re.match", + consecutive=consecutive, + ) + + def _match_lines( + self, + lines2: Sequence[str], + match_func: Callable[[str, str], bool], + match_nickname: str, + *, + consecutive: bool = False, + ) -> None: + """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``. + + :param Sequence[str] lines2: + List of string patterns to match. The actual format depends on + ``match_func``. + :param match_func: + A callable ``match_func(line, pattern)`` where line is the + captured line from stdout/stderr and pattern is the matching + pattern. + :param str match_nickname: + The nickname for the match function that will be logged to stdout + when a match occurs. + :param consecutive: + Match lines consecutively? + """ + if not isinstance(lines2, collections.abc.Sequence): + raise TypeError(f"invalid type for lines2: {type(lines2).__name__}") + lines2 = self._getlines(lines2) + lines1 = self.lines[:] + extralines = [] + __tracebackhide__ = True + wnick = len(match_nickname) + 1 + started = False + for line in lines2: + nomatchprinted = False + while lines1: + nextline = lines1.pop(0) + if line == nextline: + self._log("exact match:", repr(line)) + started = True + break + elif match_func(nextline, line): + self._log(f"{match_nickname}:", repr(line)) + self._log( + "{:>{width}}".format("with:", width=wnick), repr(nextline) + ) + started = True + break + else: + if consecutive and started: + msg = f"no consecutive match: {line!r}" + self._log(msg) + self._log( + "{:>{width}}".format("with:", width=wnick), repr(nextline) + ) + self._fail(msg) + if not nomatchprinted: + self._log( + "{:>{width}}".format("nomatch:", width=wnick), repr(line) + ) + nomatchprinted = True + self._log("{:>{width}}".format("and:", width=wnick), repr(nextline)) + extralines.append(nextline) + else: + msg = f"remains unmatched: {line!r}" + self._log(msg) + self._fail(msg) + self._log_output = [] + + def no_fnmatch_line(self, pat: str) -> None: + """Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``. + + :param str pat: The pattern to match lines. + """ + __tracebackhide__ = True + self._no_match_line(pat, fnmatch, "fnmatch") + + def no_re_match_line(self, pat: str) -> None: + """Ensure captured lines do not match the given pattern, using ``re.match``. + + :param str pat: The regular expression to match lines. + """ + __tracebackhide__ = True + self._no_match_line( + pat, lambda name, pat: bool(re.match(pat, name)), "re.match" + ) + + def _no_match_line( + self, pat: str, match_func: Callable[[str, str], bool], match_nickname: str + ) -> None: + """Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch``. + + :param str pat: The pattern to match lines. + """ + __tracebackhide__ = True + nomatch_printed = False + wnick = len(match_nickname) + 1 + for line in self.lines: + if match_func(line, pat): + msg = f"{match_nickname}: {pat!r}" + self._log(msg) + self._log("{:>{width}}".format("with:", width=wnick), repr(line)) + self._fail(msg) + else: + if not nomatch_printed: + self._log("{:>{width}}".format("nomatch:", width=wnick), repr(pat)) + nomatch_printed = True + self._log("{:>{width}}".format("and:", width=wnick), repr(line)) + self._log_output = [] + + def _fail(self, msg: str) -> None: + __tracebackhide__ = True + log_text = self._log_text + self._log_output = [] + fail(log_text) + + def str(self) -> str: + """Return the entire original text.""" + return str(self) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/pytester_assertions.py b/Backend/venv/lib/python3.12/site-packages/_pytest/pytester_assertions.py new file mode 100644 index 00000000..915cc8a1 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/pytester_assertions.py @@ -0,0 +1,74 @@ +"""Helper plugin for pytester; should not be loaded on its own.""" + +# This plugin contains assertions used by pytester. pytester cannot +# contain them itself, since it is imported by the `pytest` module, +# hence cannot be subject to assertion rewriting, which requires a +# module to not be already imported. +from __future__ import annotations + +from collections.abc import Sequence + +from _pytest.reports import CollectReport +from _pytest.reports import TestReport + + +def assertoutcome( + outcomes: tuple[ + Sequence[TestReport], + Sequence[CollectReport | TestReport], + Sequence[CollectReport | TestReport], + ], + passed: int = 0, + skipped: int = 0, + failed: int = 0, +) -> None: + __tracebackhide__ = True + + realpassed, realskipped, realfailed = outcomes + obtained = { + "passed": len(realpassed), + "skipped": len(realskipped), + "failed": len(realfailed), + } + expected = {"passed": passed, "skipped": skipped, "failed": failed} + assert obtained == expected, outcomes + + +def assert_outcomes( + outcomes: dict[str, int], + passed: int = 0, + skipped: int = 0, + failed: int = 0, + errors: int = 0, + xpassed: int = 0, + xfailed: int = 0, + warnings: int | None = None, + deselected: int | None = None, +) -> None: + """Assert that the specified outcomes appear with the respective + numbers (0 means it didn't occur) in the text output from a test run.""" + __tracebackhide__ = True + + obtained = { + "passed": outcomes.get("passed", 0), + "skipped": outcomes.get("skipped", 0), + "failed": outcomes.get("failed", 0), + "errors": outcomes.get("errors", 0), + "xpassed": outcomes.get("xpassed", 0), + "xfailed": outcomes.get("xfailed", 0), + } + expected = { + "passed": passed, + "skipped": skipped, + "failed": failed, + "errors": errors, + "xpassed": xpassed, + "xfailed": xfailed, + } + if warnings is not None: + obtained["warnings"] = outcomes.get("warnings", 0) + expected["warnings"] = warnings + if deselected is not None: + obtained["deselected"] = outcomes.get("deselected", 0) + expected["deselected"] = deselected + assert obtained == expected diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/python.py b/Backend/venv/lib/python3.12/site-packages/_pytest/python.py new file mode 100644 index 00000000..e6375187 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/python.py @@ -0,0 +1,1772 @@ +# mypy: allow-untyped-defs +"""Python test discovery, setup and run of test functions.""" + +from __future__ import annotations + +import abc +from collections import Counter +from collections import defaultdict +from collections.abc import Callable +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Mapping +from collections.abc import Sequence +import dataclasses +import enum +import fnmatch +from functools import partial +import inspect +import itertools +import os +from pathlib import Path +import re +import textwrap +import types +from typing import Any +from typing import cast +from typing import final +from typing import Literal +from typing import NoReturn +from typing import TYPE_CHECKING +import warnings + +import _pytest +from _pytest import fixtures +from _pytest import nodes +from _pytest._code import filter_traceback +from _pytest._code import getfslineno +from _pytest._code.code import ExceptionInfo +from _pytest._code.code import TerminalRepr +from _pytest._code.code import Traceback +from _pytest._io.saferepr import saferepr +from _pytest.compat import ascii_escaped +from _pytest.compat import get_default_arg_names +from _pytest.compat import get_real_func +from _pytest.compat import getimfunc +from _pytest.compat import is_async_function +from _pytest.compat import LEGACY_PATH +from _pytest.compat import NOTSET +from _pytest.compat import safe_getattr +from _pytest.compat import safe_isclass +from _pytest.config import Config +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import FixtureDef +from _pytest.fixtures import FixtureRequest +from _pytest.fixtures import FuncFixtureInfo +from _pytest.fixtures import get_scope_node +from _pytest.main import Session +from _pytest.mark import ParameterSet +from _pytest.mark.structures import _HiddenParam +from _pytest.mark.structures import get_unpacked_marks +from _pytest.mark.structures import HIDDEN_PARAM +from _pytest.mark.structures import Mark +from _pytest.mark.structures import MarkDecorator +from _pytest.mark.structures import normalize_mark_list +from _pytest.outcomes import fail +from _pytest.outcomes import skip +from _pytest.pathlib import fnmatch_ex +from _pytest.pathlib import import_path +from _pytest.pathlib import ImportPathMismatchError +from _pytest.pathlib import scandir +from _pytest.scope import _ScopeName +from _pytest.scope import Scope +from _pytest.stash import StashKey +from _pytest.warning_types import PytestCollectionWarning +from _pytest.warning_types import PytestReturnNotNoneWarning + + +if TYPE_CHECKING: + from typing_extensions import Self + + +def pytest_addoption(parser: Parser) -> None: + parser.addini( + "python_files", + type="args", + # NOTE: default is also used in AssertionRewritingHook. + default=["test_*.py", "*_test.py"], + help="Glob-style file patterns for Python test module discovery", + ) + parser.addini( + "python_classes", + type="args", + default=["Test"], + help="Prefixes or glob names for Python test class discovery", + ) + parser.addini( + "python_functions", + type="args", + default=["test"], + help="Prefixes or glob names for Python test function and method discovery", + ) + parser.addini( + "disable_test_id_escaping_and_forfeit_all_rights_to_community_support", + type="bool", + default=False, + help="Disable string escape non-ASCII characters, might cause unwanted " + "side effects(use at your own risk)", + ) + parser.addini( + "strict_parametrization_ids", + type="bool", + # None => fallback to `strict`. + default=None, + help="Emit an error if non-unique parameter set IDs are detected", + ) + + +def pytest_generate_tests(metafunc: Metafunc) -> None: + for marker in metafunc.definition.iter_markers(name="parametrize"): + metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker) + + +def pytest_configure(config: Config) -> None: + config.addinivalue_line( + "markers", + "parametrize(argnames, argvalues): call a test function multiple " + "times passing in different arguments in turn. argvalues generally " + "needs to be a list of values if argnames specifies only one name " + "or a list of tuples of values if argnames specifies multiple names. " + "Example: @parametrize('arg1', [1,2]) would lead to two calls of the " + "decorated test function, one with arg1=1 and another with arg1=2." + "see https://docs.pytest.org/en/stable/how-to/parametrize.html for more info " + "and examples.", + ) + config.addinivalue_line( + "markers", + "usefixtures(fixturename1, fixturename2, ...): mark tests as needing " + "all of the specified fixtures. see " + "https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures ", + ) + + +def async_fail(nodeid: str) -> None: + msg = ( + "async def functions are not natively supported.\n" + "You need to install a suitable plugin for your async framework, for example:\n" + " - anyio\n" + " - pytest-asyncio\n" + " - pytest-tornasync\n" + " - pytest-trio\n" + " - pytest-twisted" + ) + fail(msg, pytrace=False) + + +@hookimpl(trylast=True) +def pytest_pyfunc_call(pyfuncitem: Function) -> object | None: + testfunction = pyfuncitem.obj + if is_async_function(testfunction): + async_fail(pyfuncitem.nodeid) + funcargs = pyfuncitem.funcargs + testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} + result = testfunction(**testargs) + if hasattr(result, "__await__") or hasattr(result, "__aiter__"): + async_fail(pyfuncitem.nodeid) + elif result is not None: + warnings.warn( + PytestReturnNotNoneWarning( + f"Test functions should return None, but {pyfuncitem.nodeid} returned {type(result)!r}.\n" + "Did you mean to use `assert` instead of `return`?\n" + "See https://docs.pytest.org/en/stable/how-to/assert.html#return-not-none for more information." + ) + ) + return True + + +def pytest_collect_directory( + path: Path, parent: nodes.Collector +) -> nodes.Collector | None: + pkginit = path / "__init__.py" + try: + has_pkginit = pkginit.is_file() + except PermissionError: + # See https://github.com/pytest-dev/pytest/issues/12120#issuecomment-2106349096. + return None + if has_pkginit: + return Package.from_parent(parent, path=path) + return None + + +def pytest_collect_file(file_path: Path, parent: nodes.Collector) -> Module | None: + if file_path.suffix == ".py": + if not parent.session.isinitpath(file_path): + if not path_matches_patterns( + file_path, parent.config.getini("python_files") + ): + return None + ihook = parent.session.gethookproxy(file_path) + module: Module = ihook.pytest_pycollect_makemodule( + module_path=file_path, parent=parent + ) + return module + return None + + +def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool: + """Return whether path matches any of the patterns in the list of globs given.""" + return any(fnmatch_ex(pattern, path) for pattern in patterns) + + +def pytest_pycollect_makemodule(module_path: Path, parent) -> Module: + return Module.from_parent(parent, path=module_path) + + +@hookimpl(trylast=True) +def pytest_pycollect_makeitem( + collector: Module | Class, name: str, obj: object +) -> None | nodes.Item | nodes.Collector | list[nodes.Item | nodes.Collector]: + assert isinstance(collector, Class | Module), type(collector) + # Nothing was collected elsewhere, let's do it here. + if safe_isclass(obj): + if collector.istestclass(obj, name): + return Class.from_parent(collector, name=name, obj=obj) + elif collector.istestfunction(obj, name): + # mock seems to store unbound methods (issue473), normalize it. + obj = getattr(obj, "__func__", obj) + # We need to try and unwrap the function if it's a functools.partial + # or a functools.wrapped. + # We mustn't if it's been wrapped with mock.patch (python 2 only). + if not (inspect.isfunction(obj) or inspect.isfunction(get_real_func(obj))): + filename, lineno = getfslineno(obj) + warnings.warn_explicit( + message=PytestCollectionWarning( + f"cannot collect {name!r} because it is not a function." + ), + category=None, + filename=str(filename), + lineno=lineno + 1, + ) + elif getattr(obj, "__test__", True): + if inspect.isgeneratorfunction(obj): + fail( + f"'yield' keyword is allowed in fixtures, but not in tests ({name})", + pytrace=False, + ) + return list(collector._genfunctions(name, obj)) + return None + return None + + +class PyobjMixin(nodes.Node): + """this mix-in inherits from Node to carry over the typing information + + as its intended to always mix in before a node + its position in the mro is unaffected""" + + _ALLOW_MARKERS = True + + @property + def module(self): + """Python module object this node was collected from (can be None).""" + node = self.getparent(Module) + return node.obj if node is not None else None + + @property + def cls(self): + """Python class object this node was collected from (can be None).""" + node = self.getparent(Class) + return node.obj if node is not None else None + + @property + def instance(self): + """Python instance object the function is bound to. + + Returns None if not a test method, e.g. for a standalone test function, + a class or a module. + """ + # Overridden by Function. + return None + + @property + def obj(self): + """Underlying Python object.""" + obj = getattr(self, "_obj", None) + if obj is None: + self._obj = obj = self._getobj() + # XXX evil hack + # used to avoid Function marker duplication + if self._ALLOW_MARKERS: + self.own_markers.extend(get_unpacked_marks(self.obj)) + # This assumes that `obj` is called before there is a chance + # to add custom keys to `self.keywords`, so no fear of overriding. + self.keywords.update((mark.name, mark) for mark in self.own_markers) + return obj + + @obj.setter + def obj(self, value): + self._obj = value + + def _getobj(self): + """Get the underlying Python object. May be overwritten by subclasses.""" + # TODO: Improve the type of `parent` such that assert/ignore aren't needed. + assert self.parent is not None + obj = self.parent.obj # type: ignore[attr-defined] + return getattr(obj, self.name) + + def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str: + """Return Python path relative to the containing module.""" + parts = [] + for node in self.iter_parents(): + name = node.name + if isinstance(node, Module): + name = os.path.splitext(name)[0] + if stopatmodule: + if includemodule: + parts.append(name) + break + parts.append(name) + parts.reverse() + return ".".join(parts) + + def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]: + # XXX caching? + path, lineno = getfslineno(self.obj) + modpath = self.getmodpath() + return path, lineno, modpath + + +# As an optimization, these builtin attribute names are pre-ignored when +# iterating over an object during collection -- the pytest_pycollect_makeitem +# hook is not called for them. +# fmt: off +class _EmptyClass: pass # noqa: E701 +IGNORED_ATTRIBUTES = frozenset.union( + frozenset(), + # Module. + dir(types.ModuleType("empty_module")), + # Some extra module attributes the above doesn't catch. + {"__builtins__", "__file__", "__cached__"}, + # Class. + dir(_EmptyClass), + # Instance. + dir(_EmptyClass()), +) +del _EmptyClass +# fmt: on + + +class PyCollector(PyobjMixin, nodes.Collector, abc.ABC): + def funcnamefilter(self, name: str) -> bool: + return self._matches_prefix_or_glob_option("python_functions", name) + + def isnosetest(self, obj: object) -> bool: + """Look for the __test__ attribute, which is applied by the + @nose.tools.istest decorator. + """ + # We explicitly check for "is True" here to not mistakenly treat + # classes with a custom __getattr__ returning something truthy (like a + # function) as test classes. + return safe_getattr(obj, "__test__", False) is True + + def classnamefilter(self, name: str) -> bool: + return self._matches_prefix_or_glob_option("python_classes", name) + + def istestfunction(self, obj: object, name: str) -> bool: + if self.funcnamefilter(name) or self.isnosetest(obj): + if isinstance(obj, staticmethod | classmethod): + # staticmethods and classmethods need to be unwrapped. + obj = safe_getattr(obj, "__func__", False) + return callable(obj) and fixtures.getfixturemarker(obj) is None + else: + return False + + def istestclass(self, obj: object, name: str) -> bool: + if not (self.classnamefilter(name) or self.isnosetest(obj)): + return False + if inspect.isabstract(obj): + return False + return True + + def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool: + """Check if the given name matches the prefix or glob-pattern defined + in configuration.""" + for option in self.config.getini(option_name): + if name.startswith(option): + return True + # Check that name looks like a glob-string before calling fnmatch + # because this is called for every name in each collected module, + # and fnmatch is somewhat expensive to call. + elif ("*" in option or "?" in option or "[" in option) and fnmatch.fnmatch( + name, option + ): + return True + return False + + def collect(self) -> Iterable[nodes.Item | nodes.Collector]: + if not getattr(self.obj, "__test__", True): + return [] + + # Avoid random getattrs and peek in the __dict__ instead. + dicts = [getattr(self.obj, "__dict__", {})] + if isinstance(self.obj, type): + for basecls in self.obj.__mro__: + dicts.append(basecls.__dict__) + + # In each class, nodes should be definition ordered. + # __dict__ is definition ordered. + seen: set[str] = set() + dict_values: list[list[nodes.Item | nodes.Collector]] = [] + collect_imported_tests = self.session.config.getini("collect_imported_tests") + ihook = self.ihook + for dic in dicts: + values: list[nodes.Item | nodes.Collector] = [] + # Note: seems like the dict can change during iteration - + # be careful not to remove the list() without consideration. + for name, obj in list(dic.items()): + if name in IGNORED_ATTRIBUTES: + continue + if name in seen: + continue + seen.add(name) + + if not collect_imported_tests and isinstance(self, Module): + # Do not collect functions and classes from other modules. + if inspect.isfunction(obj) or inspect.isclass(obj): + if obj.__module__ != self._getobj().__name__: + continue + + res = ihook.pytest_pycollect_makeitem( + collector=self, name=name, obj=obj + ) + if res is None: + continue + elif isinstance(res, list): + values.extend(res) + else: + values.append(res) + dict_values.append(values) + + # Between classes in the class hierarchy, reverse-MRO order -- nodes + # inherited from base classes should come before subclasses. + result = [] + for values in reversed(dict_values): + result.extend(values) + return result + + def _genfunctions(self, name: str, funcobj) -> Iterator[Function]: + modulecol = self.getparent(Module) + assert modulecol is not None + module = modulecol.obj + clscol = self.getparent(Class) + cls = (clscol and clscol.obj) or None + + definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj) + fixtureinfo = definition._fixtureinfo + + # pytest_generate_tests impls call metafunc.parametrize() which fills + # metafunc._calls, the outcome of the hook. + metafunc = Metafunc( + definition=definition, + fixtureinfo=fixtureinfo, + config=self.config, + cls=cls, + module=module, + _ispytest=True, + ) + methods = [] + if hasattr(module, "pytest_generate_tests"): + methods.append(module.pytest_generate_tests) + if cls is not None and hasattr(cls, "pytest_generate_tests"): + methods.append(cls().pytest_generate_tests) + self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc)) + + if not metafunc._calls: + yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo) + else: + metafunc._recompute_direct_params_indices() + # Direct parametrizations taking place in module/class-specific + # `metafunc.parametrize` calls may have shadowed some fixtures, so make sure + # we update what the function really needs a.k.a its fixture closure. Note that + # direct parametrizations using `@pytest.mark.parametrize` have already been considered + # into making the closure using `ignore_args` arg to `getfixtureclosure`. + fixtureinfo.prune_dependency_tree() + + for callspec in metafunc._calls: + subname = f"{name}[{callspec.id}]" if callspec._idlist else name + yield Function.from_parent( + self, + name=subname, + callspec=callspec, + fixtureinfo=fixtureinfo, + keywords={callspec.id: True}, + originalname=name, + ) + + +def importtestmodule( + path: Path, + config: Config, +): + # We assume we are only called once per module. + importmode = config.getoption("--import-mode") + try: + mod = import_path( + path, + mode=importmode, + root=config.rootpath, + consider_namespace_packages=config.getini("consider_namespace_packages"), + ) + except SyntaxError as e: + raise nodes.Collector.CollectError( + ExceptionInfo.from_current().getrepr(style="short") + ) from e + except ImportPathMismatchError as e: + raise nodes.Collector.CollectError( + "import file mismatch:\n" + "imported module {!r} has this __file__ attribute:\n" + " {}\n" + "which is not the same as the test file we want to collect:\n" + " {}\n" + "HINT: remove __pycache__ / .pyc files and/or use a " + "unique basename for your test file modules".format(*e.args) + ) from e + except ImportError as e: + exc_info = ExceptionInfo.from_current() + if config.get_verbosity() < 2: + exc_info.traceback = exc_info.traceback.filter(filter_traceback) + exc_repr = ( + exc_info.getrepr(style="short") + if exc_info.traceback + else exc_info.exconly() + ) + formatted_tb = str(exc_repr) + raise nodes.Collector.CollectError( + f"ImportError while importing test module '{path}'.\n" + "Hint: make sure your test modules/packages have valid Python names.\n" + "Traceback:\n" + f"{formatted_tb}" + ) from e + except skip.Exception as e: + if e.allow_module_level: + raise + raise nodes.Collector.CollectError( + "Using pytest.skip outside of a test will skip the entire module. " + "If that's your intention, pass `allow_module_level=True`. " + "If you want to skip a specific test or an entire class, " + "use the @pytest.mark.skip or @pytest.mark.skipif decorators." + ) from e + config.pluginmanager.consider_module(mod) + return mod + + +class Module(nodes.File, PyCollector): + """Collector for test classes and functions in a Python module.""" + + def _getobj(self): + return importtestmodule(self.path, self.config) + + def collect(self) -> Iterable[nodes.Item | nodes.Collector]: + self._register_setup_module_fixture() + self._register_setup_function_fixture() + self.session._fixturemanager.parsefactories(self) + return super().collect() + + def _register_setup_module_fixture(self) -> None: + """Register an autouse, module-scoped fixture for the collected module object + that invokes setUpModule/tearDownModule if either or both are available. + + Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with + other fixtures (#517). + """ + setup_module = _get_first_non_fixture_func( + self.obj, ("setUpModule", "setup_module") + ) + teardown_module = _get_first_non_fixture_func( + self.obj, ("tearDownModule", "teardown_module") + ) + + if setup_module is None and teardown_module is None: + return + + def xunit_setup_module_fixture(request) -> Generator[None]: + module = request.module + if setup_module is not None: + _call_with_optional_argument(setup_module, module) + yield + if teardown_module is not None: + _call_with_optional_argument(teardown_module, module) + + self.session._fixturemanager._register_fixture( + # Use a unique name to speed up lookup. + name=f"_xunit_setup_module_fixture_{self.obj.__name__}", + func=xunit_setup_module_fixture, + nodeid=self.nodeid, + scope="module", + autouse=True, + ) + + def _register_setup_function_fixture(self) -> None: + """Register an autouse, function-scoped fixture for the collected module object + that invokes setup_function/teardown_function if either or both are available. + + Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with + other fixtures (#517). + """ + setup_function = _get_first_non_fixture_func(self.obj, ("setup_function",)) + teardown_function = _get_first_non_fixture_func( + self.obj, ("teardown_function",) + ) + if setup_function is None and teardown_function is None: + return + + def xunit_setup_function_fixture(request) -> Generator[None]: + if request.instance is not None: + # in this case we are bound to an instance, so we need to let + # setup_method handle this + yield + return + function = request.function + if setup_function is not None: + _call_with_optional_argument(setup_function, function) + yield + if teardown_function is not None: + _call_with_optional_argument(teardown_function, function) + + self.session._fixturemanager._register_fixture( + # Use a unique name to speed up lookup. + name=f"_xunit_setup_function_fixture_{self.obj.__name__}", + func=xunit_setup_function_fixture, + nodeid=self.nodeid, + scope="function", + autouse=True, + ) + + +class Package(nodes.Directory): + """Collector for files and directories in a Python packages -- directories + with an `__init__.py` file. + + .. note:: + + Directories without an `__init__.py` file are instead collected by + :class:`~pytest.Dir` by default. Both are :class:`~pytest.Directory` + collectors. + + .. versionchanged:: 8.0 + + Now inherits from :class:`~pytest.Directory`. + """ + + def __init__( + self, + fspath: LEGACY_PATH | None, + parent: nodes.Collector, + # NOTE: following args are unused: + config=None, + session=None, + nodeid=None, + path: Path | None = None, + ) -> None: + # NOTE: Could be just the following, but kept as-is for compat. + # super().__init__(self, fspath, parent=parent) + session = parent.session + super().__init__( + fspath=fspath, + path=path, + parent=parent, + config=config, + session=session, + nodeid=nodeid, + ) + + def setup(self) -> None: + init_mod = importtestmodule(self.path / "__init__.py", self.config) + + # Not using fixtures to call setup_module here because autouse fixtures + # from packages are not called automatically (#4085). + setup_module = _get_first_non_fixture_func( + init_mod, ("setUpModule", "setup_module") + ) + if setup_module is not None: + _call_with_optional_argument(setup_module, init_mod) + + teardown_module = _get_first_non_fixture_func( + init_mod, ("tearDownModule", "teardown_module") + ) + if teardown_module is not None: + func = partial(_call_with_optional_argument, teardown_module, init_mod) + self.addfinalizer(func) + + def collect(self) -> Iterable[nodes.Item | nodes.Collector]: + # Always collect __init__.py first. + def sort_key(entry: os.DirEntry[str]) -> object: + return (entry.name != "__init__.py", entry.name) + + config = self.config + col: nodes.Collector | None + cols: Sequence[nodes.Collector] + ihook = self.ihook + for direntry in scandir(self.path, sort_key): + if direntry.is_dir(): + path = Path(direntry.path) + if not self.session.isinitpath(path, with_parents=True): + if ihook.pytest_ignore_collect(collection_path=path, config=config): + continue + col = ihook.pytest_collect_directory(path=path, parent=self) + if col is not None: + yield col + + elif direntry.is_file(): + path = Path(direntry.path) + if not self.session.isinitpath(path): + if ihook.pytest_ignore_collect(collection_path=path, config=config): + continue + cols = ihook.pytest_collect_file(file_path=path, parent=self) + yield from cols + + +def _call_with_optional_argument(func, arg) -> None: + """Call the given function with the given argument if func accepts one argument, otherwise + calls func without arguments.""" + arg_count = func.__code__.co_argcount + if inspect.ismethod(func): + arg_count -= 1 + if arg_count: + func(arg) + else: + func() + + +def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> object | None: + """Return the attribute from the given object to be used as a setup/teardown + xunit-style function, but only if not marked as a fixture to avoid calling it twice. + """ + for name in names: + meth: object | None = getattr(obj, name, None) + if meth is not None and fixtures.getfixturemarker(meth) is None: + return meth + return None + + +class Class(PyCollector): + """Collector for test methods (and nested classes) in a Python class.""" + + @classmethod + def from_parent(cls, parent, *, name, obj=None, **kw) -> Self: # type: ignore[override] + """The public constructor.""" + return super().from_parent(name=name, parent=parent, **kw) + + def newinstance(self): + return self.obj() + + def collect(self) -> Iterable[nodes.Item | nodes.Collector]: + if not safe_getattr(self.obj, "__test__", True): + return [] + if hasinit(self.obj): + assert self.parent is not None + self.warn( + PytestCollectionWarning( + f"cannot collect test class {self.obj.__name__!r} because it has a " + f"__init__ constructor (from: {self.parent.nodeid})" + ) + ) + return [] + elif hasnew(self.obj): + assert self.parent is not None + self.warn( + PytestCollectionWarning( + f"cannot collect test class {self.obj.__name__!r} because it has a " + f"__new__ constructor (from: {self.parent.nodeid})" + ) + ) + return [] + + self._register_setup_class_fixture() + self._register_setup_method_fixture() + + self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid) + + return super().collect() + + def _register_setup_class_fixture(self) -> None: + """Register an autouse, class scoped fixture into the collected class object + that invokes setup_class/teardown_class if either or both are available. + + Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with + other fixtures (#517). + """ + setup_class = _get_first_non_fixture_func(self.obj, ("setup_class",)) + teardown_class = _get_first_non_fixture_func(self.obj, ("teardown_class",)) + if setup_class is None and teardown_class is None: + return + + def xunit_setup_class_fixture(request) -> Generator[None]: + cls = request.cls + if setup_class is not None: + func = getimfunc(setup_class) + _call_with_optional_argument(func, cls) + yield + if teardown_class is not None: + func = getimfunc(teardown_class) + _call_with_optional_argument(func, cls) + + self.session._fixturemanager._register_fixture( + # Use a unique name to speed up lookup. + name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}", + func=xunit_setup_class_fixture, + nodeid=self.nodeid, + scope="class", + autouse=True, + ) + + def _register_setup_method_fixture(self) -> None: + """Register an autouse, function scoped fixture into the collected class object + that invokes setup_method/teardown_method if either or both are available. + + Using a fixture to invoke these methods ensures we play nicely and unsurprisingly with + other fixtures (#517). + """ + setup_name = "setup_method" + setup_method = _get_first_non_fixture_func(self.obj, (setup_name,)) + teardown_name = "teardown_method" + teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,)) + if setup_method is None and teardown_method is None: + return + + def xunit_setup_method_fixture(request) -> Generator[None]: + instance = request.instance + method = request.function + if setup_method is not None: + func = getattr(instance, setup_name) + _call_with_optional_argument(func, method) + yield + if teardown_method is not None: + func = getattr(instance, teardown_name) + _call_with_optional_argument(func, method) + + self.session._fixturemanager._register_fixture( + # Use a unique name to speed up lookup. + name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}", + func=xunit_setup_method_fixture, + nodeid=self.nodeid, + scope="function", + autouse=True, + ) + + +def hasinit(obj: object) -> bool: + init: object = getattr(obj, "__init__", None) + if init: + return init != object.__init__ + return False + + +def hasnew(obj: object) -> bool: + new: object = getattr(obj, "__new__", None) + if new: + return new != object.__new__ + return False + + +@final +@dataclasses.dataclass(frozen=True) +class IdMaker: + """Make IDs for a parametrization.""" + + __slots__ = ( + "argnames", + "config", + "func_name", + "idfn", + "ids", + "nodeid", + "parametersets", + ) + + # The argnames of the parametrization. + argnames: Sequence[str] + # The ParameterSets of the parametrization. + parametersets: Sequence[ParameterSet] + # Optionally, a user-provided callable to make IDs for parameters in a + # ParameterSet. + idfn: Callable[[Any], object | None] | None + # Optionally, explicit IDs for ParameterSets by index. + ids: Sequence[object | None] | None + # Optionally, the pytest config. + # Used for controlling ASCII escaping, determining parametrization ID + # strictness, and for calling the :hook:`pytest_make_parametrize_id` hook. + config: Config | None + # Optionally, the ID of the node being parametrized. + # Used only for clearer error messages. + nodeid: str | None + # Optionally, the ID of the function being parametrized. + # Used only for clearer error messages. + func_name: str | None + + def make_unique_parameterset_ids(self) -> list[str | _HiddenParam]: + """Make a unique identifier for each ParameterSet, that may be used to + identify the parametrization in a node ID. + + If strict_parametrization_ids is enabled, and duplicates are detected, + raises CollectError. Otherwise makes the IDs unique as follows: + + Format is -...-[counter], where prm_x_token is + - user-provided id, if given + - else an id derived from the value, applicable for certain types + - else + The counter suffix is appended only in case a string wouldn't be unique + otherwise. + """ + resolved_ids = list(self._resolve_ids()) + # All IDs must be unique! + if len(resolved_ids) != len(set(resolved_ids)): + # Record the number of occurrences of each ID. + id_counts = Counter(resolved_ids) + + if self._strict_parametrization_ids_enabled(): + parameters = ", ".join(self.argnames) + parametersets = ", ".join( + [saferepr(list(param.values)) for param in self.parametersets] + ) + ids = ", ".join( + id if id is not HIDDEN_PARAM else "" for id in resolved_ids + ) + duplicates = ", ".join( + id if id is not HIDDEN_PARAM else "" + for id, count in id_counts.items() + if count > 1 + ) + msg = textwrap.dedent(f""" + Duplicate parametrization IDs detected, but strict_parametrization_ids is set. + + Test name: {self.nodeid} + Parameters: {parameters} + Parameter sets: {parametersets} + IDs: {ids} + Duplicates: {duplicates} + + You can fix this problem using `@pytest.mark.parametrize(..., ids=...)` or `pytest.param(..., id=...)`. + """).strip() # noqa: E501 + raise nodes.Collector.CollectError(msg) + + # Map the ID to its next suffix. + id_suffixes: dict[str, int] = defaultdict(int) + # Suffix non-unique IDs to make them unique. + for index, id in enumerate(resolved_ids): + if id_counts[id] > 1: + if id is HIDDEN_PARAM: + self._complain_multiple_hidden_parameter_sets() + suffix = "" + if id and id[-1].isdigit(): + suffix = "_" + new_id = f"{id}{suffix}{id_suffixes[id]}" + while new_id in set(resolved_ids): + id_suffixes[id] += 1 + new_id = f"{id}{suffix}{id_suffixes[id]}" + resolved_ids[index] = new_id + id_suffixes[id] += 1 + assert len(resolved_ids) == len(set(resolved_ids)), ( + f"Internal error: {resolved_ids=}" + ) + return resolved_ids + + def _strict_parametrization_ids_enabled(self) -> bool: + if self.config is None: + return False + strict_parametrization_ids = self.config.getini("strict_parametrization_ids") + if strict_parametrization_ids is None: + strict_parametrization_ids = self.config.getini("strict") + return cast(bool, strict_parametrization_ids) + + def _resolve_ids(self) -> Iterable[str | _HiddenParam]: + """Resolve IDs for all ParameterSets (may contain duplicates).""" + for idx, parameterset in enumerate(self.parametersets): + if parameterset.id is not None: + # ID provided directly - pytest.param(..., id="...") + if parameterset.id is HIDDEN_PARAM: + yield HIDDEN_PARAM + else: + yield _ascii_escaped_by_config(parameterset.id, self.config) + elif self.ids and idx < len(self.ids) and self.ids[idx] is not None: + # ID provided in the IDs list - parametrize(..., ids=[...]). + if self.ids[idx] is HIDDEN_PARAM: + yield HIDDEN_PARAM + else: + yield self._idval_from_value_required(self.ids[idx], idx) + else: + # ID not provided - generate it. + yield "-".join( + self._idval(val, argname, idx) + for val, argname in zip( + parameterset.values, self.argnames, strict=True + ) + ) + + def _idval(self, val: object, argname: str, idx: int) -> str: + """Make an ID for a parameter in a ParameterSet.""" + idval = self._idval_from_function(val, argname, idx) + if idval is not None: + return idval + idval = self._idval_from_hook(val, argname) + if idval is not None: + return idval + idval = self._idval_from_value(val) + if idval is not None: + return idval + return self._idval_from_argname(argname, idx) + + def _idval_from_function(self, val: object, argname: str, idx: int) -> str | None: + """Try to make an ID for a parameter in a ParameterSet using the + user-provided id callable, if given.""" + if self.idfn is None: + return None + try: + id = self.idfn(val) + except Exception as e: + prefix = f"{self.nodeid}: " if self.nodeid is not None else "" + msg = "error raised while trying to determine id of parameter '{}' at position {}" + msg = prefix + msg.format(argname, idx) + raise ValueError(msg) from e + if id is None: + return None + return self._idval_from_value(id) + + def _idval_from_hook(self, val: object, argname: str) -> str | None: + """Try to make an ID for a parameter in a ParameterSet by calling the + :hook:`pytest_make_parametrize_id` hook.""" + if self.config: + id: str | None = self.config.hook.pytest_make_parametrize_id( + config=self.config, val=val, argname=argname + ) + return id + return None + + def _idval_from_value(self, val: object) -> str | None: + """Try to make an ID for a parameter in a ParameterSet from its value, + if the value type is supported.""" + if isinstance(val, str | bytes): + return _ascii_escaped_by_config(val, self.config) + elif val is None or isinstance(val, float | int | bool | complex): + return str(val) + elif isinstance(val, re.Pattern): + return ascii_escaped(val.pattern) + elif val is NOTSET: + # Fallback to default. Note that NOTSET is an enum.Enum. + pass + elif isinstance(val, enum.Enum): + return str(val) + elif isinstance(getattr(val, "__name__", None), str): + # Name of a class, function, module, etc. + name: str = getattr(val, "__name__") + return name + return None + + def _idval_from_value_required(self, val: object, idx: int) -> str: + """Like _idval_from_value(), but fails if the type is not supported.""" + id = self._idval_from_value(val) + if id is not None: + return id + + # Fail. + prefix = self._make_error_prefix() + msg = ( + f"{prefix}ids contains unsupported value {saferepr(val)} (type: {type(val)!r}) at index {idx}. " + "Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__." + ) + fail(msg, pytrace=False) + + @staticmethod + def _idval_from_argname(argname: str, idx: int) -> str: + """Make an ID for a parameter in a ParameterSet from the argument name + and the index of the ParameterSet.""" + return str(argname) + str(idx) + + def _complain_multiple_hidden_parameter_sets(self) -> NoReturn: + fail( + f"{self._make_error_prefix()}multiple instances of HIDDEN_PARAM " + "cannot be used in the same parametrize call, " + "because the tests names need to be unique." + ) + + def _make_error_prefix(self) -> str: + if self.func_name is not None: + return f"In {self.func_name}: " + elif self.nodeid is not None: + return f"In {self.nodeid}: " + else: + return "" + + +@final +@dataclasses.dataclass(frozen=True) +class CallSpec2: + """A planned parameterized invocation of a test function. + + Calculated during collection for a given test function's Metafunc. + Once collection is over, each callspec is turned into a single Item + and stored in item.callspec. + """ + + # arg name -> arg value which will be passed to a fixture or pseudo-fixture + # of the same name. (indirect or direct parametrization respectively) + params: dict[str, object] = dataclasses.field(default_factory=dict) + # arg name -> arg index. + indices: dict[str, int] = dataclasses.field(default_factory=dict) + # arg name -> parameter scope. + # Used for sorting parametrized resources. + _arg2scope: Mapping[str, Scope] = dataclasses.field(default_factory=dict) + # Parts which will be added to the item's name in `[..]` separated by "-". + _idlist: Sequence[str] = dataclasses.field(default_factory=tuple) + # Marks which will be applied to the item. + marks: list[Mark] = dataclasses.field(default_factory=list) + + def setmulti( + self, + *, + argnames: Iterable[str], + valset: Iterable[object], + id: str | _HiddenParam, + marks: Iterable[Mark | MarkDecorator], + scope: Scope, + param_index: int, + nodeid: str, + ) -> CallSpec2: + params = self.params.copy() + indices = self.indices.copy() + arg2scope = dict(self._arg2scope) + for arg, val in zip(argnames, valset, strict=True): + if arg in params: + raise nodes.Collector.CollectError( + f"{nodeid}: duplicate parametrization of {arg!r}" + ) + params[arg] = val + indices[arg] = param_index + arg2scope[arg] = scope + return CallSpec2( + params=params, + indices=indices, + _arg2scope=arg2scope, + _idlist=self._idlist if id is HIDDEN_PARAM else [*self._idlist, id], + marks=[*self.marks, *normalize_mark_list(marks)], + ) + + def getparam(self, name: str) -> object: + try: + return self.params[name] + except KeyError as e: + raise ValueError(name) from e + + @property + def id(self) -> str: + return "-".join(self._idlist) + + +def get_direct_param_fixture_func(request: FixtureRequest) -> Any: + return request.param + + +# Used for storing pseudo fixturedefs for direct parametrization. +name2pseudofixturedef_key = StashKey[dict[str, FixtureDef[Any]]]() + + +@final +class Metafunc: + """Objects passed to the :hook:`pytest_generate_tests` hook. + + They help to inspect a test function and to generate tests according to + test configuration or values specified in the class or module where a + test function is defined. + """ + + def __init__( + self, + definition: FunctionDefinition, + fixtureinfo: fixtures.FuncFixtureInfo, + config: Config, + cls=None, + module=None, + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + + #: Access to the underlying :class:`_pytest.python.FunctionDefinition`. + self.definition = definition + + #: Access to the :class:`pytest.Config` object for the test session. + self.config = config + + #: The module object where the test function is defined in. + self.module = module + + #: Underlying Python test function. + self.function = definition.obj + + #: Set of fixture names required by the test function. + self.fixturenames = fixtureinfo.names_closure + + #: Class object where the test function is defined in or ``None``. + self.cls = cls + + self._arg2fixturedefs = fixtureinfo.name2fixturedefs + + # Result of parametrize(). + self._calls: list[CallSpec2] = [] + + self._params_directness: dict[str, Literal["indirect", "direct"]] = {} + + def parametrize( + self, + argnames: str | Sequence[str], + argvalues: Iterable[ParameterSet | Sequence[object] | object], + indirect: bool | Sequence[str] = False, + ids: Iterable[object | None] | Callable[[Any], object | None] | None = None, + scope: _ScopeName | None = None, + *, + _param_mark: Mark | None = None, + ) -> None: + """Add new invocations to the underlying test function using the list + of argvalues for the given argnames. Parametrization is performed + during the collection phase. If you need to setup expensive resources + see about setting ``indirect`` to do it at test setup time instead. + + Can be called multiple times per test function (but only on different + argument names), in which case each call parametrizes all previous + parametrizations, e.g. + + :: + + unparametrized: t + parametrize ["x", "y"]: t[x], t[y] + parametrize [1, 2]: t[x-1], t[x-2], t[y-1], t[y-2] + + :param argnames: + A comma-separated string denoting one or more argument names, or + a list/tuple of argument strings. + + :param argvalues: + The list of argvalues determines how often a test is invoked with + different argument values. + + If only one argname was specified argvalues is a list of values. + If N argnames were specified, argvalues must be a list of + N-tuples, where each tuple-element specifies a value for its + respective argname. + + :param indirect: + A list of arguments' names (subset of argnames) or a boolean. + If True the list contains all names from the argnames. Each + argvalue corresponding to an argname in this list will + be passed as request.param to its respective argname fixture + function so that it can perform more expensive setups during the + setup phase of a test rather than at collection time. + + :param ids: + Sequence of (or generator for) ids for ``argvalues``, + or a callable to return part of the id for each argvalue. + + With sequences (and generators like ``itertools.count()``) the + returned ids should be of type ``string``, ``int``, ``float``, + ``bool``, or ``None``. + They are mapped to the corresponding index in ``argvalues``. + ``None`` means to use the auto-generated id. + + .. versionadded:: 8.4 + :ref:`hidden-param` means to hide the parameter set + from the test name. Can only be used at most 1 time, as + test names need to be unique. + + If it is a callable it will be called for each entry in + ``argvalues``, and the return value is used as part of the + auto-generated id for the whole set (where parts are joined with + dashes ("-")). + This is useful to provide more specific ids for certain items, e.g. + dates. Returning ``None`` will use an auto-generated id. + + If no ids are provided they will be generated automatically from + the argvalues. + + :param scope: + If specified it denotes the scope of the parameters. + The scope is used for grouping tests by parameter instances. + It will also override any fixture-function defined scope, allowing + to set a dynamic scope using test context or configuration. + """ + nodeid = self.definition.nodeid + + argnames, parametersets = ParameterSet._for_parametrize( + argnames, + argvalues, + self.function, + self.config, + nodeid=self.definition.nodeid, + ) + del argvalues + + if "request" in argnames: + fail( + f"{nodeid}: 'request' is a reserved name and cannot be used in @pytest.mark.parametrize", + pytrace=False, + ) + + if scope is not None: + scope_ = Scope.from_user( + scope, descr=f"parametrize() call in {self.function.__name__}" + ) + else: + scope_ = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect) + + self._validate_if_using_arg_names(argnames, indirect) + + # Use any already (possibly) generated ids with parametrize Marks. + if _param_mark and _param_mark._param_ids_from: + generated_ids = _param_mark._param_ids_from._param_ids_generated + if generated_ids is not None: + ids = generated_ids + + ids = self._resolve_parameter_set_ids( + argnames, ids, parametersets, nodeid=self.definition.nodeid + ) + + # Store used (possibly generated) ids with parametrize Marks. + if _param_mark and _param_mark._param_ids_from and generated_ids is None: + object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids) + + # Calculate directness. + arg_directness = self._resolve_args_directness(argnames, indirect) + self._params_directness.update(arg_directness) + + # Add direct parametrizations as fixturedefs to arg2fixturedefs by + # registering artificial "pseudo" FixtureDef's such that later at test + # setup time we can rely on FixtureDefs to exist for all argnames. + node = None + # For scopes higher than function, a "pseudo" FixtureDef might have + # already been created for the scope. We thus store and cache the + # FixtureDef on the node related to the scope. + if scope_ is Scope.Function: + name2pseudofixturedef = None + else: + collector = self.definition.parent + assert collector is not None + node = get_scope_node(collector, scope_) + if node is None: + # If used class scope and there is no class, use module-level + # collector (for now). + if scope_ is Scope.Class: + assert isinstance(collector, Module) + node = collector + # If used package scope and there is no package, use session + # (for now). + elif scope_ is Scope.Package: + node = collector.session + else: + assert False, f"Unhandled missing scope: {scope}" + default: dict[str, FixtureDef[Any]] = {} + name2pseudofixturedef = node.stash.setdefault( + name2pseudofixturedef_key, default + ) + for argname in argnames: + if arg_directness[argname] == "indirect": + continue + if name2pseudofixturedef is not None and argname in name2pseudofixturedef: + fixturedef = name2pseudofixturedef[argname] + else: + fixturedef = FixtureDef( + config=self.config, + baseid="", + argname=argname, + func=get_direct_param_fixture_func, + scope=scope_, + params=None, + ids=None, + _ispytest=True, + ) + if name2pseudofixturedef is not None: + name2pseudofixturedef[argname] = fixturedef + self._arg2fixturedefs[argname] = [fixturedef] + + # Create the new calls: if we are parametrize() multiple times (by applying the decorator + # more than once) then we accumulate those calls generating the cartesian product + # of all calls. + newcalls = [] + for callspec in self._calls or [CallSpec2()]: + for param_index, (param_id, param_set) in enumerate( + zip(ids, parametersets, strict=True) + ): + newcallspec = callspec.setmulti( + argnames=argnames, + valset=param_set.values, + id=param_id, + marks=param_set.marks, + scope=scope_, + param_index=param_index, + nodeid=nodeid, + ) + newcalls.append(newcallspec) + self._calls = newcalls + + def _resolve_parameter_set_ids( + self, + argnames: Sequence[str], + ids: Iterable[object | None] | Callable[[Any], object | None] | None, + parametersets: Sequence[ParameterSet], + nodeid: str, + ) -> list[str | _HiddenParam]: + """Resolve the actual ids for the given parameter sets. + + :param argnames: + Argument names passed to ``parametrize()``. + :param ids: + The `ids` parameter of the ``parametrize()`` call (see docs). + :param parametersets: + The parameter sets, each containing a set of values corresponding + to ``argnames``. + :param nodeid str: + The nodeid of the definition item that generated this + parametrization. + :returns: + List with ids for each parameter set given. + """ + if ids is None: + idfn = None + ids_ = None + elif callable(ids): + idfn = ids + ids_ = None + else: + idfn = None + ids_ = self._validate_ids(ids, parametersets, self.function.__name__) + id_maker = IdMaker( + argnames, + parametersets, + idfn, + ids_, + self.config, + nodeid=nodeid, + func_name=self.function.__name__, + ) + return id_maker.make_unique_parameterset_ids() + + def _validate_ids( + self, + ids: Iterable[object | None], + parametersets: Sequence[ParameterSet], + func_name: str, + ) -> list[object | None]: + try: + num_ids = len(ids) # type: ignore[arg-type] + except TypeError: + try: + iter(ids) + except TypeError as e: + raise TypeError("ids must be a callable or an iterable") from e + num_ids = len(parametersets) + + # num_ids == 0 is a special case: https://github.com/pytest-dev/pytest/issues/1849 + if num_ids != len(parametersets) and num_ids != 0: + msg = "In {}: {} parameter sets specified, with different number of ids: {}" + fail(msg.format(func_name, len(parametersets), num_ids), pytrace=False) + + return list(itertools.islice(ids, num_ids)) + + def _resolve_args_directness( + self, + argnames: Sequence[str], + indirect: bool | Sequence[str], + ) -> dict[str, Literal["indirect", "direct"]]: + """Resolve if each parametrized argument must be considered an indirect + parameter to a fixture of the same name, or a direct parameter to the + parametrized function, based on the ``indirect`` parameter of the + parametrized() call. + + :param argnames: + List of argument names passed to ``parametrize()``. + :param indirect: + Same as the ``indirect`` parameter of ``parametrize()``. + :returns + A dict mapping each arg name to either "indirect" or "direct". + """ + arg_directness: dict[str, Literal["indirect", "direct"]] + if isinstance(indirect, bool): + arg_directness = dict.fromkeys( + argnames, "indirect" if indirect else "direct" + ) + elif isinstance(indirect, Sequence): + arg_directness = dict.fromkeys(argnames, "direct") + for arg in indirect: + if arg not in argnames: + fail( + f"In {self.function.__name__}: indirect fixture '{arg}' doesn't exist", + pytrace=False, + ) + arg_directness[arg] = "indirect" + else: + fail( + f"In {self.function.__name__}: expected Sequence or boolean" + f" for indirect, got {type(indirect).__name__}", + pytrace=False, + ) + return arg_directness + + def _validate_if_using_arg_names( + self, + argnames: Sequence[str], + indirect: bool | Sequence[str], + ) -> None: + """Check if all argnames are being used, by default values, or directly/indirectly. + + :param List[str] argnames: List of argument names passed to ``parametrize()``. + :param indirect: Same as the ``indirect`` parameter of ``parametrize()``. + :raises ValueError: If validation fails. + """ + default_arg_names = set(get_default_arg_names(self.function)) + func_name = self.function.__name__ + for arg in argnames: + if arg not in self.fixturenames: + if arg in default_arg_names: + fail( + f"In {func_name}: function already takes an argument '{arg}' with a default value", + pytrace=False, + ) + else: + if isinstance(indirect, Sequence): + name = "fixture" if arg in indirect else "argument" + else: + name = "fixture" if indirect else "argument" + fail( + f"In {func_name}: function uses no {name} '{arg}'", + pytrace=False, + ) + + def _recompute_direct_params_indices(self) -> None: + for argname, param_type in self._params_directness.items(): + if param_type == "direct": + for i, callspec in enumerate(self._calls): + callspec.indices[argname] = i + + +def _find_parametrized_scope( + argnames: Sequence[str], + arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[object]]], + indirect: bool | Sequence[str], +) -> Scope: + """Find the most appropriate scope for a parametrized call based on its arguments. + + When there's at least one direct argument, always use "function" scope. + + When a test function is parametrized and all its arguments are indirect + (e.g. fixtures), return the most narrow scope based on the fixtures used. + + Related to issue #1832, based on code posted by @Kingdread. + """ + if isinstance(indirect, Sequence): + all_arguments_are_fixtures = len(indirect) == len(argnames) + else: + all_arguments_are_fixtures = bool(indirect) + + if all_arguments_are_fixtures: + fixturedefs = arg2fixturedefs or {} + used_scopes = [ + fixturedef[-1]._scope + for name, fixturedef in fixturedefs.items() + if name in argnames + ] + # Takes the most narrow scope from used fixtures. + return min(used_scopes, default=Scope.Function) + + return Scope.Function + + +def _ascii_escaped_by_config(val: str | bytes, config: Config | None) -> str: + if config is None: + escape_option = False + else: + escape_option = config.getini( + "disable_test_id_escaping_and_forfeit_all_rights_to_community_support" + ) + # TODO: If escaping is turned off and the user passes bytes, + # will return a bytes. For now we ignore this but the + # code *probably* doesn't handle this case. + return val if escape_option else ascii_escaped(val) # type: ignore + + +class Function(PyobjMixin, nodes.Item): + """Item responsible for setting up and executing a Python test function. + + :param name: + The full function name, including any decorations like those + added by parametrization (``my_func[my_param]``). + :param parent: + The parent Node. + :param config: + The pytest Config object. + :param callspec: + If given, this function has been parametrized and the callspec contains + meta information about the parametrization. + :param callobj: + If given, the object which will be called when the Function is invoked, + otherwise the callobj will be obtained from ``parent`` using ``originalname``. + :param keywords: + Keywords bound to the function object for "-k" matching. + :param session: + The pytest Session object. + :param fixtureinfo: + Fixture information already resolved at this fixture node.. + :param originalname: + The attribute name to use for accessing the underlying function object. + Defaults to ``name``. Set this if name is different from the original name, + for example when it contains decorations like those added by parametrization + (``my_func[my_param]``). + """ + + # Disable since functions handle it themselves. + _ALLOW_MARKERS = False + + def __init__( + self, + name: str, + parent, + config: Config | None = None, + callspec: CallSpec2 | None = None, + callobj=NOTSET, + keywords: Mapping[str, Any] | None = None, + session: Session | None = None, + fixtureinfo: FuncFixtureInfo | None = None, + originalname: str | None = None, + ) -> None: + super().__init__(name, parent, config=config, session=session) + + if callobj is not NOTSET: + self._obj = callobj + self._instance = getattr(callobj, "__self__", None) + + #: Original function name, without any decorations (for example + #: parametrization adds a ``"[...]"`` suffix to function names), used to access + #: the underlying function object from ``parent`` (in case ``callobj`` is not given + #: explicitly). + #: + #: .. versionadded:: 3.0 + self.originalname = originalname or name + + # Note: when FunctionDefinition is introduced, we should change ``originalname`` + # to a readonly property that returns FunctionDefinition.name. + + self.own_markers.extend(get_unpacked_marks(self.obj)) + if callspec: + self.callspec = callspec + self.own_markers.extend(callspec.marks) + + # todo: this is a hell of a hack + # https://github.com/pytest-dev/pytest/issues/4569 + # Note: the order of the updates is important here; indicates what + # takes priority (ctor argument over function attributes over markers). + # Take own_markers only; NodeKeywords handles parent traversal on its own. + self.keywords.update((mark.name, mark) for mark in self.own_markers) + self.keywords.update(self.obj.__dict__) + if keywords: + self.keywords.update(keywords) + + if fixtureinfo is None: + fm = self.session._fixturemanager + fixtureinfo = fm.getfixtureinfo(self, self.obj, self.cls) + self._fixtureinfo: FuncFixtureInfo = fixtureinfo + self.fixturenames = fixtureinfo.names_closure + self._initrequest() + + # todo: determine sound type limitations + @classmethod + def from_parent(cls, parent, **kw) -> Self: + """The public constructor.""" + return super().from_parent(parent=parent, **kw) + + def _initrequest(self) -> None: + self.funcargs: dict[str, object] = {} + self._request = fixtures.TopRequest(self, _ispytest=True) + + @property + def function(self): + """Underlying python 'function' object.""" + return getimfunc(self.obj) + + @property + def instance(self): + try: + return self._instance + except AttributeError: + if isinstance(self.parent, Class): + # Each Function gets a fresh class instance. + self._instance = self._getinstance() + else: + self._instance = None + return self._instance + + def _getinstance(self): + if isinstance(self.parent, Class): + # Each Function gets a fresh class instance. + return self.parent.newinstance() + else: + return None + + def _getobj(self): + instance = self.instance + if instance is not None: + parent_obj = instance + else: + assert self.parent is not None + parent_obj = self.parent.obj # type: ignore[attr-defined] + return getattr(parent_obj, self.originalname) + + @property + def _pyfuncitem(self): + """(compatonly) for code expecting pytest-2.2 style request objects.""" + return self + + def runtest(self) -> None: + """Execute the underlying test function.""" + self.ihook.pytest_pyfunc_call(pyfuncitem=self) + + def setup(self) -> None: + self._request._fillfixtures() + + def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: + if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False): + code = _pytest._code.Code.from_function(get_real_func(self.obj)) + path, firstlineno = code.path, code.firstlineno + traceback = excinfo.traceback + ntraceback = traceback.cut(path=path, firstlineno=firstlineno) + if ntraceback == traceback: + ntraceback = ntraceback.cut(path=path) + if ntraceback == traceback: + ntraceback = ntraceback.filter(filter_traceback) + if not ntraceback: + ntraceback = traceback + ntraceback = ntraceback.filter(excinfo) + + # issue364: mark all but first and last frames to + # only show a single-line message for each frame. + if self.config.getoption("tbstyle", "auto") == "auto": + if len(ntraceback) > 2: + ntraceback = Traceback( + ( + ntraceback[0], + *(t.with_repr_style("short") for t in ntraceback[1:-1]), + ntraceback[-1], + ) + ) + + return ntraceback + return excinfo.traceback + + # TODO: Type ignored -- breaks Liskov Substitution. + def repr_failure( # type: ignore[override] + self, + excinfo: ExceptionInfo[BaseException], + ) -> str | TerminalRepr: + style = self.config.getoption("tbstyle", "auto") + if style == "auto": + style = "long" + return self._repr_failure_py(excinfo, style=style) + + +class FunctionDefinition(Function): + """This class is a stop gap solution until we evolve to have actual function + definition nodes and manage to get rid of ``metafunc``.""" + + def runtest(self) -> None: + raise RuntimeError("function definitions are not supposed to be run as tests") + + setup = runtest diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/python_api.py b/Backend/venv/lib/python3.12/site-packages/_pytest/python_api.py new file mode 100644 index 00000000..1e389eb0 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/python_api.py @@ -0,0 +1,820 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +from collections.abc import Collection +from collections.abc import Mapping +from collections.abc import Sequence +from collections.abc import Sized +from decimal import Decimal +import math +from numbers import Complex +import pprint +import sys +from typing import Any +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from numpy import ndarray + + +def _compare_approx( + full_object: object, + message_data: Sequence[tuple[str, str, str]], + number_of_elements: int, + different_ids: Sequence[object], + max_abs_diff: float, + max_rel_diff: float, +) -> list[str]: + message_list = list(message_data) + message_list.insert(0, ("Index", "Obtained", "Expected")) + max_sizes = [0, 0, 0] + for index, obtained, expected in message_list: + max_sizes[0] = max(max_sizes[0], len(index)) + max_sizes[1] = max(max_sizes[1], len(obtained)) + max_sizes[2] = max(max_sizes[2], len(expected)) + explanation = [ + f"comparison failed. Mismatched elements: {len(different_ids)} / {number_of_elements}:", + f"Max absolute difference: {max_abs_diff}", + f"Max relative difference: {max_rel_diff}", + ] + [ + f"{indexes:<{max_sizes[0]}} | {obtained:<{max_sizes[1]}} | {expected:<{max_sizes[2]}}" + for indexes, obtained, expected in message_list + ] + return explanation + + +# builtin pytest.approx helper + + +class ApproxBase: + """Provide shared utilities for making approximate comparisons between + numbers or sequences of numbers.""" + + # Tell numpy to use our `__eq__` operator instead of its. + __array_ufunc__ = None + __array_priority__ = 100 + + def __init__(self, expected, rel=None, abs=None, nan_ok: bool = False) -> None: + __tracebackhide__ = True + self.expected = expected + self.abs = abs + self.rel = rel + self.nan_ok = nan_ok + self._check_type() + + def __repr__(self) -> str: + raise NotImplementedError + + def _repr_compare(self, other_side: Any) -> list[str]: + return [ + "comparison failed", + f"Obtained: {other_side}", + f"Expected: {self}", + ] + + def __eq__(self, actual) -> bool: + return all( + a == self._approx_scalar(x) for a, x in self._yield_comparisons(actual) + ) + + def __bool__(self): + __tracebackhide__ = True + raise AssertionError( + "approx() is not supported in a boolean context.\nDid you mean: `assert a == approx(b)`?" + ) + + # Ignore type because of https://github.com/python/mypy/issues/4266. + __hash__ = None # type: ignore + + def __ne__(self, actual) -> bool: + return not (actual == self) + + def _approx_scalar(self, x) -> ApproxScalar: + if isinstance(x, Decimal): + return ApproxDecimal(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok) + return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok) + + def _yield_comparisons(self, actual): + """Yield all the pairs of numbers to be compared. + + This is used to implement the `__eq__` method. + """ + raise NotImplementedError + + def _check_type(self) -> None: + """Raise a TypeError if the expected value is not a valid type.""" + # This is only a concern if the expected value is a sequence. In every + # other case, the approx() function ensures that the expected value has + # a numeric type. For this reason, the default is to do nothing. The + # classes that deal with sequences should reimplement this method to + # raise if there are any non-numeric elements in the sequence. + + +def _recursive_sequence_map(f, x): + """Recursively map a function over a sequence of arbitrary depth""" + if isinstance(x, list | tuple): + seq_type = type(x) + return seq_type(_recursive_sequence_map(f, xi) for xi in x) + elif _is_sequence_like(x): + return [_recursive_sequence_map(f, xi) for xi in x] + else: + return f(x) + + +class ApproxNumpy(ApproxBase): + """Perform approximate comparisons where the expected value is numpy array.""" + + def __repr__(self) -> str: + list_scalars = _recursive_sequence_map( + self._approx_scalar, self.expected.tolist() + ) + return f"approx({list_scalars!r})" + + def _repr_compare(self, other_side: ndarray | list[Any]) -> list[str]: + import itertools + import math + + def get_value_from_nested_list( + nested_list: list[Any], nd_index: tuple[Any, ...] + ) -> Any: + """ + Helper function to get the value out of a nested list, given an n-dimensional index. + This mimics numpy's indexing, but for raw nested python lists. + """ + value: Any = nested_list + for i in nd_index: + value = value[i] + return value + + np_array_shape = self.expected.shape + approx_side_as_seq = _recursive_sequence_map( + self._approx_scalar, self.expected.tolist() + ) + + # convert other_side to numpy array to ensure shape attribute is available + other_side_as_array = _as_numpy_array(other_side) + assert other_side_as_array is not None + + if np_array_shape != other_side_as_array.shape: + return [ + "Impossible to compare arrays with different shapes.", + f"Shapes: {np_array_shape} and {other_side_as_array.shape}", + ] + + number_of_elements = self.expected.size + max_abs_diff = -math.inf + max_rel_diff = -math.inf + different_ids = [] + for index in itertools.product(*(range(i) for i in np_array_shape)): + approx_value = get_value_from_nested_list(approx_side_as_seq, index) + other_value = get_value_from_nested_list(other_side_as_array, index) + if approx_value != other_value: + abs_diff = abs(approx_value.expected - other_value) + max_abs_diff = max(max_abs_diff, abs_diff) + if other_value == 0.0: + max_rel_diff = math.inf + else: + max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value)) + different_ids.append(index) + + message_data = [ + ( + str(index), + str(get_value_from_nested_list(other_side_as_array, index)), + str(get_value_from_nested_list(approx_side_as_seq, index)), + ) + for index in different_ids + ] + return _compare_approx( + self.expected, + message_data, + number_of_elements, + different_ids, + max_abs_diff, + max_rel_diff, + ) + + def __eq__(self, actual) -> bool: + import numpy as np + + # self.expected is supposed to always be an array here. + + if not np.isscalar(actual): + try: + actual = np.asarray(actual) + except Exception as e: + raise TypeError(f"cannot compare '{actual}' to numpy.ndarray") from e + + if not np.isscalar(actual) and actual.shape != self.expected.shape: + return False + + return super().__eq__(actual) + + def _yield_comparisons(self, actual): + import numpy as np + + # `actual` can either be a numpy array or a scalar, it is treated in + # `__eq__` before being passed to `ApproxBase.__eq__`, which is the + # only method that calls this one. + + if np.isscalar(actual): + for i in np.ndindex(self.expected.shape): + yield actual, self.expected[i].item() + else: + for i in np.ndindex(self.expected.shape): + yield actual[i].item(), self.expected[i].item() + + +class ApproxMapping(ApproxBase): + """Perform approximate comparisons where the expected value is a mapping + with numeric values (the keys can be anything).""" + + def __repr__(self) -> str: + return f"approx({ ({k: self._approx_scalar(v) for k, v in self.expected.items()})!r})" + + def _repr_compare(self, other_side: Mapping[object, float]) -> list[str]: + import math + + if len(self.expected) != len(other_side): + return [ + "Impossible to compare mappings with different sizes.", + f"Lengths: {len(self.expected)} and {len(other_side)}", + ] + + if set(self.expected.keys()) != set(other_side.keys()): + return [ + "comparison failed.", + f"Mappings has different keys: expected {self.expected.keys()} but got {other_side.keys()}", + ] + + approx_side_as_map = { + k: self._approx_scalar(v) for k, v in self.expected.items() + } + + number_of_elements = len(approx_side_as_map) + max_abs_diff = -math.inf + max_rel_diff = -math.inf + different_ids = [] + for (approx_key, approx_value), other_value in zip( + approx_side_as_map.items(), other_side.values(), strict=True + ): + if approx_value != other_value: + if approx_value.expected is not None and other_value is not None: + try: + max_abs_diff = max( + max_abs_diff, abs(approx_value.expected - other_value) + ) + if approx_value.expected == 0.0: + max_rel_diff = math.inf + else: + max_rel_diff = max( + max_rel_diff, + abs( + (approx_value.expected - other_value) + / approx_value.expected + ), + ) + except ZeroDivisionError: + pass + different_ids.append(approx_key) + + message_data = [ + (str(key), str(other_side[key]), str(approx_side_as_map[key])) + for key in different_ids + ] + + return _compare_approx( + self.expected, + message_data, + number_of_elements, + different_ids, + max_abs_diff, + max_rel_diff, + ) + + def __eq__(self, actual) -> bool: + try: + if set(actual.keys()) != set(self.expected.keys()): + return False + except AttributeError: + return False + + return super().__eq__(actual) + + def _yield_comparisons(self, actual): + for k in self.expected.keys(): + yield actual[k], self.expected[k] + + def _check_type(self) -> None: + __tracebackhide__ = True + for key, value in self.expected.items(): + if isinstance(value, type(self.expected)): + msg = "pytest.approx() does not support nested dictionaries: key={!r} value={!r}\n full mapping={}" + raise TypeError(msg.format(key, value, pprint.pformat(self.expected))) + + +class ApproxSequenceLike(ApproxBase): + """Perform approximate comparisons where the expected value is a sequence of numbers.""" + + def __repr__(self) -> str: + seq_type = type(self.expected) + if seq_type not in (tuple, list): + seq_type = list + return f"approx({seq_type(self._approx_scalar(x) for x in self.expected)!r})" + + def _repr_compare(self, other_side: Sequence[float]) -> list[str]: + import math + + if len(self.expected) != len(other_side): + return [ + "Impossible to compare lists with different sizes.", + f"Lengths: {len(self.expected)} and {len(other_side)}", + ] + + approx_side_as_map = _recursive_sequence_map(self._approx_scalar, self.expected) + + number_of_elements = len(approx_side_as_map) + max_abs_diff = -math.inf + max_rel_diff = -math.inf + different_ids = [] + for i, (approx_value, other_value) in enumerate( + zip(approx_side_as_map, other_side, strict=True) + ): + if approx_value != other_value: + try: + abs_diff = abs(approx_value.expected - other_value) + max_abs_diff = max(max_abs_diff, abs_diff) + # Ignore non-numbers for the diff calculations (#13012). + except TypeError: + pass + else: + if other_value == 0.0: + max_rel_diff = math.inf + else: + max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value)) + different_ids.append(i) + message_data = [ + (str(i), str(other_side[i]), str(approx_side_as_map[i])) + for i in different_ids + ] + + return _compare_approx( + self.expected, + message_data, + number_of_elements, + different_ids, + max_abs_diff, + max_rel_diff, + ) + + def __eq__(self, actual) -> bool: + try: + if len(actual) != len(self.expected): + return False + except TypeError: + return False + return super().__eq__(actual) + + def _yield_comparisons(self, actual): + return zip(actual, self.expected, strict=True) + + def _check_type(self) -> None: + __tracebackhide__ = True + for index, x in enumerate(self.expected): + if isinstance(x, type(self.expected)): + msg = "pytest.approx() does not support nested data structures: {!r} at index {}\n full sequence: {}" + raise TypeError(msg.format(x, index, pprint.pformat(self.expected))) + + +class ApproxScalar(ApproxBase): + """Perform approximate comparisons where the expected value is a single number.""" + + # Using Real should be better than this Union, but not possible yet: + # https://github.com/python/typeshed/pull/3108 + DEFAULT_ABSOLUTE_TOLERANCE: float | Decimal = 1e-12 + DEFAULT_RELATIVE_TOLERANCE: float | Decimal = 1e-6 + + def __repr__(self) -> str: + """Return a string communicating both the expected value and the + tolerance for the comparison being made. + + For example, ``1.0 ± 1e-6``, ``(3+4j) ± 5e-6 ∠ ±180°``. + """ + # Don't show a tolerance for values that aren't compared using + # tolerances, i.e. non-numerics and infinities. Need to call abs to + # handle complex numbers, e.g. (inf + 1j). + if ( + isinstance(self.expected, bool) + or (not isinstance(self.expected, Complex | Decimal)) + or math.isinf(abs(self.expected) or isinstance(self.expected, bool)) + ): + return str(self.expected) + + # If a sensible tolerance can't be calculated, self.tolerance will + # raise a ValueError. In this case, display '???'. + try: + if 1e-3 <= self.tolerance < 1e3: + vetted_tolerance = f"{self.tolerance:n}" + else: + vetted_tolerance = f"{self.tolerance:.1e}" + + if ( + isinstance(self.expected, Complex) + and self.expected.imag + and not math.isinf(self.tolerance) + ): + vetted_tolerance += " ∠ ±180°" + except ValueError: + vetted_tolerance = "???" + + return f"{self.expected} ± {vetted_tolerance}" + + def __eq__(self, actual) -> bool: + """Return whether the given value is equal to the expected value + within the pre-specified tolerance.""" + + def is_bool(val: Any) -> bool: + # Check if `val` is a native bool or numpy bool. + if isinstance(val, bool): + return True + if np := sys.modules.get("numpy"): + return isinstance(val, np.bool_) + return False + + asarray = _as_numpy_array(actual) + if asarray is not None: + # Call ``__eq__()`` manually to prevent infinite-recursion with + # numpy<1.13. See #3748. + return all(self.__eq__(a) for a in asarray.flat) + + # Short-circuit exact equality, except for bool and np.bool_ + if is_bool(self.expected) and not is_bool(actual): + return False + elif actual == self.expected: + return True + + # If either type is non-numeric, fall back to strict equality. + # NB: we need Complex, rather than just Number, to ensure that __abs__, + # __sub__, and __float__ are defined. Also, consider bool to be + # non-numeric, even though it has the required arithmetic. + if is_bool(self.expected) or not ( + isinstance(self.expected, Complex | Decimal) + and isinstance(actual, Complex | Decimal) + ): + return False + + # Allow the user to control whether NaNs are considered equal to each + # other or not. The abs() calls are for compatibility with complex + # numbers. + if math.isnan(abs(self.expected)): + return self.nan_ok and math.isnan(abs(actual)) + + # Infinity shouldn't be approximately equal to anything but itself, but + # if there's a relative tolerance, it will be infinite and infinity + # will seem approximately equal to everything. The equal-to-itself + # case would have been short circuited above, so here we can just + # return false if the expected value is infinite. The abs() call is + # for compatibility with complex numbers. + if math.isinf(abs(self.expected)): + return False + + # Return true if the two numbers are within the tolerance. + result: bool = abs(self.expected - actual) <= self.tolerance + return result + + __hash__ = None + + @property + def tolerance(self): + """Return the tolerance for the comparison. + + This could be either an absolute tolerance or a relative tolerance, + depending on what the user specified or which would be larger. + """ + + def set_default(x, default): + return x if x is not None else default + + # Figure out what the absolute tolerance should be. ``self.abs`` is + # either None or a value specified by the user. + absolute_tolerance = set_default(self.abs, self.DEFAULT_ABSOLUTE_TOLERANCE) + + if absolute_tolerance < 0: + raise ValueError( + f"absolute tolerance can't be negative: {absolute_tolerance}" + ) + if math.isnan(absolute_tolerance): + raise ValueError("absolute tolerance can't be NaN.") + + # If the user specified an absolute tolerance but not a relative one, + # just return the absolute tolerance. + if self.rel is None: + if self.abs is not None: + return absolute_tolerance + + # Figure out what the relative tolerance should be. ``self.rel`` is + # either None or a value specified by the user. This is done after + # we've made sure the user didn't ask for an absolute tolerance only, + # because we don't want to raise errors about the relative tolerance if + # we aren't even going to use it. + relative_tolerance = set_default( + self.rel, self.DEFAULT_RELATIVE_TOLERANCE + ) * abs(self.expected) + + if relative_tolerance < 0: + raise ValueError( + f"relative tolerance can't be negative: {relative_tolerance}" + ) + if math.isnan(relative_tolerance): + raise ValueError("relative tolerance can't be NaN.") + + # Return the larger of the relative and absolute tolerances. + return max(relative_tolerance, absolute_tolerance) + + +class ApproxDecimal(ApproxScalar): + """Perform approximate comparisons where the expected value is a Decimal.""" + + DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12") + DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6") + + def __repr__(self) -> str: + if isinstance(self.rel, float): + rel = Decimal.from_float(self.rel) + else: + rel = self.rel + + if isinstance(self.abs, float): + abs_ = Decimal.from_float(self.abs) + else: + abs_ = self.abs + + tol_str = "???" + if rel is not None and Decimal("1e-3") <= rel <= Decimal("1e3"): + tol_str = f"{rel:.1e}" + elif abs_ is not None: + tol_str = f"{abs_:.1e}" + + return f"{self.expected} ± {tol_str}" + + +def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: + """Assert that two numbers (or two ordered sequences of numbers) are equal to each other + within some tolerance. + + Due to the :doc:`python:tutorial/floatingpoint`, numbers that we + would intuitively expect to be equal are not always so:: + + >>> 0.1 + 0.2 == 0.3 + False + + This problem is commonly encountered when writing tests, e.g. when making + sure that floating-point values are what you expect them to be. One way to + deal with this problem is to assert that two floating-point numbers are + equal to within some appropriate tolerance:: + + >>> abs((0.1 + 0.2) - 0.3) < 1e-6 + True + + However, comparisons like this are tedious to write and difficult to + understand. Furthermore, absolute comparisons like the one above are + usually discouraged because there's no tolerance that works well for all + situations. ``1e-6`` is good for numbers around ``1``, but too small for + very big numbers and too big for very small ones. It's better to express + the tolerance as a fraction of the expected value, but relative comparisons + like that are even more difficult to write correctly and concisely. + + The ``approx`` class performs floating-point comparisons using a syntax + that's as intuitive as possible:: + + >>> from pytest import approx + >>> 0.1 + 0.2 == approx(0.3) + True + + The same syntax also works for ordered sequences of numbers:: + + >>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6)) + True + + ``numpy`` arrays:: + + >>> import numpy as np # doctest: +SKIP + >>> np.array([0.1, 0.2]) + np.array([0.2, 0.4]) == approx(np.array([0.3, 0.6])) # doctest: +SKIP + True + + And for a ``numpy`` array against a scalar:: + + >>> import numpy as np # doctest: +SKIP + >>> np.array([0.1, 0.2]) + np.array([0.2, 0.1]) == approx(0.3) # doctest: +SKIP + True + + Only ordered sequences are supported, because ``approx`` needs + to infer the relative position of the sequences without ambiguity. This means + ``sets`` and other unordered sequences are not supported. + + Finally, dictionary *values* can also be compared:: + + >>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6}) + True + + The comparison will be true if both mappings have the same keys and their + respective values match the expected tolerances. + + **Tolerances** + + By default, ``approx`` considers numbers within a relative tolerance of + ``1e-6`` (i.e. one part in a million) of its expected value to be equal. + This treatment would lead to surprising results if the expected value was + ``0.0``, because nothing but ``0.0`` itself is relatively close to ``0.0``. + To handle this case less surprisingly, ``approx`` also considers numbers + within an absolute tolerance of ``1e-12`` of its expected value to be + equal. Infinity and NaN are special cases. Infinity is only considered + equal to itself, regardless of the relative tolerance. NaN is not + considered equal to anything by default, but you can make it be equal to + itself by setting the ``nan_ok`` argument to True. (This is meant to + facilitate comparing arrays that use NaN to mean "no data".) + + Both the relative and absolute tolerances can be changed by passing + arguments to the ``approx`` constructor:: + + >>> 1.0001 == approx(1) + False + >>> 1.0001 == approx(1, rel=1e-3) + True + >>> 1.0001 == approx(1, abs=1e-3) + True + + If you specify ``abs`` but not ``rel``, the comparison will not consider + the relative tolerance at all. In other words, two numbers that are within + the default relative tolerance of ``1e-6`` will still be considered unequal + if they exceed the specified absolute tolerance. If you specify both + ``abs`` and ``rel``, the numbers will be considered equal if either + tolerance is met:: + + >>> 1 + 1e-8 == approx(1) + True + >>> 1 + 1e-8 == approx(1, abs=1e-12) + False + >>> 1 + 1e-8 == approx(1, rel=1e-6, abs=1e-12) + True + + **Non-numeric types** + + You can also use ``approx`` to compare non-numeric types, or dicts and + sequences containing non-numeric types, in which case it falls back to + strict equality. This can be useful for comparing dicts and sequences that + can contain optional values:: + + >>> {"required": 1.0000005, "optional": None} == approx({"required": 1, "optional": None}) + True + >>> [None, 1.0000005] == approx([None,1]) + True + >>> ["foo", 1.0000005] == approx([None,1]) + False + + If you're thinking about using ``approx``, then you might want to know how + it compares to other good ways of comparing floating-point numbers. All of + these algorithms are based on relative and absolute tolerances and should + agree for the most part, but they do have meaningful differences: + + - ``math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0)``: True if the relative + tolerance is met w.r.t. either ``a`` or ``b`` or if the absolute + tolerance is met. Because the relative tolerance is calculated w.r.t. + both ``a`` and ``b``, this test is symmetric (i.e. neither ``a`` nor + ``b`` is a "reference value"). You have to specify an absolute tolerance + if you want to compare to ``0.0`` because there is no tolerance by + default. More information: :py:func:`math.isclose`. + + - ``numpy.isclose(a, b, rtol=1e-5, atol=1e-8)``: True if the difference + between ``a`` and ``b`` is less that the sum of the relative tolerance + w.r.t. ``b`` and the absolute tolerance. Because the relative tolerance + is only calculated w.r.t. ``b``, this test is asymmetric and you can + think of ``b`` as the reference value. Support for comparing sequences + is provided by :py:func:`numpy.allclose`. More information: + :std:doc:`numpy:reference/generated/numpy.isclose`. + + - ``unittest.TestCase.assertAlmostEqual(a, b)``: True if ``a`` and ``b`` + are within an absolute tolerance of ``1e-7``. No relative tolerance is + considered , so this function is not appropriate for very large or very + small numbers. Also, it's only available in subclasses of ``unittest.TestCase`` + and it's ugly because it doesn't follow PEP8. More information: + :py:meth:`unittest.TestCase.assertAlmostEqual`. + + - ``a == pytest.approx(b, rel=1e-6, abs=1e-12)``: True if the relative + tolerance is met w.r.t. ``b`` or if the absolute tolerance is met. + Because the relative tolerance is only calculated w.r.t. ``b``, this test + is asymmetric and you can think of ``b`` as the reference value. In the + special case that you explicitly specify an absolute tolerance but not a + relative tolerance, only the absolute tolerance is considered. + + .. note:: + + ``approx`` can handle numpy arrays, but we recommend the + specialised test helpers in :std:doc:`numpy:reference/routines.testing` + if you need support for comparisons, NaNs, or ULP-based tolerances. + + To match strings using regex, you can use + `Matches `_ + from the + `re_assert package `_. + + + .. note:: + + Unlike built-in equality, this function considers + booleans unequal to numeric zero or one. For example:: + + >>> 1 == approx(True) + False + + .. warning:: + + .. versionchanged:: 3.2 + + In order to avoid inconsistent behavior, :py:exc:`TypeError` is + raised for ``>``, ``>=``, ``<`` and ``<=`` comparisons. + The example below illustrates the problem:: + + assert approx(0.1) > 0.1 + 1e-10 # calls approx(0.1).__gt__(0.1 + 1e-10) + assert 0.1 + 1e-10 > approx(0.1) # calls approx(0.1).__lt__(0.1 + 1e-10) + + In the second example one expects ``approx(0.1).__le__(0.1 + 1e-10)`` + to be called. But instead, ``approx(0.1).__lt__(0.1 + 1e-10)`` is used to + comparison. This is because the call hierarchy of rich comparisons + follows a fixed behavior. More information: :py:meth:`object.__ge__` + + .. versionchanged:: 3.7.1 + ``approx`` raises ``TypeError`` when it encounters a dict value or + sequence element of non-numeric type. + + .. versionchanged:: 6.1.0 + ``approx`` falls back to strict equality for non-numeric types instead + of raising ``TypeError``. + """ + # Delegate the comparison to a class that knows how to deal with the type + # of the expected value (e.g. int, float, list, dict, numpy.array, etc). + # + # The primary responsibility of these classes is to implement ``__eq__()`` + # and ``__repr__()``. The former is used to actually check if some + # "actual" value is equivalent to the given expected value within the + # allowed tolerance. The latter is used to show the user the expected + # value and tolerance, in the case that a test failed. + # + # The actual logic for making approximate comparisons can be found in + # ApproxScalar, which is used to compare individual numbers. All of the + # other Approx classes eventually delegate to this class. The ApproxBase + # class provides some convenient methods and overloads, but isn't really + # essential. + + __tracebackhide__ = True + + if isinstance(expected, Decimal): + cls: type[ApproxBase] = ApproxDecimal + elif isinstance(expected, Mapping): + cls = ApproxMapping + elif _is_numpy_array(expected): + expected = _as_numpy_array(expected) + cls = ApproxNumpy + elif _is_sequence_like(expected): + cls = ApproxSequenceLike + elif isinstance(expected, Collection) and not isinstance(expected, str | bytes): + msg = f"pytest.approx() only supports ordered sequences, but got: {expected!r}" + raise TypeError(msg) + else: + cls = ApproxScalar + + return cls(expected, rel, abs, nan_ok) + + +def _is_sequence_like(expected: object) -> bool: + return ( + hasattr(expected, "__getitem__") + and isinstance(expected, Sized) + and not isinstance(expected, str | bytes) + ) + + +def _is_numpy_array(obj: object) -> bool: + """ + Return true if the given object is implicitly convertible to ndarray, + and numpy is already imported. + """ + return _as_numpy_array(obj) is not None + + +def _as_numpy_array(obj: object) -> ndarray | None: + """ + Return an ndarray if the given object is implicitly convertible to ndarray, + and numpy is already imported, otherwise None. + """ + np: Any = sys.modules.get("numpy") + if np is not None: + # avoid infinite recursion on numpy scalars, which have __array__ + if np.isscalar(obj): + return None + elif isinstance(obj, np.ndarray): + return obj + elif hasattr(obj, "__array__") or hasattr("obj", "__array_interface__"): + return np.asarray(obj) + return None diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/raises.py b/Backend/venv/lib/python3.12/site-packages/_pytest/raises.py new file mode 100644 index 00000000..7c246fde --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/raises.py @@ -0,0 +1,1517 @@ +from __future__ import annotations + +from abc import ABC +from abc import abstractmethod +import re +from re import Pattern +import sys +from textwrap import indent +from typing import Any +from typing import cast +from typing import final +from typing import Generic +from typing import get_args +from typing import get_origin +from typing import Literal +from typing import overload +from typing import TYPE_CHECKING +import warnings + +from _pytest._code import ExceptionInfo +from _pytest._code.code import stringify_exception +from _pytest.outcomes import fail +from _pytest.warning_types import PytestWarning + + +if TYPE_CHECKING: + from collections.abc import Callable + from collections.abc import Sequence + + # for some reason Sphinx does not play well with 'from types import TracebackType' + import types + from typing import TypeGuard + + from typing_extensions import ParamSpec + from typing_extensions import TypeVar + + P = ParamSpec("P") + + # this conditional definition is because we want to allow a TypeVar default + BaseExcT_co_default = TypeVar( + "BaseExcT_co_default", + bound=BaseException, + default=BaseException, + covariant=True, + ) + + # Use short name because it shows up in docs. + E = TypeVar("E", bound=BaseException, default=BaseException) +else: + from typing import TypeVar + + BaseExcT_co_default = TypeVar( + "BaseExcT_co_default", bound=BaseException, covariant=True + ) + +# RaisesGroup doesn't work with a default. +BaseExcT_co = TypeVar("BaseExcT_co", bound=BaseException, covariant=True) +BaseExcT_1 = TypeVar("BaseExcT_1", bound=BaseException) +BaseExcT_2 = TypeVar("BaseExcT_2", bound=BaseException) +ExcT_1 = TypeVar("ExcT_1", bound=Exception) +ExcT_2 = TypeVar("ExcT_2", bound=Exception) + +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup + from exceptiongroup import ExceptionGroup + + +# String patterns default to including the unicode flag. +_REGEX_NO_FLAGS = re.compile(r"").flags + + +# pytest.raises helper +@overload +def raises( + expected_exception: type[E] | tuple[type[E], ...], + *, + match: str | re.Pattern[str] | None = ..., + check: Callable[[E], bool] = ..., +) -> RaisesExc[E]: ... + + +@overload +def raises( + *, + match: str | re.Pattern[str], + # If exception_type is not provided, check() must do any typechecks itself. + check: Callable[[BaseException], bool] = ..., +) -> RaisesExc[BaseException]: ... + + +@overload +def raises(*, check: Callable[[BaseException], bool]) -> RaisesExc[BaseException]: ... + + +@overload +def raises( + expected_exception: type[E] | tuple[type[E], ...], + func: Callable[..., Any], + *args: Any, + **kwargs: Any, +) -> ExceptionInfo[E]: ... + + +def raises( + expected_exception: type[E] | tuple[type[E], ...] | None = None, + *args: Any, + **kwargs: Any, +) -> RaisesExc[BaseException] | ExceptionInfo[E]: + r"""Assert that a code block/function call raises an exception type, or one of its subclasses. + + :param expected_exception: + The expected exception type, or a tuple if one of multiple possible + exception types are expected. Note that subclasses of the passed exceptions + will also match. + + This is not a required parameter, you may opt to only use ``match`` and/or + ``check`` for verifying the raised exception. + + :kwparam str | re.Pattern[str] | None match: + If specified, a string containing a regular expression, + or a regular expression object, that is tested against the string + representation of the exception and its :pep:`678` `__notes__` + using :func:`re.search`. + + To match a literal string that may contain :ref:`special characters + `, the pattern can first be escaped with :func:`re.escape`. + + (This is only used when ``pytest.raises`` is used as a context manager, + and passed through to the function otherwise. + When using ``pytest.raises`` as a function, you can use: + ``pytest.raises(Exc, func, match="passed on").match("my pattern")``.) + + :kwparam Callable[[BaseException], bool] check: + + .. versionadded:: 8.4 + + If specified, a callable that will be called with the exception as a parameter + after checking the type and the match regex if specified. + If it returns ``True`` it will be considered a match, if not it will + be considered a failed match. + + + Use ``pytest.raises`` as a context manager, which will capture the exception of the given + type, or any of its subclasses:: + + >>> import pytest + >>> with pytest.raises(ZeroDivisionError): + ... 1/0 + + If the code block does not raise the expected exception (:class:`ZeroDivisionError` in the example + above), or no exception at all, the check will fail instead. + + You can also use the keyword argument ``match`` to assert that the + exception matches a text or regex:: + + >>> with pytest.raises(ValueError, match='must be 0 or None'): + ... raise ValueError("value must be 0 or None") + + >>> with pytest.raises(ValueError, match=r'must be \d+$'): + ... raise ValueError("value must be 42") + + The ``match`` argument searches the formatted exception string, which includes any + `PEP-678 `__ ``__notes__``: + + >>> with pytest.raises(ValueError, match=r"had a note added"): # doctest: +SKIP + ... e = ValueError("value must be 42") + ... e.add_note("had a note added") + ... raise e + + The ``check`` argument, if provided, must return True when passed the raised exception + for the match to be successful, otherwise an :exc:`AssertionError` is raised. + + >>> import errno + >>> with pytest.raises(OSError, check=lambda e: e.errno == errno.EACCES): + ... raise OSError(errno.EACCES, "no permission to view") + + The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the + details of the captured exception:: + + >>> with pytest.raises(ValueError) as exc_info: + ... raise ValueError("value must be 42") + >>> assert exc_info.type is ValueError + >>> assert exc_info.value.args[0] == "value must be 42" + + .. warning:: + + Given that ``pytest.raises`` matches subclasses, be wary of using it to match :class:`Exception` like this:: + + # Careful, this will catch ANY exception raised. + with pytest.raises(Exception): + some_function() + + Because :class:`Exception` is the base class of almost all exceptions, it is easy for this to hide + real bugs, where the user wrote this expecting a specific exception, but some other exception is being + raised due to a bug introduced during a refactoring. + + Avoid using ``pytest.raises`` to catch :class:`Exception` unless certain that you really want to catch + **any** exception raised. + + .. note:: + + When using ``pytest.raises`` as a context manager, it's worthwhile to + note that normal context manager rules apply and that the exception + raised *must* be the final line in the scope of the context manager. + Lines of code after that, within the scope of the context manager will + not be executed. For example:: + + >>> value = 15 + >>> with pytest.raises(ValueError) as exc_info: + ... if value > 10: + ... raise ValueError("value must be <= 10") + ... assert exc_info.type is ValueError # This will not execute. + + Instead, the following approach must be taken (note the difference in + scope):: + + >>> with pytest.raises(ValueError) as exc_info: + ... if value > 10: + ... raise ValueError("value must be <= 10") + ... + >>> assert exc_info.type is ValueError + + **Expecting exception groups** + + When expecting exceptions wrapped in :exc:`BaseExceptionGroup` or + :exc:`ExceptionGroup`, you should instead use :class:`pytest.RaisesGroup`. + + **Using with** ``pytest.mark.parametrize`` + + When using :ref:`pytest.mark.parametrize ref` + it is possible to parametrize tests such that + some runs raise an exception and others do not. + + See :ref:`parametrizing_conditional_raising` for an example. + + .. seealso:: + + :ref:`assertraises` for more examples and detailed discussion. + + **Legacy form** + + It is possible to specify a callable by passing a to-be-called lambda:: + + >>> raises(ZeroDivisionError, lambda: 1/0) + + + or you can specify an arbitrary callable with arguments:: + + >>> def f(x): return 1/x + ... + >>> raises(ZeroDivisionError, f, 0) + + >>> raises(ZeroDivisionError, f, x=0) + + + The form above is fully supported but discouraged for new code because the + context manager form is regarded as more readable and less error-prone. + + .. note:: + Similar to caught exception objects in Python, explicitly clearing + local references to returned ``ExceptionInfo`` objects can + help the Python interpreter speed up its garbage collection. + + Clearing those references breaks a reference cycle + (``ExceptionInfo`` --> caught exception --> frame stack raising + the exception --> current frame stack --> local variables --> + ``ExceptionInfo``) which makes Python keep all objects referenced + from that cycle (including all local variables in the current + frame) alive until the next cyclic garbage collection run. + More detailed information can be found in the official Python + documentation for :ref:`the try statement `. + """ + __tracebackhide__ = True + + if not args: + if set(kwargs) - {"match", "check", "expected_exception"}: + msg = "Unexpected keyword arguments passed to pytest.raises: " + msg += ", ".join(sorted(kwargs)) + msg += "\nUse context-manager form instead?" + raise TypeError(msg) + + if expected_exception is None: + return RaisesExc(**kwargs) + return RaisesExc(expected_exception, **kwargs) + + if not expected_exception: + raise ValueError( + f"Expected an exception type or a tuple of exception types, but got `{expected_exception!r}`. " + f"Raising exceptions is already understood as failing the test, so you don't need " + f"any special code to say 'this should never raise an exception'." + ) + func = args[0] + if not callable(func): + raise TypeError(f"{func!r} object (type: {type(func)}) must be callable") + with RaisesExc(expected_exception) as excinfo: + func(*args[1:], **kwargs) + try: + return excinfo + finally: + del excinfo + + +# note: RaisesExc/RaisesGroup uses fail() internally, so this alias +# indicates (to [internal] plugins?) that `pytest.raises` will +# raise `_pytest.outcomes.Failed`, where +# `outcomes.Failed is outcomes.fail.Exception is raises.Exception` +# note: this is *not* the same as `_pytest.main.Failed` +# note: mypy does not recognize this attribute, and it's not possible +# to use a protocol/decorator like the others in outcomes due to +# https://github.com/python/mypy/issues/18715 +raises.Exception = fail.Exception # type: ignore[attr-defined] + + +def _match_pattern(match: Pattern[str]) -> str | Pattern[str]: + """Helper function to remove redundant `re.compile` calls when printing regex""" + return match.pattern if match.flags == _REGEX_NO_FLAGS else match + + +def repr_callable(fun: Callable[[BaseExcT_1], bool]) -> str: + """Get the repr of a ``check`` parameter. + + Split out so it can be monkeypatched (e.g. by hypothesis) + """ + return repr(fun) + + +def backquote(s: str) -> str: + return "`" + s + "`" + + +def _exception_type_name( + e: type[BaseException] | tuple[type[BaseException], ...], +) -> str: + if isinstance(e, type): + return e.__name__ + if len(e) == 1: + return e[0].__name__ + return "(" + ", ".join(ee.__name__ for ee in e) + ")" + + +def _check_raw_type( + expected_type: type[BaseException] | tuple[type[BaseException], ...] | None, + exception: BaseException, +) -> str | None: + if expected_type is None or expected_type == (): + return None + + if not isinstance( + exception, + expected_type, + ): + actual_type_str = backquote(_exception_type_name(type(exception)) + "()") + expected_type_str = backquote(_exception_type_name(expected_type)) + if ( + isinstance(exception, BaseExceptionGroup) + and isinstance(expected_type, type) + and not issubclass(expected_type, BaseExceptionGroup) + ): + return f"Unexpected nested {actual_type_str}, expected {expected_type_str}" + return f"{actual_type_str} is not an instance of {expected_type_str}" + return None + + +def is_fully_escaped(s: str) -> bool: + # we know we won't compile with re.VERBOSE, so whitespace doesn't need to be escaped + metacharacters = "{}()+.*?^$[]" + return not any( + c in metacharacters and (i == 0 or s[i - 1] != "\\") for (i, c) in enumerate(s) + ) + + +def unescape(s: str) -> str: + return re.sub(r"\\([{}()+-.*?^$\[\]\s\\])", r"\1", s) + + +# These classes conceptually differ from ExceptionInfo in that ExceptionInfo is tied, and +# constructed from, a particular exception - whereas these are constructed with expected +# exceptions, and later allow matching towards particular exceptions. +# But there's overlap in `ExceptionInfo.match` and `AbstractRaises._check_match`, as with +# `AbstractRaises.matches` and `ExceptionInfo.errisinstance`+`ExceptionInfo.group_contains`. +# The interaction between these classes should perhaps be improved. +class AbstractRaises(ABC, Generic[BaseExcT_co]): + """ABC with common functionality shared between RaisesExc and RaisesGroup""" + + def __init__( + self, + *, + match: str | Pattern[str] | None, + check: Callable[[BaseExcT_co], bool] | None, + ) -> None: + if isinstance(match, str): + # juggle error in order to avoid context to fail (necessary?) + re_error = None + try: + self.match: Pattern[str] | None = re.compile(match) + except re.error as e: + re_error = e + if re_error is not None: + fail(f"Invalid regex pattern provided to 'match': {re_error}") + if match == "": + warnings.warn( + PytestWarning( + "matching against an empty string will *always* pass. If you want " + "to check for an empty message you need to pass '^$'. If you don't " + "want to match you should pass `None` or leave out the parameter." + ), + stacklevel=2, + ) + else: + self.match = match + + # check if this is a fully escaped regex and has ^$ to match fully + # in which case we can do a proper diff on error + self.rawmatch: str | None = None + if isinstance(match, str) or ( + isinstance(match, Pattern) and match.flags == _REGEX_NO_FLAGS + ): + if isinstance(match, Pattern): + match = match.pattern + if ( + match + and match[0] == "^" + and match[-1] == "$" + and is_fully_escaped(match[1:-1]) + ): + self.rawmatch = unescape(match[1:-1]) + + self.check = check + self._fail_reason: str | None = None + + # used to suppress repeated printing of `repr(self.check)` + self._nested: bool = False + + # set in self._parse_exc + self.is_baseexception = False + + def _parse_exc( + self, exc: type[BaseExcT_1] | types.GenericAlias, expected: str + ) -> type[BaseExcT_1]: + if isinstance(exc, type) and issubclass(exc, BaseException): + if not issubclass(exc, Exception): + self.is_baseexception = True + return exc + # because RaisesGroup does not support variable number of exceptions there's + # still a use for RaisesExc(ExceptionGroup[Exception]). + origin_exc: type[BaseException] | None = get_origin(exc) + if origin_exc and issubclass(origin_exc, BaseExceptionGroup): + exc_type = get_args(exc)[0] + if ( + issubclass(origin_exc, ExceptionGroup) and exc_type in (Exception, Any) + ) or ( + issubclass(origin_exc, BaseExceptionGroup) + and exc_type in (BaseException, Any) + ): + if not issubclass(origin_exc, ExceptionGroup): + self.is_baseexception = True + return cast(type[BaseExcT_1], origin_exc) + else: + raise ValueError( + f"Only `ExceptionGroup[Exception]` or `BaseExceptionGroup[BaseException]` " + f"are accepted as generic types but got `{exc}`. " + f"As `raises` will catch all instances of the specified group regardless of the " + f"generic argument specific nested exceptions has to be checked " + f"with `RaisesGroup`." + ) + # unclear if the Type/ValueError distinction is even helpful here + msg = f"Expected {expected}, but got " + if isinstance(exc, type): # type: ignore[unreachable] + raise ValueError(msg + f"{exc.__name__!r}") + if isinstance(exc, BaseException): # type: ignore[unreachable] + raise TypeError(msg + f"an exception instance: {type(exc).__name__}") + raise TypeError(msg + repr(type(exc).__name__)) + + @property + def fail_reason(self) -> str | None: + """Set after a call to :meth:`matches` to give a human-readable reason for why the match failed. + When used as a context manager the string will be printed as the reason for the + test failing.""" + return self._fail_reason + + def _check_check( + self: AbstractRaises[BaseExcT_1], + exception: BaseExcT_1, + ) -> bool: + if self.check is None: + return True + + if self.check(exception): + return True + + check_repr = "" if self._nested else " " + repr_callable(self.check) + self._fail_reason = f"check{check_repr} did not return True" + return False + + # TODO: harmonize with ExceptionInfo.match + def _check_match(self, e: BaseException) -> bool: + if self.match is None or re.search( + self.match, + stringified_exception := stringify_exception( + e, include_subexception_msg=False + ), + ): + return True + + # if we're matching a group, make sure we're explicit to reduce confusion + # if they're trying to match an exception contained within the group + maybe_specify_type = ( + f" the `{_exception_type_name(type(e))}()`" + if isinstance(e, BaseExceptionGroup) + else "" + ) + if isinstance(self.rawmatch, str): + # TODO: it instructs to use `-v` to print leading text, but that doesn't work + # I also don't know if this is the proper entry point, or tool to use at all + from _pytest.assertion.util import _diff_text + from _pytest.assertion.util import dummy_highlighter + + diff = _diff_text(self.rawmatch, stringified_exception, dummy_highlighter) + self._fail_reason = ("\n" if diff[0][0] == "-" else "") + "\n".join(diff) + return False + + self._fail_reason = ( + f"Regex pattern did not match{maybe_specify_type}.\n" + f" Expected regex: {_match_pattern(self.match)!r}\n" + f" Actual message: {stringified_exception!r}" + ) + if _match_pattern(self.match) == stringified_exception: + self._fail_reason += "\n Did you mean to `re.escape()` the regex?" + return False + + @abstractmethod + def matches( + self: AbstractRaises[BaseExcT_1], exception: BaseException + ) -> TypeGuard[BaseExcT_1]: + """Check if an exception matches the requirements of this AbstractRaises. + If it fails, :meth:`AbstractRaises.fail_reason` should be set. + """ + + +@final +class RaisesExc(AbstractRaises[BaseExcT_co_default]): + """ + .. versionadded:: 8.4 + + + This is the class constructed when calling :func:`pytest.raises`, but may be used + directly as a helper class with :class:`RaisesGroup` when you want to specify + requirements on sub-exceptions. + + You don't need this if you only want to specify the type, since :class:`RaisesGroup` + accepts ``type[BaseException]``. + + :param type[BaseException] | tuple[type[BaseException]] | None expected_exception: + The expected type, or one of several possible types. + May be ``None`` in order to only make use of ``match`` and/or ``check`` + + The type is checked with :func:`isinstance`, and does not need to be an exact match. + If that is wanted you can use the ``check`` parameter. + + :kwparam str | Pattern[str] match: + A regex to match. + + :kwparam Callable[[BaseException], bool] check: + If specified, a callable that will be called with the exception as a parameter + after checking the type and the match regex if specified. + If it returns ``True`` it will be considered a match, if not it will + be considered a failed match. + + :meth:`RaisesExc.matches` can also be used standalone to check individual exceptions. + + Examples:: + + with RaisesGroup(RaisesExc(ValueError, match="string")) + ... + with RaisesGroup(RaisesExc(check=lambda x: x.args == (3, "hello"))): + ... + with RaisesGroup(RaisesExc(check=lambda x: type(x) is ValueError)): + ... + """ + + # Trio bundled hypothesis monkeypatching, we will probably instead assume that + # hypothesis will handle that in their pytest plugin by the time this is released. + # Alternatively we could add a version of get_pretty_function_description ourselves + # https://github.com/HypothesisWorks/hypothesis/blob/8ced2f59f5c7bea3344e35d2d53e1f8f8eb9fcd8/hypothesis-python/src/hypothesis/internal/reflection.py#L439 + + # At least one of the three parameters must be passed. + @overload + def __init__( + self, + expected_exception: ( + type[BaseExcT_co_default] | tuple[type[BaseExcT_co_default], ...] + ), + /, + *, + match: str | Pattern[str] | None = ..., + check: Callable[[BaseExcT_co_default], bool] | None = ..., + ) -> None: ... + + @overload + def __init__( + self: RaisesExc[BaseException], # Give E a value. + /, + *, + match: str | Pattern[str] | None, + # If exception_type is not provided, check() must do any typechecks itself. + check: Callable[[BaseException], bool] | None = ..., + ) -> None: ... + + @overload + def __init__(self, /, *, check: Callable[[BaseException], bool]) -> None: ... + + def __init__( + self, + expected_exception: ( + type[BaseExcT_co_default] | tuple[type[BaseExcT_co_default], ...] | None + ) = None, + /, + *, + match: str | Pattern[str] | None = None, + check: Callable[[BaseExcT_co_default], bool] | None = None, + ): + super().__init__(match=match, check=check) + if isinstance(expected_exception, tuple): + expected_exceptions = expected_exception + elif expected_exception is None: + expected_exceptions = () + else: + expected_exceptions = (expected_exception,) + + if (expected_exceptions == ()) and match is None and check is None: + raise ValueError("You must specify at least one parameter to match on.") + + self.expected_exceptions = tuple( + self._parse_exc(e, expected="a BaseException type") + for e in expected_exceptions + ) + + self._just_propagate = False + + def matches( + self, + exception: BaseException | None, + ) -> TypeGuard[BaseExcT_co_default]: + """Check if an exception matches the requirements of this :class:`RaisesExc`. + If it fails, :attr:`RaisesExc.fail_reason` will be set. + + Examples:: + + assert RaisesExc(ValueError).matches(my_exception): + # is equivalent to + assert isinstance(my_exception, ValueError) + + # this can be useful when checking e.g. the ``__cause__`` of an exception. + with pytest.raises(ValueError) as excinfo: + ... + assert RaisesExc(SyntaxError, match="foo").matches(excinfo.value.__cause__) + # above line is equivalent to + assert isinstance(excinfo.value.__cause__, SyntaxError) + assert re.search("foo", str(excinfo.value.__cause__) + + """ + self._just_propagate = False + if exception is None: + self._fail_reason = "exception is None" + return False + if not self._check_type(exception): + self._just_propagate = True + return False + + if not self._check_match(exception): + return False + + return self._check_check(exception) + + def __repr__(self) -> str: + parameters = [] + if self.expected_exceptions: + parameters.append(_exception_type_name(self.expected_exceptions)) + if self.match is not None: + # If no flags were specified, discard the redundant re.compile() here. + parameters.append( + f"match={_match_pattern(self.match)!r}", + ) + if self.check is not None: + parameters.append(f"check={repr_callable(self.check)}") + return f"RaisesExc({', '.join(parameters)})" + + def _check_type(self, exception: BaseException) -> TypeGuard[BaseExcT_co_default]: + self._fail_reason = _check_raw_type(self.expected_exceptions, exception) + return self._fail_reason is None + + def __enter__(self) -> ExceptionInfo[BaseExcT_co_default]: + self.excinfo: ExceptionInfo[BaseExcT_co_default] = ExceptionInfo.for_later() + return self.excinfo + + # TODO: move common code into superclass + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> bool: + __tracebackhide__ = True + if exc_type is None: + if not self.expected_exceptions: + fail("DID NOT RAISE any exception") + if len(self.expected_exceptions) > 1: + fail(f"DID NOT RAISE any of {self.expected_exceptions!r}") + + fail(f"DID NOT RAISE {self.expected_exceptions[0]!r}") + + assert self.excinfo is not None, ( + "Internal error - should have been constructed in __enter__" + ) + + if not self.matches(exc_val): + if self._just_propagate: + return False + raise AssertionError(self._fail_reason) + + # Cast to narrow the exception type now that it's verified.... + # even though the TypeGuard in self.matches should be narrowing + exc_info = cast( + "tuple[type[BaseExcT_co_default], BaseExcT_co_default, types.TracebackType]", + (exc_type, exc_val, exc_tb), + ) + self.excinfo.fill_unfilled(exc_info) + return True + + +@final +class RaisesGroup(AbstractRaises[BaseExceptionGroup[BaseExcT_co]]): + """ + .. versionadded:: 8.4 + + Contextmanager for checking for an expected :exc:`ExceptionGroup`. + This works similar to :func:`pytest.raises`, but allows for specifying the structure of an :exc:`ExceptionGroup`. + :meth:`ExceptionInfo.group_contains` also tries to handle exception groups, + but it is very bad at checking that you *didn't* get unexpected exceptions. + + The catching behaviour differs from :ref:`except* `, being much + stricter about the structure by default. + By using ``allow_unwrapped=True`` and ``flatten_subgroups=True`` you can match + :ref:`except* ` fully when expecting a single exception. + + :param args: + Any number of exception types, :class:`RaisesGroup` or :class:`RaisesExc` + to specify the exceptions contained in this exception. + All specified exceptions must be present in the raised group, *and no others*. + + If you expect a variable number of exceptions you need to use + :func:`pytest.raises(ExceptionGroup) ` and manually check + the contained exceptions. Consider making use of :meth:`RaisesExc.matches`. + + It does not care about the order of the exceptions, so + ``RaisesGroup(ValueError, TypeError)`` + is equivalent to + ``RaisesGroup(TypeError, ValueError)``. + :kwparam str | re.Pattern[str] | None match: + If specified, a string containing a regular expression, + or a regular expression object, that is tested against the string + representation of the exception group and its :pep:`678` `__notes__` + using :func:`re.search`. + + To match a literal string that may contain :ref:`special characters + `, the pattern can first be escaped with :func:`re.escape`. + + Note that " (5 subgroups)" will be stripped from the ``repr`` before matching. + :kwparam Callable[[E], bool] check: + If specified, a callable that will be called with the group as a parameter + after successfully matching the expected exceptions. If it returns ``True`` + it will be considered a match, if not it will be considered a failed match. + :kwparam bool allow_unwrapped: + If expecting a single exception or :class:`RaisesExc` it will match even + if the exception is not inside an exceptiongroup. + + Using this together with ``match``, ``check`` or expecting multiple exceptions + will raise an error. + :kwparam bool flatten_subgroups: + "flatten" any groups inside the raised exception group, extracting all exceptions + inside any nested groups, before matching. Without this it expects you to + fully specify the nesting structure by passing :class:`RaisesGroup` as expected + parameter. + + Examples:: + + with RaisesGroup(ValueError): + raise ExceptionGroup("", (ValueError(),)) + # match + with RaisesGroup( + ValueError, + ValueError, + RaisesExc(TypeError, match="^expected int$"), + match="^my group$", + ): + raise ExceptionGroup( + "my group", + [ + ValueError(), + TypeError("expected int"), + ValueError(), + ], + ) + # check + with RaisesGroup( + KeyboardInterrupt, + match="^hello$", + check=lambda x: isinstance(x.__cause__, ValueError), + ): + raise BaseExceptionGroup("hello", [KeyboardInterrupt()]) from ValueError + # nested groups + with RaisesGroup(RaisesGroup(ValueError)): + raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),)) + + # flatten_subgroups + with RaisesGroup(ValueError, flatten_subgroups=True): + raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),)) + + # allow_unwrapped + with RaisesGroup(ValueError, allow_unwrapped=True): + raise ValueError + + + :meth:`RaisesGroup.matches` can also be used directly to check a standalone exception group. + + + The matching algorithm is greedy, which means cases such as this may fail:: + + with RaisesGroup(ValueError, RaisesExc(ValueError, match="hello")): + raise ExceptionGroup("", (ValueError("hello"), ValueError("goodbye"))) + + even though it generally does not care about the order of the exceptions in the group. + To avoid the above you should specify the first :exc:`ValueError` with a :class:`RaisesExc` as well. + + .. note:: + When raised exceptions don't match the expected ones, you'll get a detailed error + message explaining why. This includes ``repr(check)`` if set, which in Python can be + overly verbose, showing memory locations etc etc. + + If installed and imported (in e.g. ``conftest.py``), the ``hypothesis`` library will + monkeypatch this output to provide shorter & more readable repr's. + """ + + # allow_unwrapped=True requires: singular exception, exception not being + # RaisesGroup instance, match is None, check is None + @overload + def __init__( + self, + expected_exception: type[BaseExcT_co] | RaisesExc[BaseExcT_co], + /, + *, + allow_unwrapped: Literal[True], + flatten_subgroups: bool = False, + ) -> None: ... + + # flatten_subgroups = True also requires no nested RaisesGroup + @overload + def __init__( + self, + expected_exception: type[BaseExcT_co] | RaisesExc[BaseExcT_co], + /, + *other_exceptions: type[BaseExcT_co] | RaisesExc[BaseExcT_co], + flatten_subgroups: Literal[True], + match: str | Pattern[str] | None = None, + check: Callable[[BaseExceptionGroup[BaseExcT_co]], bool] | None = None, + ) -> None: ... + + # simplify the typevars if possible (the following 3 are equivalent but go simpler->complicated) + # ... the first handles RaisesGroup[ValueError], the second RaisesGroup[ExceptionGroup[ValueError]], + # the third RaisesGroup[ValueError | ExceptionGroup[ValueError]]. + # ... otherwise, we will get results like RaisesGroup[ValueError | ExceptionGroup[Never]] (I think) + # (technically correct but misleading) + @overload + def __init__( + self: RaisesGroup[ExcT_1], + expected_exception: type[ExcT_1] | RaisesExc[ExcT_1], + /, + *other_exceptions: type[ExcT_1] | RaisesExc[ExcT_1], + match: str | Pattern[str] | None = None, + check: Callable[[ExceptionGroup[ExcT_1]], bool] | None = None, + ) -> None: ... + + @overload + def __init__( + self: RaisesGroup[ExceptionGroup[ExcT_2]], + expected_exception: RaisesGroup[ExcT_2], + /, + *other_exceptions: RaisesGroup[ExcT_2], + match: str | Pattern[str] | None = None, + check: Callable[[ExceptionGroup[ExceptionGroup[ExcT_2]]], bool] | None = None, + ) -> None: ... + + @overload + def __init__( + self: RaisesGroup[ExcT_1 | ExceptionGroup[ExcT_2]], + expected_exception: type[ExcT_1] | RaisesExc[ExcT_1] | RaisesGroup[ExcT_2], + /, + *other_exceptions: type[ExcT_1] | RaisesExc[ExcT_1] | RaisesGroup[ExcT_2], + match: str | Pattern[str] | None = None, + check: ( + Callable[[ExceptionGroup[ExcT_1 | ExceptionGroup[ExcT_2]]], bool] | None + ) = None, + ) -> None: ... + + # same as the above 3 but handling BaseException + @overload + def __init__( + self: RaisesGroup[BaseExcT_1], + expected_exception: type[BaseExcT_1] | RaisesExc[BaseExcT_1], + /, + *other_exceptions: type[BaseExcT_1] | RaisesExc[BaseExcT_1], + match: str | Pattern[str] | None = None, + check: Callable[[BaseExceptionGroup[BaseExcT_1]], bool] | None = None, + ) -> None: ... + + @overload + def __init__( + self: RaisesGroup[BaseExceptionGroup[BaseExcT_2]], + expected_exception: RaisesGroup[BaseExcT_2], + /, + *other_exceptions: RaisesGroup[BaseExcT_2], + match: str | Pattern[str] | None = None, + check: ( + Callable[[BaseExceptionGroup[BaseExceptionGroup[BaseExcT_2]]], bool] | None + ) = None, + ) -> None: ... + + @overload + def __init__( + self: RaisesGroup[BaseExcT_1 | BaseExceptionGroup[BaseExcT_2]], + expected_exception: type[BaseExcT_1] + | RaisesExc[BaseExcT_1] + | RaisesGroup[BaseExcT_2], + /, + *other_exceptions: type[BaseExcT_1] + | RaisesExc[BaseExcT_1] + | RaisesGroup[BaseExcT_2], + match: str | Pattern[str] | None = None, + check: ( + Callable[ + [BaseExceptionGroup[BaseExcT_1 | BaseExceptionGroup[BaseExcT_2]]], + bool, + ] + | None + ) = None, + ) -> None: ... + + def __init__( + self: RaisesGroup[ExcT_1 | BaseExcT_1 | BaseExceptionGroup[BaseExcT_2]], + expected_exception: type[BaseExcT_1] + | RaisesExc[BaseExcT_1] + | RaisesGroup[BaseExcT_2], + /, + *other_exceptions: type[BaseExcT_1] + | RaisesExc[BaseExcT_1] + | RaisesGroup[BaseExcT_2], + allow_unwrapped: bool = False, + flatten_subgroups: bool = False, + match: str | Pattern[str] | None = None, + check: ( + Callable[[BaseExceptionGroup[BaseExcT_1]], bool] + | Callable[[ExceptionGroup[ExcT_1]], bool] + | None + ) = None, + ): + # The type hint on the `self` and `check` parameters uses different formats + # that are *very* hard to reconcile while adhering to the overloads, so we cast + # it to avoid an error when passing it to super().__init__ + check = cast( + "Callable[[BaseExceptionGroup[ExcT_1|BaseExcT_1|BaseExceptionGroup[BaseExcT_2]]], bool]", + check, + ) + super().__init__(match=match, check=check) + self.allow_unwrapped = allow_unwrapped + self.flatten_subgroups: bool = flatten_subgroups + self.is_baseexception = False + + if allow_unwrapped and other_exceptions: + raise ValueError( + "You cannot specify multiple exceptions with `allow_unwrapped=True.`" + " If you want to match one of multiple possible exceptions you should" + " use a `RaisesExc`." + " E.g. `RaisesExc(check=lambda e: isinstance(e, (...)))`", + ) + if allow_unwrapped and isinstance(expected_exception, RaisesGroup): + raise ValueError( + "`allow_unwrapped=True` has no effect when expecting a `RaisesGroup`." + " You might want it in the expected `RaisesGroup`, or" + " `flatten_subgroups=True` if you don't care about the structure.", + ) + if allow_unwrapped and (match is not None or check is not None): + raise ValueError( + "`allow_unwrapped=True` bypasses the `match` and `check` parameters" + " if the exception is unwrapped. If you intended to match/check the" + " exception you should use a `RaisesExc` object. If you want to match/check" + " the exceptiongroup when the exception *is* wrapped you need to" + " do e.g. `if isinstance(exc.value, ExceptionGroup):" + " assert RaisesGroup(...).matches(exc.value)` afterwards.", + ) + + self.expected_exceptions: tuple[ + type[BaseExcT_co] | RaisesExc[BaseExcT_co] | RaisesGroup[BaseException], ... + ] = tuple( + self._parse_excgroup(e, "a BaseException type, RaisesExc, or RaisesGroup") + for e in ( + expected_exception, + *other_exceptions, + ) + ) + + def _parse_excgroup( + self, + exc: ( + type[BaseExcT_co] + | types.GenericAlias + | RaisesExc[BaseExcT_1] + | RaisesGroup[BaseExcT_2] + ), + expected: str, + ) -> type[BaseExcT_co] | RaisesExc[BaseExcT_1] | RaisesGroup[BaseExcT_2]: + # verify exception type and set `self.is_baseexception` + if isinstance(exc, RaisesGroup): + if self.flatten_subgroups: + raise ValueError( + "You cannot specify a nested structure inside a RaisesGroup with" + " `flatten_subgroups=True`. The parameter will flatten subgroups" + " in the raised exceptiongroup before matching, which would never" + " match a nested structure.", + ) + self.is_baseexception |= exc.is_baseexception + exc._nested = True + return exc + elif isinstance(exc, RaisesExc): + self.is_baseexception |= exc.is_baseexception + exc._nested = True + return exc + elif isinstance(exc, tuple): + raise TypeError( + f"Expected {expected}, but got {type(exc).__name__!r}.\n" + "RaisesGroup does not support tuples of exception types when expecting one of " + "several possible exception types like RaisesExc.\n" + "If you meant to expect a group with multiple exceptions, list them as separate arguments." + ) + else: + return super()._parse_exc(exc, expected) + + @overload + def __enter__( + self: RaisesGroup[ExcT_1], + ) -> ExceptionInfo[ExceptionGroup[ExcT_1]]: ... + @overload + def __enter__( + self: RaisesGroup[BaseExcT_1], + ) -> ExceptionInfo[BaseExceptionGroup[BaseExcT_1]]: ... + + def __enter__(self) -> ExceptionInfo[BaseExceptionGroup[BaseException]]: + self.excinfo: ExceptionInfo[BaseExceptionGroup[BaseExcT_co]] = ( + ExceptionInfo.for_later() + ) + return self.excinfo + + def __repr__(self) -> str: + reqs = [ + e.__name__ if isinstance(e, type) else repr(e) + for e in self.expected_exceptions + ] + if self.allow_unwrapped: + reqs.append(f"allow_unwrapped={self.allow_unwrapped}") + if self.flatten_subgroups: + reqs.append(f"flatten_subgroups={self.flatten_subgroups}") + if self.match is not None: + # If no flags were specified, discard the redundant re.compile() here. + reqs.append(f"match={_match_pattern(self.match)!r}") + if self.check is not None: + reqs.append(f"check={repr_callable(self.check)}") + return f"RaisesGroup({', '.join(reqs)})" + + def _unroll_exceptions( + self, + exceptions: Sequence[BaseException], + ) -> Sequence[BaseException]: + """Used if `flatten_subgroups=True`.""" + res: list[BaseException] = [] + for exc in exceptions: + if isinstance(exc, BaseExceptionGroup): + res.extend(self._unroll_exceptions(exc.exceptions)) + + else: + res.append(exc) + return res + + @overload + def matches( + self: RaisesGroup[ExcT_1], + exception: BaseException | None, + ) -> TypeGuard[ExceptionGroup[ExcT_1]]: ... + @overload + def matches( + self: RaisesGroup[BaseExcT_1], + exception: BaseException | None, + ) -> TypeGuard[BaseExceptionGroup[BaseExcT_1]]: ... + + def matches( + self, + exception: BaseException | None, + ) -> bool: + """Check if an exception matches the requirements of this RaisesGroup. + If it fails, `RaisesGroup.fail_reason` will be set. + + Example:: + + with pytest.raises(TypeError) as excinfo: + ... + assert RaisesGroup(ValueError).matches(excinfo.value.__cause__) + # the above line is equivalent to + myexc = excinfo.value.__cause + assert isinstance(myexc, BaseExceptionGroup) + assert len(myexc.exceptions) == 1 + assert isinstance(myexc.exceptions[0], ValueError) + """ + self._fail_reason = None + if exception is None: + self._fail_reason = "exception is None" + return False + if not isinstance(exception, BaseExceptionGroup): + # we opt to only print type of the exception here, as the repr would + # likely be quite long + not_group_msg = f"`{type(exception).__name__}()` is not an exception group" + if len(self.expected_exceptions) > 1: + self._fail_reason = not_group_msg + return False + # if we have 1 expected exception, check if it would work even if + # allow_unwrapped is not set + res = self._check_expected(self.expected_exceptions[0], exception) + if res is None and self.allow_unwrapped: + return True + + if res is None: + self._fail_reason = ( + f"{not_group_msg}, but would match with `allow_unwrapped=True`" + ) + elif self.allow_unwrapped: + self._fail_reason = res + else: + self._fail_reason = not_group_msg + return False + + actual_exceptions: Sequence[BaseException] = exception.exceptions + if self.flatten_subgroups: + actual_exceptions = self._unroll_exceptions(actual_exceptions) + + if not self._check_match(exception): + self._fail_reason = cast(str, self._fail_reason) + old_reason = self._fail_reason + if ( + len(actual_exceptions) == len(self.expected_exceptions) == 1 + and isinstance(expected := self.expected_exceptions[0], type) + and isinstance(actual := actual_exceptions[0], expected) + and self._check_match(actual) + ): + assert self.match is not None, "can't be None if _check_match failed" + assert self._fail_reason is old_reason is not None + self._fail_reason += ( + f"\n" + f" but matched the expected `{self._repr_expected(expected)}`.\n" + f" You might want " + f"`RaisesGroup(RaisesExc({expected.__name__}, match={_match_pattern(self.match)!r}))`" + ) + else: + self._fail_reason = old_reason + return False + + # do the full check on expected exceptions + if not self._check_exceptions( + exception, + actual_exceptions, + ): + self._fail_reason = cast(str, self._fail_reason) + assert self._fail_reason is not None + old_reason = self._fail_reason + # if we're not expecting a nested structure, and there is one, do a second + # pass where we try flattening it + if ( + not self.flatten_subgroups + and not any( + isinstance(e, RaisesGroup) for e in self.expected_exceptions + ) + and any(isinstance(e, BaseExceptionGroup) for e in actual_exceptions) + and self._check_exceptions( + exception, + self._unroll_exceptions(exception.exceptions), + ) + ): + # only indent if it's a single-line reason. In a multi-line there's already + # indented lines that this does not belong to. + indent = " " if "\n" not in self._fail_reason else "" + self._fail_reason = ( + old_reason + + f"\n{indent}Did you mean to use `flatten_subgroups=True`?" + ) + else: + self._fail_reason = old_reason + return False + + # Only run `self.check` once we know `exception` is of the correct type. + if not self._check_check(exception): + reason = ( + cast(str, self._fail_reason) + f" on the {type(exception).__name__}" + ) + if ( + len(actual_exceptions) == len(self.expected_exceptions) == 1 + and isinstance(expected := self.expected_exceptions[0], type) + # we explicitly break typing here :) + and self._check_check(actual_exceptions[0]) # type: ignore[arg-type] + ): + self._fail_reason = reason + ( + f", but did return True for the expected {self._repr_expected(expected)}." + f" You might want RaisesGroup(RaisesExc({expected.__name__}, check=<...>))" + ) + else: + self._fail_reason = reason + return False + + return True + + @staticmethod + def _check_expected( + expected_type: ( + type[BaseException] | RaisesExc[BaseException] | RaisesGroup[BaseException] + ), + exception: BaseException, + ) -> str | None: + """Helper method for `RaisesGroup.matches` and `RaisesGroup._check_exceptions` + to check one of potentially several expected exceptions.""" + if isinstance(expected_type, type): + return _check_raw_type(expected_type, exception) + res = expected_type.matches(exception) + if res: + return None + assert expected_type.fail_reason is not None + if expected_type.fail_reason.startswith("\n"): + return f"\n{expected_type!r}: {indent(expected_type.fail_reason, ' ')}" + return f"{expected_type!r}: {expected_type.fail_reason}" + + @staticmethod + def _repr_expected(e: type[BaseException] | AbstractRaises[BaseException]) -> str: + """Get the repr of an expected type/RaisesExc/RaisesGroup, but we only want + the name if it's a type""" + if isinstance(e, type): + return _exception_type_name(e) + return repr(e) + + @overload + def _check_exceptions( + self: RaisesGroup[ExcT_1], + _exception: Exception, + actual_exceptions: Sequence[Exception], + ) -> TypeGuard[ExceptionGroup[ExcT_1]]: ... + @overload + def _check_exceptions( + self: RaisesGroup[BaseExcT_1], + _exception: BaseException, + actual_exceptions: Sequence[BaseException], + ) -> TypeGuard[BaseExceptionGroup[BaseExcT_1]]: ... + + def _check_exceptions( + self, + _exception: BaseException, + actual_exceptions: Sequence[BaseException], + ) -> bool: + """Helper method for RaisesGroup.matches that attempts to pair up expected and actual exceptions""" + # The _exception parameter is not used, but necessary for the TypeGuard + + # full table with all results + results = ResultHolder(self.expected_exceptions, actual_exceptions) + + # (indexes of) raised exceptions that haven't (yet) found an expected + remaining_actual = list(range(len(actual_exceptions))) + # (indexes of) expected exceptions that haven't found a matching raised + failed_expected: list[int] = [] + # successful greedy matches + matches: dict[int, int] = {} + + # loop over expected exceptions first to get a more predictable result + for i_exp, expected in enumerate(self.expected_exceptions): + for i_rem in remaining_actual: + res = self._check_expected(expected, actual_exceptions[i_rem]) + results.set_result(i_exp, i_rem, res) + if res is None: + remaining_actual.remove(i_rem) + matches[i_exp] = i_rem + break + else: + failed_expected.append(i_exp) + + # All exceptions matched up successfully + if not remaining_actual and not failed_expected: + return True + + # in case of a single expected and single raised we simplify the output + if 1 == len(actual_exceptions) == len(self.expected_exceptions): + assert not matches + self._fail_reason = res + return False + + # The test case is failing, so we can do a slow and exhaustive check to find + # duplicate matches etc that will be helpful in debugging + for i_exp, expected in enumerate(self.expected_exceptions): + for i_actual, actual in enumerate(actual_exceptions): + if results.has_result(i_exp, i_actual): + continue + results.set_result( + i_exp, i_actual, self._check_expected(expected, actual) + ) + + successful_str = ( + f"{len(matches)} matched exception{'s' if len(matches) > 1 else ''}. " + if matches + else "" + ) + + # all expected were found + if not failed_expected and results.no_match_for_actual(remaining_actual): + self._fail_reason = ( + f"{successful_str}Unexpected exception(s):" + f" {[actual_exceptions[i] for i in remaining_actual]!r}" + ) + return False + # all raised exceptions were expected + if not remaining_actual and results.no_match_for_expected(failed_expected): + no_match_for_str = ", ".join( + self._repr_expected(self.expected_exceptions[i]) + for i in failed_expected + ) + self._fail_reason = f"{successful_str}Too few exceptions raised, found no match for: [{no_match_for_str}]" + return False + + # if there's only one remaining and one failed, and the unmatched didn't match anything else, + # we elect to only print why the remaining and the failed didn't match. + if ( + 1 == len(remaining_actual) == len(failed_expected) + and results.no_match_for_actual(remaining_actual) + and results.no_match_for_expected(failed_expected) + ): + self._fail_reason = f"{successful_str}{results.get_result(failed_expected[0], remaining_actual[0])}" + return False + + # there's both expected and raised exceptions without matches + s = "" + if matches: + s += f"\n{successful_str}" + indent_1 = " " * 2 + indent_2 = " " * 4 + + if not remaining_actual: + s += "\nToo few exceptions raised!" + elif not failed_expected: + s += "\nUnexpected exception(s)!" + + if failed_expected: + s += "\nThe following expected exceptions did not find a match:" + rev_matches = {v: k for k, v in matches.items()} + for i_failed in failed_expected: + s += ( + f"\n{indent_1}{self._repr_expected(self.expected_exceptions[i_failed])}" + ) + for i_actual, actual in enumerate(actual_exceptions): + if results.get_result(i_exp, i_actual) is None: + # we print full repr of match target + s += ( + f"\n{indent_2}It matches {backquote(repr(actual))} which was paired with " + + backquote( + self._repr_expected( + self.expected_exceptions[rev_matches[i_actual]] + ) + ) + ) + + if remaining_actual: + s += "\nThe following raised exceptions did not find a match" + for i_actual in remaining_actual: + s += f"\n{indent_1}{actual_exceptions[i_actual]!r}:" + for i_exp, expected in enumerate(self.expected_exceptions): + res = results.get_result(i_exp, i_actual) + if i_exp in failed_expected: + assert res is not None + if res[0] != "\n": + s += "\n" + s += indent(res, indent_2) + if res is None: + # we print full repr of match target + s += ( + f"\n{indent_2}It matches {backquote(self._repr_expected(expected))} " + f"which was paired with {backquote(repr(actual_exceptions[matches[i_exp]]))}" + ) + + if len(self.expected_exceptions) == len(actual_exceptions) and possible_match( + results + ): + s += ( + "\nThere exist a possible match when attempting an exhaustive check," + " but RaisesGroup uses a greedy algorithm. " + "Please make your expected exceptions more stringent with `RaisesExc` etc" + " so the greedy algorithm can function." + ) + self._fail_reason = s + return False + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> bool: + __tracebackhide__ = True + if exc_type is None: + fail(f"DID NOT RAISE any exception, expected `{self.expected_type()}`") + + assert self.excinfo is not None, ( + "Internal error - should have been constructed in __enter__" + ) + + # group_str is the only thing that differs between RaisesExc and RaisesGroup... + # I might just scrap it? Or make it part of fail_reason + group_str = ( + "(group)" + if self.allow_unwrapped and not issubclass(exc_type, BaseExceptionGroup) + else "group" + ) + + if not self.matches(exc_val): + fail(f"Raised exception {group_str} did not match: {self._fail_reason}") + + # Cast to narrow the exception type now that it's verified.... + # even though the TypeGuard in self.matches should be narrowing + exc_info = cast( + "tuple[type[BaseExceptionGroup[BaseExcT_co]], BaseExceptionGroup[BaseExcT_co], types.TracebackType]", + (exc_type, exc_val, exc_tb), + ) + self.excinfo.fill_unfilled(exc_info) + return True + + def expected_type(self) -> str: + subexcs = [] + for e in self.expected_exceptions: + if isinstance(e, RaisesExc): + subexcs.append(repr(e)) + elif isinstance(e, RaisesGroup): + subexcs.append(e.expected_type()) + elif isinstance(e, type): + subexcs.append(e.__name__) + else: # pragma: no cover + raise AssertionError("unknown type") + group_type = "Base" if self.is_baseexception else "" + return f"{group_type}ExceptionGroup({', '.join(subexcs)})" + + +@final +class NotChecked: + """Singleton for unchecked values in ResultHolder""" + + +class ResultHolder: + """Container for results of checking exceptions. + Used in RaisesGroup._check_exceptions and possible_match. + """ + + def __init__( + self, + expected_exceptions: tuple[ + type[BaseException] | AbstractRaises[BaseException], ... + ], + actual_exceptions: Sequence[BaseException], + ) -> None: + self.results: list[list[str | type[NotChecked] | None]] = [ + [NotChecked for _ in expected_exceptions] for _ in actual_exceptions + ] + + def set_result(self, expected: int, actual: int, result: str | None) -> None: + self.results[actual][expected] = result + + def get_result(self, expected: int, actual: int) -> str | None: + res = self.results[actual][expected] + assert res is not NotChecked + # mypy doesn't support identity checking against anything but None + return res # type: ignore[return-value] + + def has_result(self, expected: int, actual: int) -> bool: + return self.results[actual][expected] is not NotChecked + + def no_match_for_expected(self, expected: list[int]) -> bool: + for i in expected: + for actual_results in self.results: + assert actual_results[i] is not NotChecked + if actual_results[i] is None: + return False + return True + + def no_match_for_actual(self, actual: list[int]) -> bool: + for i in actual: + for res in self.results[i]: + assert res is not NotChecked + if res is None: + return False + return True + + +def possible_match(results: ResultHolder, used: set[int] | None = None) -> bool: + if used is None: + used = set() + curr_row = len(used) + if curr_row == len(results.results): + return True + return any( + val is None and i not in used and possible_match(results, used | {i}) + for (i, val) in enumerate(results.results[curr_row]) + ) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/recwarn.py b/Backend/venv/lib/python3.12/site-packages/_pytest/recwarn.py new file mode 100644 index 00000000..e3db717b --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/recwarn.py @@ -0,0 +1,367 @@ +# mypy: allow-untyped-defs +"""Record warnings during test function execution.""" + +from __future__ import annotations + +from collections.abc import Callable +from collections.abc import Generator +from collections.abc import Iterator +from pprint import pformat +import re +from types import TracebackType +from typing import Any +from typing import final +from typing import overload +from typing import TYPE_CHECKING +from typing import TypeVar + + +if TYPE_CHECKING: + from typing_extensions import Self + +import warnings + +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import fixture +from _pytest.outcomes import Exit +from _pytest.outcomes import fail + + +T = TypeVar("T") + + +@fixture +def recwarn() -> Generator[WarningsRecorder]: + """Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. + + See :ref:`warnings` for information on warning categories. + """ + wrec = WarningsRecorder(_ispytest=True) + with wrec: + warnings.simplefilter("default") + yield wrec + + +@overload +def deprecated_call( + *, match: str | re.Pattern[str] | None = ... +) -> WarningsRecorder: ... + + +@overload +def deprecated_call(func: Callable[..., T], *args: Any, **kwargs: Any) -> T: ... + + +def deprecated_call( + func: Callable[..., Any] | None = None, *args: Any, **kwargs: Any +) -> WarningsRecorder | Any: + """Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning`` or ``FutureWarning``. + + This function can be used as a context manager:: + + >>> import warnings + >>> def api_call_v2(): + ... warnings.warn('use v3 of this api', DeprecationWarning) + ... return 200 + + >>> import pytest + >>> with pytest.deprecated_call(): + ... assert api_call_v2() == 200 + + It can also be used by passing a function and ``*args`` and ``**kwargs``, + in which case it will ensure calling ``func(*args, **kwargs)`` produces one of + the warnings types above. The return value is the return value of the function. + + In the context manager form you may use the keyword argument ``match`` to assert + that the warning matches a text or regex. + + The context manager produces a list of :class:`warnings.WarningMessage` objects, + one for each warning raised. + """ + __tracebackhide__ = True + if func is not None: + args = (func, *args) + return warns( + (DeprecationWarning, PendingDeprecationWarning, FutureWarning), *args, **kwargs + ) + + +@overload +def warns( + expected_warning: type[Warning] | tuple[type[Warning], ...] = ..., + *, + match: str | re.Pattern[str] | None = ..., +) -> WarningsChecker: ... + + +@overload +def warns( + expected_warning: type[Warning] | tuple[type[Warning], ...], + func: Callable[..., T], + *args: Any, + **kwargs: Any, +) -> T: ... + + +def warns( + expected_warning: type[Warning] | tuple[type[Warning], ...] = Warning, + *args: Any, + match: str | re.Pattern[str] | None = None, + **kwargs: Any, +) -> WarningsChecker | Any: + r"""Assert that code raises a particular class of warning. + + Specifically, the parameter ``expected_warning`` can be a warning class or tuple + of warning classes, and the code inside the ``with`` block must issue at least one + warning of that class or classes. + + This helper produces a list of :class:`warnings.WarningMessage` objects, one for + each warning emitted (regardless of whether it is an ``expected_warning`` or not). + Since pytest 8.0, unmatched warnings are also re-emitted when the context closes. + + This function can be used as a context manager:: + + >>> import pytest + >>> with pytest.warns(RuntimeWarning): + ... warnings.warn("my warning", RuntimeWarning) + + In the context manager form you may use the keyword argument ``match`` to assert + that the warning matches a text or regex:: + + >>> with pytest.warns(UserWarning, match='must be 0 or None'): + ... warnings.warn("value must be 0 or None", UserWarning) + + >>> with pytest.warns(UserWarning, match=r'must be \d+$'): + ... warnings.warn("value must be 42", UserWarning) + + >>> with pytest.warns(UserWarning): # catch re-emitted warning + ... with pytest.warns(UserWarning, match=r'must be \d+$'): + ... warnings.warn("this is not here", UserWarning) + Traceback (most recent call last): + ... + Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted... + + **Using with** ``pytest.mark.parametrize`` + + When using :ref:`pytest.mark.parametrize ref` it is possible to parametrize tests + such that some runs raise a warning and others do not. + + This could be achieved in the same way as with exceptions, see + :ref:`parametrizing_conditional_raising` for an example. + + """ + __tracebackhide__ = True + if not args: + if kwargs: + argnames = ", ".join(sorted(kwargs)) + raise TypeError( + f"Unexpected keyword arguments passed to pytest.warns: {argnames}" + "\nUse context-manager form instead?" + ) + return WarningsChecker(expected_warning, match_expr=match, _ispytest=True) + else: + func = args[0] + if not callable(func): + raise TypeError(f"{func!r} object (type: {type(func)}) must be callable") + with WarningsChecker(expected_warning, _ispytest=True): + return func(*args[1:], **kwargs) + + +class WarningsRecorder(warnings.catch_warnings): + """A context manager to record raised warnings. + + Each recorded warning is an instance of :class:`warnings.WarningMessage`. + + Adapted from `warnings.catch_warnings`. + + .. note:: + ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated + differently; see :ref:`ensuring_function_triggers`. + + """ + + def __init__(self, *, _ispytest: bool = False) -> None: + check_ispytest(_ispytest) + super().__init__(record=True) + self._entered = False + self._list: list[warnings.WarningMessage] = [] + + @property + def list(self) -> list[warnings.WarningMessage]: + """The list of recorded warnings.""" + return self._list + + def __getitem__(self, i: int) -> warnings.WarningMessage: + """Get a recorded warning by index.""" + return self._list[i] + + def __iter__(self) -> Iterator[warnings.WarningMessage]: + """Iterate through the recorded warnings.""" + return iter(self._list) + + def __len__(self) -> int: + """The number of recorded warnings.""" + return len(self._list) + + def pop(self, cls: type[Warning] = Warning) -> warnings.WarningMessage: + """Pop the first recorded warning which is an instance of ``cls``, + but not an instance of a child class of any other match. + Raises ``AssertionError`` if there is no match. + """ + best_idx: int | None = None + for i, w in enumerate(self._list): + if w.category == cls: + return self._list.pop(i) # exact match, stop looking + if issubclass(w.category, cls) and ( + best_idx is None + or not issubclass(w.category, self._list[best_idx].category) + ): + best_idx = i + if best_idx is not None: + return self._list.pop(best_idx) + __tracebackhide__ = True + raise AssertionError(f"{cls!r} not found in warning list") + + def clear(self) -> None: + """Clear the list of recorded warnings.""" + self._list[:] = [] + + # Type ignored because we basically want the `catch_warnings` generic type + # parameter to be ourselves but that is not possible(?). + def __enter__(self) -> Self: # type: ignore[override] + if self._entered: + __tracebackhide__ = True + raise RuntimeError(f"Cannot enter {self!r} twice") + _list = super().__enter__() + # record=True means it's None. + assert _list is not None + self._list = _list + warnings.simplefilter("always") + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if not self._entered: + __tracebackhide__ = True + raise RuntimeError(f"Cannot exit {self!r} without entering first") + + super().__exit__(exc_type, exc_val, exc_tb) + + # Built-in catch_warnings does not reset entered state so we do it + # manually here for this context manager to become reusable. + self._entered = False + + +@final +class WarningsChecker(WarningsRecorder): + def __init__( + self, + expected_warning: type[Warning] | tuple[type[Warning], ...] = Warning, + match_expr: str | re.Pattern[str] | None = None, + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + super().__init__(_ispytest=True) + + msg = "exceptions must be derived from Warning, not %s" + if isinstance(expected_warning, tuple): + for exc in expected_warning: + if not issubclass(exc, Warning): + raise TypeError(msg % type(exc)) + expected_warning_tup = expected_warning + elif isinstance(expected_warning, type) and issubclass( + expected_warning, Warning + ): + expected_warning_tup = (expected_warning,) + else: + raise TypeError(msg % type(expected_warning)) + + self.expected_warning = expected_warning_tup + self.match_expr = match_expr + + def matches(self, warning: warnings.WarningMessage) -> bool: + assert self.expected_warning is not None + return issubclass(warning.category, self.expected_warning) and bool( + self.match_expr is None or re.search(self.match_expr, str(warning.message)) + ) + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + super().__exit__(exc_type, exc_val, exc_tb) + + __tracebackhide__ = True + + # BaseExceptions like pytest.{skip,fail,xfail,exit} or Ctrl-C within + # pytest.warns should *not* trigger "DID NOT WARN" and get suppressed + # when the warning doesn't happen. Control-flow exceptions should always + # propagate. + if exc_val is not None and ( + not isinstance(exc_val, Exception) + # Exit is an Exception, not a BaseException, for some reason. + or isinstance(exc_val, Exit) + ): + return + + def found_str() -> str: + return pformat([record.message for record in self], indent=2) + + try: + if not any(issubclass(w.category, self.expected_warning) for w in self): + fail( + f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n" + f" Emitted warnings: {found_str()}." + ) + elif not any(self.matches(w) for w in self): + fail( + f"DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted.\n" + f" Regex: {self.match_expr}\n" + f" Emitted warnings: {found_str()}." + ) + finally: + # Whether or not any warnings matched, we want to re-emit all unmatched warnings. + for w in self: + if not self.matches(w): + warnings.warn_explicit( + message=w.message, + category=w.category, + filename=w.filename, + lineno=w.lineno, + module=w.__module__, + source=w.source, + ) + + # Currently in Python it is possible to pass other types than an + # `str` message when creating `Warning` instances, however this + # causes an exception when :func:`warnings.filterwarnings` is used + # to filter those warnings. See + # https://github.com/python/cpython/issues/103577 for a discussion. + # While this can be considered a bug in CPython, we put guards in + # pytest as the error message produced without this check in place + # is confusing (#10865). + for w in self: + if type(w.message) is not UserWarning: + # If the warning was of an incorrect type then `warnings.warn()` + # creates a UserWarning. Any other warning must have been specified + # explicitly. + continue + if not w.message.args: + # UserWarning() without arguments must have been specified explicitly. + continue + msg = w.message.args[0] + if isinstance(msg, str): + continue + # It's possible that UserWarning was explicitly specified, and + # its first argument was not a string. But that case can't be + # distinguished from an invalid type. + raise TypeError( + f"Warning must be str or Warning, got {msg!r} (type {type(msg).__name__})" + ) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/reports.py b/Backend/venv/lib/python3.12/site-packages/_pytest/reports.py new file mode 100644 index 00000000..011a69db --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/reports.py @@ -0,0 +1,694 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Mapping +from collections.abc import Sequence +import dataclasses +from io import StringIO +import os +from pprint import pprint +import sys +from typing import Any +from typing import cast +from typing import final +from typing import Literal +from typing import NoReturn +from typing import TYPE_CHECKING + +from _pytest._code.code import ExceptionChainRepr +from _pytest._code.code import ExceptionInfo +from _pytest._code.code import ExceptionRepr +from _pytest._code.code import ReprEntry +from _pytest._code.code import ReprEntryNative +from _pytest._code.code import ReprExceptionInfo +from _pytest._code.code import ReprFileLocation +from _pytest._code.code import ReprFuncArgs +from _pytest._code.code import ReprLocals +from _pytest._code.code import ReprTraceback +from _pytest._code.code import TerminalRepr +from _pytest._io import TerminalWriter +from _pytest.config import Config +from _pytest.nodes import Collector +from _pytest.nodes import Item +from _pytest.outcomes import fail +from _pytest.outcomes import skip + + +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup + + +if TYPE_CHECKING: + from typing_extensions import Self + + from _pytest.runner import CallInfo + + +def getworkerinfoline(node): + try: + return node._workerinfocache + except AttributeError: + d = node.workerinfo + ver = "{}.{}.{}".format(*d["version_info"][:3]) + node._workerinfocache = s = "[{}] {} -- Python {} {}".format( + d["id"], d["sysplatform"], ver, d["executable"] + ) + return s + + +class BaseReport: + when: str | None + location: tuple[str, int | None, str] | None + longrepr: ( + None | ExceptionInfo[BaseException] | tuple[str, int, str] | str | TerminalRepr + ) + sections: list[tuple[str, str]] + nodeid: str + outcome: Literal["passed", "failed", "skipped"] + + def __init__(self, **kw: Any) -> None: + self.__dict__.update(kw) + + if TYPE_CHECKING: + # Can have arbitrary fields given to __init__(). + def __getattr__(self, key: str) -> Any: ... + + def toterminal(self, out: TerminalWriter) -> None: + if hasattr(self, "node"): + worker_info = getworkerinfoline(self.node) + if worker_info: + out.line(worker_info) + + longrepr = self.longrepr + if longrepr is None: + return + + if hasattr(longrepr, "toterminal"): + longrepr_terminal = cast(TerminalRepr, longrepr) + longrepr_terminal.toterminal(out) + else: + try: + s = str(longrepr) + except UnicodeEncodeError: + s = "" + out.line(s) + + def get_sections(self, prefix: str) -> Iterator[tuple[str, str]]: + for name, content in self.sections: + if name.startswith(prefix): + yield prefix, content + + @property + def longreprtext(self) -> str: + """Read-only property that returns the full string representation of + ``longrepr``. + + .. versionadded:: 3.0 + """ + file = StringIO() + tw = TerminalWriter(file) + tw.hasmarkup = False + self.toterminal(tw) + exc = file.getvalue() + return exc.strip() + + @property + def caplog(self) -> str: + """Return captured log lines, if log capturing is enabled. + + .. versionadded:: 3.5 + """ + return "\n".join( + content for (prefix, content) in self.get_sections("Captured log") + ) + + @property + def capstdout(self) -> str: + """Return captured text from stdout, if capturing is enabled. + + .. versionadded:: 3.0 + """ + return "".join( + content for (prefix, content) in self.get_sections("Captured stdout") + ) + + @property + def capstderr(self) -> str: + """Return captured text from stderr, if capturing is enabled. + + .. versionadded:: 3.0 + """ + return "".join( + content for (prefix, content) in self.get_sections("Captured stderr") + ) + + @property + def passed(self) -> bool: + """Whether the outcome is passed.""" + return self.outcome == "passed" + + @property + def failed(self) -> bool: + """Whether the outcome is failed.""" + return self.outcome == "failed" + + @property + def skipped(self) -> bool: + """Whether the outcome is skipped.""" + return self.outcome == "skipped" + + @property + def fspath(self) -> str: + """The path portion of the reported node, as a string.""" + return self.nodeid.split("::")[0] + + @property + def count_towards_summary(self) -> bool: + """**Experimental** Whether this report should be counted towards the + totals shown at the end of the test session: "1 passed, 1 failure, etc". + + .. note:: + + This function is considered **experimental**, so beware that it is subject to changes + even in patch releases. + """ + return True + + @property + def head_line(self) -> str | None: + """**Experimental** The head line shown with longrepr output for this + report, more commonly during traceback representation during + failures:: + + ________ Test.foo ________ + + + In the example above, the head_line is "Test.foo". + + .. note:: + + This function is considered **experimental**, so beware that it is subject to changes + even in patch releases. + """ + if self.location is not None: + _fspath, _lineno, domain = self.location + return domain + return None + + def _get_verbose_word_with_markup( + self, config: Config, default_markup: Mapping[str, bool] + ) -> tuple[str, Mapping[str, bool]]: + _category, _short, verbose = config.hook.pytest_report_teststatus( + report=self, config=config + ) + + if isinstance(verbose, str): + return verbose, default_markup + + if isinstance(verbose, Sequence) and len(verbose) == 2: + word, markup = verbose + if isinstance(word, str) and isinstance(markup, Mapping): + return word, markup + + fail( # pragma: no cover + "pytest_report_teststatus() hook (from a plugin) returned " + f"an invalid verbose value: {verbose!r}.\nExpected either a string " + "or a tuple of (word, markup)." + ) + + def _to_json(self) -> dict[str, Any]: + """Return the contents of this report as a dict of builtin entries, + suitable for serialization. + + This was originally the serialize_report() function from xdist (ca03269). + + Experimental method. + """ + return _report_to_json(self) + + @classmethod + def _from_json(cls, reportdict: dict[str, object]) -> Self: + """Create either a TestReport or CollectReport, depending on the calling class. + + It is the callers responsibility to know which class to pass here. + + This was originally the serialize_report() function from xdist (ca03269). + + Experimental method. + """ + kwargs = _report_kwargs_from_json(reportdict) + return cls(**kwargs) + + +def _report_unserialization_failure( + type_name: str, report_class: type[BaseReport], reportdict +) -> NoReturn: + url = "https://github.com/pytest-dev/pytest/issues" + stream = StringIO() + pprint("-" * 100, stream=stream) + pprint(f"INTERNALERROR: Unknown entry type returned: {type_name}", stream=stream) + pprint(f"report_name: {report_class}", stream=stream) + pprint(reportdict, stream=stream) + pprint(f"Please report this bug at {url}", stream=stream) + pprint("-" * 100, stream=stream) + raise RuntimeError(stream.getvalue()) + + +def _format_failed_longrepr( + item: Item, call: CallInfo[None], excinfo: ExceptionInfo[BaseException] +): + if call.when == "call": + longrepr = item.repr_failure(excinfo) + else: + # Exception in setup or teardown. + longrepr = item._repr_failure_py( + excinfo, style=item.config.getoption("tbstyle", "auto") + ) + return longrepr + + +def _format_exception_group_all_skipped_longrepr( + item: Item, + excinfo: ExceptionInfo[BaseExceptionGroup[BaseException | BaseExceptionGroup]], +) -> tuple[str, int, str]: + r = excinfo._getreprcrash() + assert r is not None, ( + "There should always be a traceback entry for skipping a test." + ) + if all( + getattr(skip, "_use_item_location", False) for skip in excinfo.value.exceptions + ): + path, line = item.reportinfo()[:2] + assert line is not None + loc = (os.fspath(path), line + 1) + default_msg = "skipped" + else: + loc = (str(r.path), r.lineno) + default_msg = r.message + + # Get all unique skip messages. + msgs: list[str] = [] + for exception in excinfo.value.exceptions: + m = getattr(exception, "msg", None) or ( + exception.args[0] if exception.args else None + ) + if m and m not in msgs: + msgs.append(m) + + reason = "; ".join(msgs) if msgs else default_msg + longrepr = (*loc, reason) + return longrepr + + +class TestReport(BaseReport): + """Basic test report object (also used for setup and teardown calls if + they fail). + + Reports can contain arbitrary extra attributes. + """ + + __test__ = False + + # Defined by skipping plugin. + # xfail reason if xfailed, otherwise not defined. Use hasattr to distinguish. + wasxfail: str + + def __init__( + self, + nodeid: str, + location: tuple[str, int | None, str], + keywords: Mapping[str, Any], + outcome: Literal["passed", "failed", "skipped"], + longrepr: None + | ExceptionInfo[BaseException] + | tuple[str, int, str] + | str + | TerminalRepr, + when: Literal["setup", "call", "teardown"], + sections: Iterable[tuple[str, str]] = (), + duration: float = 0, + start: float = 0, + stop: float = 0, + user_properties: Iterable[tuple[str, object]] | None = None, + **extra, + ) -> None: + #: Normalized collection nodeid. + self.nodeid = nodeid + + #: A (filesystempath, lineno, domaininfo) tuple indicating the + #: actual location of a test item - it might be different from the + #: collected one e.g. if a method is inherited from a different module. + #: The filesystempath may be relative to ``config.rootdir``. + #: The line number is 0-based. + self.location: tuple[str, int | None, str] = location + + #: A name -> value dictionary containing all keywords and + #: markers associated with a test invocation. + self.keywords: Mapping[str, Any] = keywords + + #: Test outcome, always one of "passed", "failed", "skipped". + self.outcome = outcome + + #: None or a failure representation. + self.longrepr = longrepr + + #: One of 'setup', 'call', 'teardown' to indicate runtest phase. + self.when: Literal["setup", "call", "teardown"] = when + + #: User properties is a list of tuples (name, value) that holds user + #: defined properties of the test. + self.user_properties = list(user_properties or []) + + #: Tuples of str ``(heading, content)`` with extra information + #: for the test report. Used by pytest to add text captured + #: from ``stdout``, ``stderr``, and intercepted logging events. May + #: be used by other plugins to add arbitrary information to reports. + self.sections = list(sections) + + #: Time it took to run just the test. + self.duration: float = duration + + #: The system time when the call started, in seconds since the epoch. + self.start: float = start + #: The system time when the call ended, in seconds since the epoch. + self.stop: float = stop + + self.__dict__.update(extra) + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.nodeid!r} when={self.when!r} outcome={self.outcome!r}>" + + @classmethod + def from_item_and_call(cls, item: Item, call: CallInfo[None]) -> TestReport: + """Create and fill a TestReport with standard item and call info. + + :param item: The item. + :param call: The call info. + """ + when = call.when + # Remove "collect" from the Literal type -- only for collection calls. + assert when != "collect" + duration = call.duration + start = call.start + stop = call.stop + keywords = {x: 1 for x in item.keywords} + excinfo = call.excinfo + sections = [] + if not call.excinfo: + outcome: Literal["passed", "failed", "skipped"] = "passed" + longrepr: ( + None + | ExceptionInfo[BaseException] + | tuple[str, int, str] + | str + | TerminalRepr + ) = None + else: + if not isinstance(excinfo, ExceptionInfo): + outcome = "failed" + longrepr = excinfo + elif isinstance(excinfo.value, skip.Exception): + outcome = "skipped" + r = excinfo._getreprcrash() + assert r is not None, ( + "There should always be a traceback entry for skipping a test." + ) + if excinfo.value._use_item_location: + path, line = item.reportinfo()[:2] + assert line is not None + longrepr = (os.fspath(path), line + 1, r.message) + else: + longrepr = (str(r.path), r.lineno, r.message) + elif isinstance(excinfo.value, BaseExceptionGroup) and ( + excinfo.value.split(skip.Exception)[1] is None + ): + # All exceptions in the group are skip exceptions. + outcome = "skipped" + excinfo = cast( + ExceptionInfo[ + BaseExceptionGroup[BaseException | BaseExceptionGroup] + ], + excinfo, + ) + longrepr = _format_exception_group_all_skipped_longrepr(item, excinfo) + else: + outcome = "failed" + longrepr = _format_failed_longrepr(item, call, excinfo) + for rwhen, key, content in item._report_sections: + sections.append((f"Captured {key} {rwhen}", content)) + return cls( + item.nodeid, + item.location, + keywords, + outcome, + longrepr, + when, + sections, + duration, + start, + stop, + user_properties=item.user_properties, + ) + + +@final +class CollectReport(BaseReport): + """Collection report object. + + Reports can contain arbitrary extra attributes. + """ + + when = "collect" + + def __init__( + self, + nodeid: str, + outcome: Literal["passed", "failed", "skipped"], + longrepr: None + | ExceptionInfo[BaseException] + | tuple[str, int, str] + | str + | TerminalRepr, + result: list[Item | Collector] | None, + sections: Iterable[tuple[str, str]] = (), + **extra, + ) -> None: + #: Normalized collection nodeid. + self.nodeid = nodeid + + #: Test outcome, always one of "passed", "failed", "skipped". + self.outcome = outcome + + #: None or a failure representation. + self.longrepr = longrepr + + #: The collected items and collection nodes. + self.result = result or [] + + #: Tuples of str ``(heading, content)`` with extra information + #: for the test report. Used by pytest to add text captured + #: from ``stdout``, ``stderr``, and intercepted logging events. May + #: be used by other plugins to add arbitrary information to reports. + self.sections = list(sections) + + self.__dict__.update(extra) + + @property + def location( # type:ignore[override] + self, + ) -> tuple[str, int | None, str] | None: + return (self.fspath, None, self.fspath) + + def __repr__(self) -> str: + return f"" + + +class CollectErrorRepr(TerminalRepr): + def __init__(self, msg: str) -> None: + self.longrepr = msg + + def toterminal(self, out: TerminalWriter) -> None: + out.line(self.longrepr, red=True) + + +def pytest_report_to_serializable( + report: CollectReport | TestReport, +) -> dict[str, Any] | None: + if isinstance(report, TestReport | CollectReport): + data = report._to_json() + data["$report_type"] = report.__class__.__name__ + return data + # TODO: Check if this is actually reachable. + return None # type: ignore[unreachable] + + +def pytest_report_from_serializable( + data: dict[str, Any], +) -> CollectReport | TestReport | None: + if "$report_type" in data: + if data["$report_type"] == "TestReport": + return TestReport._from_json(data) + elif data["$report_type"] == "CollectReport": + return CollectReport._from_json(data) + assert False, "Unknown report_type unserialize data: {}".format( + data["$report_type"] + ) + return None + + +def _report_to_json(report: BaseReport) -> dict[str, Any]: + """Return the contents of this report as a dict of builtin entries, + suitable for serialization. + + This was originally the serialize_report() function from xdist (ca03269). + """ + + def serialize_repr_entry( + entry: ReprEntry | ReprEntryNative, + ) -> dict[str, Any]: + data = dataclasses.asdict(entry) + for key, value in data.items(): + if hasattr(value, "__dict__"): + data[key] = dataclasses.asdict(value) + entry_data = {"type": type(entry).__name__, "data": data} + return entry_data + + def serialize_repr_traceback(reprtraceback: ReprTraceback) -> dict[str, Any]: + result = dataclasses.asdict(reprtraceback) + result["reprentries"] = [ + serialize_repr_entry(x) for x in reprtraceback.reprentries + ] + return result + + def serialize_repr_crash( + reprcrash: ReprFileLocation | None, + ) -> dict[str, Any] | None: + if reprcrash is not None: + return dataclasses.asdict(reprcrash) + else: + return None + + def serialize_exception_longrepr(rep: BaseReport) -> dict[str, Any]: + assert rep.longrepr is not None + # TODO: Investigate whether the duck typing is really necessary here. + longrepr = cast(ExceptionRepr, rep.longrepr) + result: dict[str, Any] = { + "reprcrash": serialize_repr_crash(longrepr.reprcrash), + "reprtraceback": serialize_repr_traceback(longrepr.reprtraceback), + "sections": longrepr.sections, + } + if isinstance(longrepr, ExceptionChainRepr): + result["chain"] = [] + for repr_traceback, repr_crash, description in longrepr.chain: + result["chain"].append( + ( + serialize_repr_traceback(repr_traceback), + serialize_repr_crash(repr_crash), + description, + ) + ) + else: + result["chain"] = None + return result + + d = report.__dict__.copy() + if hasattr(report.longrepr, "toterminal"): + if hasattr(report.longrepr, "reprtraceback") and hasattr( + report.longrepr, "reprcrash" + ): + d["longrepr"] = serialize_exception_longrepr(report) + else: + d["longrepr"] = str(report.longrepr) + else: + d["longrepr"] = report.longrepr + for name in d: + if isinstance(d[name], os.PathLike): + d[name] = os.fspath(d[name]) + elif name == "result": + d[name] = None # for now + return d + + +def _report_kwargs_from_json(reportdict: dict[str, Any]) -> dict[str, Any]: + """Return **kwargs that can be used to construct a TestReport or + CollectReport instance. + + This was originally the serialize_report() function from xdist (ca03269). + """ + + def deserialize_repr_entry(entry_data): + data = entry_data["data"] + entry_type = entry_data["type"] + if entry_type == "ReprEntry": + reprfuncargs = None + reprfileloc = None + reprlocals = None + if data["reprfuncargs"]: + reprfuncargs = ReprFuncArgs(**data["reprfuncargs"]) + if data["reprfileloc"]: + reprfileloc = ReprFileLocation(**data["reprfileloc"]) + if data["reprlocals"]: + reprlocals = ReprLocals(data["reprlocals"]["lines"]) + + reprentry: ReprEntry | ReprEntryNative = ReprEntry( + lines=data["lines"], + reprfuncargs=reprfuncargs, + reprlocals=reprlocals, + reprfileloc=reprfileloc, + style=data["style"], + ) + elif entry_type == "ReprEntryNative": + reprentry = ReprEntryNative(data["lines"]) + else: + _report_unserialization_failure(entry_type, TestReport, reportdict) + return reprentry + + def deserialize_repr_traceback(repr_traceback_dict): + repr_traceback_dict["reprentries"] = [ + deserialize_repr_entry(x) for x in repr_traceback_dict["reprentries"] + ] + return ReprTraceback(**repr_traceback_dict) + + def deserialize_repr_crash(repr_crash_dict: dict[str, Any] | None): + if repr_crash_dict is not None: + return ReprFileLocation(**repr_crash_dict) + else: + return None + + if ( + reportdict["longrepr"] + and "reprcrash" in reportdict["longrepr"] + and "reprtraceback" in reportdict["longrepr"] + ): + reprtraceback = deserialize_repr_traceback( + reportdict["longrepr"]["reprtraceback"] + ) + reprcrash = deserialize_repr_crash(reportdict["longrepr"]["reprcrash"]) + if reportdict["longrepr"]["chain"]: + chain = [] + for repr_traceback_data, repr_crash_data, description in reportdict[ + "longrepr" + ]["chain"]: + chain.append( + ( + deserialize_repr_traceback(repr_traceback_data), + deserialize_repr_crash(repr_crash_data), + description, + ) + ) + exception_info: ExceptionChainRepr | ReprExceptionInfo = ExceptionChainRepr( + chain + ) + else: + exception_info = ReprExceptionInfo( + reprtraceback=reprtraceback, + reprcrash=reprcrash, + ) + + for section in reportdict["longrepr"]["sections"]: + exception_info.addsection(*section) + reportdict["longrepr"] = exception_info + + return reportdict diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/runner.py b/Backend/venv/lib/python3.12/site-packages/_pytest/runner.py new file mode 100644 index 00000000..9c20ff9e --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/runner.py @@ -0,0 +1,580 @@ +# mypy: allow-untyped-defs +"""Basic collect and runtest protocol implementations.""" + +from __future__ import annotations + +import bdb +from collections.abc import Callable +import dataclasses +import os +import sys +import types +from typing import cast +from typing import final +from typing import Generic +from typing import Literal +from typing import TYPE_CHECKING +from typing import TypeVar + +from .config import Config +from .reports import BaseReport +from .reports import CollectErrorRepr +from .reports import CollectReport +from .reports import TestReport +from _pytest import timing +from _pytest._code.code import ExceptionChainRepr +from _pytest._code.code import ExceptionInfo +from _pytest._code.code import TerminalRepr +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.nodes import Collector +from _pytest.nodes import Directory +from _pytest.nodes import Item +from _pytest.nodes import Node +from _pytest.outcomes import Exit +from _pytest.outcomes import OutcomeException +from _pytest.outcomes import Skipped +from _pytest.outcomes import TEST_OUTCOME + + +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup + +if TYPE_CHECKING: + from _pytest.main import Session + from _pytest.terminal import TerminalReporter + +# +# pytest plugin hooks. + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("terminal reporting", "Reporting", after="general") + group.addoption( + "--durations", + action="store", + type=int, + default=None, + metavar="N", + help="Show N slowest setup/test durations (N=0 for all)", + ) + group.addoption( + "--durations-min", + action="store", + type=float, + default=None, + metavar="N", + help="Minimal duration in seconds for inclusion in slowest list. " + "Default: 0.005 (or 0.0 if -vv is given).", + ) + + +def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None: + durations = terminalreporter.config.option.durations + durations_min = terminalreporter.config.option.durations_min + verbose = terminalreporter.config.get_verbosity() + if durations is None: + return + if durations_min is None: + durations_min = 0.005 if verbose < 2 else 0.0 + tr = terminalreporter + dlist = [] + for replist in tr.stats.values(): + for rep in replist: + if hasattr(rep, "duration"): + dlist.append(rep) + if not dlist: + return + dlist.sort(key=lambda x: x.duration, reverse=True) + if not durations: + tr.write_sep("=", "slowest durations") + else: + tr.write_sep("=", f"slowest {durations} durations") + dlist = dlist[:durations] + + for i, rep in enumerate(dlist): + if rep.duration < durations_min: + tr.write_line("") + message = f"({len(dlist) - i} durations < {durations_min:g}s hidden." + if terminalreporter.config.option.durations_min is None: + message += " Use -vv to show these durations." + message += ")" + tr.write_line(message) + break + tr.write_line(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}") + + +def pytest_sessionstart(session: Session) -> None: + session._setupstate = SetupState() + + +def pytest_sessionfinish(session: Session) -> None: + session._setupstate.teardown_exact(None) + + +def pytest_runtest_protocol(item: Item, nextitem: Item | None) -> bool: + ihook = item.ihook + ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location) + runtestprotocol(item, nextitem=nextitem) + ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location) + return True + + +def runtestprotocol( + item: Item, log: bool = True, nextitem: Item | None = None +) -> list[TestReport]: + hasrequest = hasattr(item, "_request") + if hasrequest and not item._request: # type: ignore[attr-defined] + # This only happens if the item is re-run, as is done by + # pytest-rerunfailures. + item._initrequest() # type: ignore[attr-defined] + rep = call_and_report(item, "setup", log) + reports = [rep] + if rep.passed: + if item.config.getoption("setupshow", False): + show_test_item(item) + if not item.config.getoption("setuponly", False): + reports.append(call_and_report(item, "call", log)) + # If the session is about to fail or stop, teardown everything - this is + # necessary to correctly report fixture teardown errors (see #11706) + if item.session.shouldfail or item.session.shouldstop: + nextitem = None + reports.append(call_and_report(item, "teardown", log, nextitem=nextitem)) + # After all teardown hooks have been called + # want funcargs and request info to go away. + if hasrequest: + item._request = False # type: ignore[attr-defined] + item.funcargs = None # type: ignore[attr-defined] + return reports + + +def show_test_item(item: Item) -> None: + """Show test function, parameters and the fixtures of the test item.""" + tw = item.config.get_terminal_writer() + tw.line() + tw.write(" " * 8) + tw.write(item.nodeid) + used_fixtures = sorted(getattr(item, "fixturenames", [])) + if used_fixtures: + tw.write(" (fixtures used: {})".format(", ".join(used_fixtures))) + tw.flush() + + +def pytest_runtest_setup(item: Item) -> None: + _update_current_test_var(item, "setup") + item.session._setupstate.setup(item) + + +def pytest_runtest_call(item: Item) -> None: + _update_current_test_var(item, "call") + try: + del sys.last_type + del sys.last_value + del sys.last_traceback + if sys.version_info >= (3, 12, 0): + del sys.last_exc # type:ignore[attr-defined] + except AttributeError: + pass + try: + item.runtest() + except Exception as e: + # Store trace info to allow postmortem debugging + sys.last_type = type(e) + sys.last_value = e + if sys.version_info >= (3, 12, 0): + sys.last_exc = e # type:ignore[attr-defined] + assert e.__traceback__ is not None + # Skip *this* frame + sys.last_traceback = e.__traceback__.tb_next + raise + + +def pytest_runtest_teardown(item: Item, nextitem: Item | None) -> None: + _update_current_test_var(item, "teardown") + item.session._setupstate.teardown_exact(nextitem) + _update_current_test_var(item, None) + + +def _update_current_test_var( + item: Item, when: Literal["setup", "call", "teardown"] | None +) -> None: + """Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage. + + If ``when`` is None, delete ``PYTEST_CURRENT_TEST`` from the environment. + """ + var_name = "PYTEST_CURRENT_TEST" + if when: + value = f"{item.nodeid} ({when})" + # don't allow null bytes on environment variables (see #2644, #2957) + value = value.replace("\x00", "(null)") + os.environ[var_name] = value + else: + os.environ.pop(var_name) + + +def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str] | None: + if report.when in ("setup", "teardown"): + if report.failed: + # category, shortletter, verbose-word + return "error", "E", "ERROR" + elif report.skipped: + return "skipped", "s", "SKIPPED" + else: + return "", "", "" + return None + + +# +# Implementation + + +def call_and_report( + item: Item, when: Literal["setup", "call", "teardown"], log: bool = True, **kwds +) -> TestReport: + ihook = item.ihook + if when == "setup": + runtest_hook: Callable[..., None] = ihook.pytest_runtest_setup + elif when == "call": + runtest_hook = ihook.pytest_runtest_call + elif when == "teardown": + runtest_hook = ihook.pytest_runtest_teardown + else: + assert False, f"Unhandled runtest hook case: {when}" + + call = CallInfo.from_call( + lambda: runtest_hook(item=item, **kwds), + when=when, + reraise=get_reraise_exceptions(item.config), + ) + report: TestReport = ihook.pytest_runtest_makereport(item=item, call=call) + if log: + ihook.pytest_runtest_logreport(report=report) + if check_interactive_exception(call, report): + ihook.pytest_exception_interact(node=item, call=call, report=report) + return report + + +def get_reraise_exceptions(config: Config) -> tuple[type[BaseException], ...]: + """Return exception types that should not be suppressed in general.""" + reraise: tuple[type[BaseException], ...] = (Exit,) + if not config.getoption("usepdb", False): + reraise += (KeyboardInterrupt,) + return reraise + + +def check_interactive_exception(call: CallInfo[object], report: BaseReport) -> bool: + """Check whether the call raised an exception that should be reported as + interactive.""" + if call.excinfo is None: + # Didn't raise. + return False + if hasattr(report, "wasxfail"): + # Exception was expected. + return False + if isinstance(call.excinfo.value, Skipped | bdb.BdbQuit): + # Special control flow exception. + return False + return True + + +TResult = TypeVar("TResult", covariant=True) + + +@final +@dataclasses.dataclass +class CallInfo(Generic[TResult]): + """Result/Exception info of a function invocation.""" + + _result: TResult | None + #: The captured exception of the call, if it raised. + excinfo: ExceptionInfo[BaseException] | None + #: The system time when the call started, in seconds since the epoch. + start: float + #: The system time when the call ended, in seconds since the epoch. + stop: float + #: The call duration, in seconds. + duration: float + #: The context of invocation: "collect", "setup", "call" or "teardown". + when: Literal["collect", "setup", "call", "teardown"] + + def __init__( + self, + result: TResult | None, + excinfo: ExceptionInfo[BaseException] | None, + start: float, + stop: float, + duration: float, + when: Literal["collect", "setup", "call", "teardown"], + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + self._result = result + self.excinfo = excinfo + self.start = start + self.stop = stop + self.duration = duration + self.when = when + + @property + def result(self) -> TResult: + """The return value of the call, if it didn't raise. + + Can only be accessed if excinfo is None. + """ + if self.excinfo is not None: + raise AttributeError(f"{self!r} has no valid result") + # The cast is safe because an exception wasn't raised, hence + # _result has the expected function return type (which may be + # None, that's why a cast and not an assert). + return cast(TResult, self._result) + + @classmethod + def from_call( + cls, + func: Callable[[], TResult], + when: Literal["collect", "setup", "call", "teardown"], + reraise: type[BaseException] | tuple[type[BaseException], ...] | None = None, + ) -> CallInfo[TResult]: + """Call func, wrapping the result in a CallInfo. + + :param func: + The function to call. Called without arguments. + :type func: Callable[[], _pytest.runner.TResult] + :param when: + The phase in which the function is called. + :param reraise: + Exception or exceptions that shall propagate if raised by the + function, instead of being wrapped in the CallInfo. + """ + excinfo = None + instant = timing.Instant() + try: + result: TResult | None = func() + except BaseException: + excinfo = ExceptionInfo.from_current() + if reraise is not None and isinstance(excinfo.value, reraise): + raise + result = None + duration = instant.elapsed() + return cls( + start=duration.start.time, + stop=duration.stop.time, + duration=duration.seconds, + when=when, + result=result, + excinfo=excinfo, + _ispytest=True, + ) + + def __repr__(self) -> str: + if self.excinfo is None: + return f"" + return f"" + + +def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport: + return TestReport.from_item_and_call(item, call) + + +def pytest_make_collect_report(collector: Collector) -> CollectReport: + def collect() -> list[Item | Collector]: + # Before collecting, if this is a Directory, load the conftests. + # If a conftest import fails to load, it is considered a collection + # error of the Directory collector. This is why it's done inside of the + # CallInfo wrapper. + # + # Note: initial conftests are loaded early, not here. + if isinstance(collector, Directory): + collector.config.pluginmanager._loadconftestmodules( + collector.path, + collector.config.getoption("importmode"), + rootpath=collector.config.rootpath, + consider_namespace_packages=collector.config.getini( + "consider_namespace_packages" + ), + ) + + return list(collector.collect()) + + call = CallInfo.from_call( + collect, "collect", reraise=(KeyboardInterrupt, SystemExit) + ) + longrepr: None | tuple[str, int, str] | str | TerminalRepr = None + if not call.excinfo: + outcome: Literal["passed", "skipped", "failed"] = "passed" + else: + skip_exceptions = [Skipped] + unittest = sys.modules.get("unittest") + if unittest is not None: + skip_exceptions.append(unittest.SkipTest) + if isinstance(call.excinfo.value, tuple(skip_exceptions)): + outcome = "skipped" + r_ = collector._repr_failure_py(call.excinfo, "line") + assert isinstance(r_, ExceptionChainRepr), repr(r_) + r = r_.reprcrash + assert r + longrepr = (str(r.path), r.lineno, r.message) + else: + outcome = "failed" + errorinfo = collector.repr_failure(call.excinfo) + if not hasattr(errorinfo, "toterminal"): + assert isinstance(errorinfo, str) + errorinfo = CollectErrorRepr(errorinfo) + longrepr = errorinfo + result = call.result if not call.excinfo else None + rep = CollectReport(collector.nodeid, outcome, longrepr, result) + rep.call = call # type: ignore # see collect_one_node + return rep + + +class SetupState: + """Shared state for setting up/tearing down test items or collectors + in a session. + + Suppose we have a collection tree as follows: + + + + + + + + The SetupState maintains a stack. The stack starts out empty: + + [] + + During the setup phase of item1, setup(item1) is called. What it does + is: + + push session to stack, run session.setup() + push mod1 to stack, run mod1.setup() + push item1 to stack, run item1.setup() + + The stack is: + + [session, mod1, item1] + + While the stack is in this shape, it is allowed to add finalizers to + each of session, mod1, item1 using addfinalizer(). + + During the teardown phase of item1, teardown_exact(item2) is called, + where item2 is the next item to item1. What it does is: + + pop item1 from stack, run its teardowns + pop mod1 from stack, run its teardowns + + mod1 was popped because it ended its purpose with item1. The stack is: + + [session] + + During the setup phase of item2, setup(item2) is called. What it does + is: + + push mod2 to stack, run mod2.setup() + push item2 to stack, run item2.setup() + + Stack: + + [session, mod2, item2] + + During the teardown phase of item2, teardown_exact(None) is called, + because item2 is the last item. What it does is: + + pop item2 from stack, run its teardowns + pop mod2 from stack, run its teardowns + pop session from stack, run its teardowns + + Stack: + + [] + + The end! + """ + + def __init__(self) -> None: + # The stack is in the dict insertion order. + self.stack: dict[ + Node, + tuple[ + # Node's finalizers. + list[Callable[[], object]], + # Node's exception and original traceback, if its setup raised. + tuple[OutcomeException | Exception, types.TracebackType | None] | None, + ], + ] = {} + + def setup(self, item: Item) -> None: + """Setup objects along the collector chain to the item.""" + needed_collectors = item.listchain() + + # If a collector fails its setup, fail its entire subtree of items. + # The setup is not retried for each item - the same exception is used. + for col, (finalizers, exc) in self.stack.items(): + assert col in needed_collectors, "previous item was not torn down properly" + if exc: + raise exc[0].with_traceback(exc[1]) + + for col in needed_collectors[len(self.stack) :]: + assert col not in self.stack + # Push onto the stack. + self.stack[col] = ([col.teardown], None) + try: + col.setup() + except TEST_OUTCOME as exc: + self.stack[col] = (self.stack[col][0], (exc, exc.__traceback__)) + raise + + def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None: + """Attach a finalizer to the given node. + + The node must be currently active in the stack. + """ + assert node and not isinstance(node, tuple) + assert callable(finalizer) + assert node in self.stack, (node, self.stack) + self.stack[node][0].append(finalizer) + + def teardown_exact(self, nextitem: Item | None) -> None: + """Teardown the current stack up until reaching nodes that nextitem + also descends from. + + When nextitem is None (meaning we're at the last item), the entire + stack is torn down. + """ + needed_collectors = (nextitem and nextitem.listchain()) or [] + exceptions: list[BaseException] = [] + while self.stack: + if list(self.stack.keys()) == needed_collectors[: len(self.stack)]: + break + node, (finalizers, _) = self.stack.popitem() + these_exceptions = [] + while finalizers: + fin = finalizers.pop() + try: + fin() + except TEST_OUTCOME as e: + these_exceptions.append(e) + + if len(these_exceptions) == 1: + exceptions.extend(these_exceptions) + elif these_exceptions: + msg = f"errors while tearing down {node!r}" + exceptions.append(BaseExceptionGroup(msg, these_exceptions[::-1])) + + if len(exceptions) == 1: + raise exceptions[0] + elif exceptions: + raise BaseExceptionGroup("errors during test teardown", exceptions[::-1]) + if nextitem is None: + assert not self.stack + + +def collect_one_node(collector: Collector) -> CollectReport: + ihook = collector.ihook + ihook.pytest_collectstart(collector=collector) + rep: CollectReport = ihook.pytest_make_collect_report(collector=collector) + call = rep.__dict__.pop("call", None) + if call and check_interactive_exception(call, rep): + ihook.pytest_exception_interact(node=collector, call=call, report=rep) + return rep diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/scope.py b/Backend/venv/lib/python3.12/site-packages/_pytest/scope.py new file mode 100644 index 00000000..2b007e87 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/scope.py @@ -0,0 +1,91 @@ +""" +Scope definition and related utilities. + +Those are defined here, instead of in the 'fixtures' module because +their use is spread across many other pytest modules, and centralizing it in 'fixtures' +would cause circular references. + +Also this makes the module light to import, as it should. +""" + +from __future__ import annotations + +from enum import Enum +from functools import total_ordering +from typing import Literal + + +_ScopeName = Literal["session", "package", "module", "class", "function"] + + +@total_ordering +class Scope(Enum): + """ + Represents one of the possible fixture scopes in pytest. + + Scopes are ordered from lower to higher, that is: + + ->>> higher ->>> + + Function < Class < Module < Package < Session + + <<<- lower <<<- + """ + + # Scopes need to be listed from lower to higher. + Function = "function" + Class = "class" + Module = "module" + Package = "package" + Session = "session" + + def next_lower(self) -> Scope: + """Return the next lower scope.""" + index = _SCOPE_INDICES[self] + if index == 0: + raise ValueError(f"{self} is the lower-most scope") + return _ALL_SCOPES[index - 1] + + def next_higher(self) -> Scope: + """Return the next higher scope.""" + index = _SCOPE_INDICES[self] + if index == len(_SCOPE_INDICES) - 1: + raise ValueError(f"{self} is the upper-most scope") + return _ALL_SCOPES[index + 1] + + def __lt__(self, other: Scope) -> bool: + self_index = _SCOPE_INDICES[self] + other_index = _SCOPE_INDICES[other] + return self_index < other_index + + @classmethod + def from_user( + cls, scope_name: _ScopeName, descr: str, where: str | None = None + ) -> Scope: + """ + Given a scope name from the user, return the equivalent Scope enum. Should be used + whenever we want to convert a user provided scope name to its enum object. + + If the scope name is invalid, construct a user friendly message and call pytest.fail. + """ + from _pytest.outcomes import fail + + try: + # Holding this reference is necessary for mypy at the moment. + scope = Scope(scope_name) + except ValueError: + fail( + "{} {}got an unexpected scope value '{}'".format( + descr, f"from {where} " if where else "", scope_name + ), + pytrace=False, + ) + return scope + + +_ALL_SCOPES = list(Scope) +_SCOPE_INDICES = {scope: index for index, scope in enumerate(_ALL_SCOPES)} + + +# Ordered list of scopes which can contain many tests (in practice all except Function). +HIGH_SCOPES = [x for x in Scope if x is not Scope.Function] diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/setuponly.py b/Backend/venv/lib/python3.12/site-packages/_pytest/setuponly.py new file mode 100644 index 00000000..7e6b46bc --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/setuponly.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from collections.abc import Generator + +from _pytest._io.saferepr import saferepr +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config.argparsing import Parser +from _pytest.fixtures import FixtureDef +from _pytest.fixtures import SubRequest +from _pytest.scope import Scope +import pytest + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("debugconfig") + group.addoption( + "--setuponly", + "--setup-only", + action="store_true", + help="Only setup fixtures, do not execute tests", + ) + group.addoption( + "--setupshow", + "--setup-show", + action="store_true", + help="Show setup of fixtures while executing tests", + ) + + +@pytest.hookimpl(wrapper=True) +def pytest_fixture_setup( + fixturedef: FixtureDef[object], request: SubRequest +) -> Generator[None, object, object]: + try: + return (yield) + finally: + if request.config.option.setupshow: + if hasattr(request, "param"): + # Save the fixture parameter so ._show_fixture_action() can + # display it now and during the teardown (in .finish()). + if fixturedef.ids: + if callable(fixturedef.ids): + param = fixturedef.ids(request.param) + else: + param = fixturedef.ids[request.param_index] + else: + param = request.param + fixturedef.cached_param = param # type: ignore[attr-defined] + _show_fixture_action(fixturedef, request.config, "SETUP") + + +def pytest_fixture_post_finalizer( + fixturedef: FixtureDef[object], request: SubRequest +) -> None: + if fixturedef.cached_result is not None: + config = request.config + if config.option.setupshow: + _show_fixture_action(fixturedef, request.config, "TEARDOWN") + if hasattr(fixturedef, "cached_param"): + del fixturedef.cached_param + + +def _show_fixture_action( + fixturedef: FixtureDef[object], config: Config, msg: str +) -> None: + capman = config.pluginmanager.getplugin("capturemanager") + if capman: + capman.suspend_global_capture() + + tw = config.get_terminal_writer() + tw.line() + # Use smaller indentation the higher the scope: Session = 0, Package = 1, etc. + scope_indent = list(reversed(Scope)).index(fixturedef._scope) + tw.write(" " * 2 * scope_indent) + + scopename = fixturedef.scope[0].upper() + tw.write(f"{msg:<8} {scopename} {fixturedef.argname}") + + if msg == "SETUP": + deps = sorted(arg for arg in fixturedef.argnames if arg != "request") + if deps: + tw.write(" (fixtures used: {})".format(", ".join(deps))) + + if hasattr(fixturedef, "cached_param"): + tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]") + + tw.flush() + + if capman: + capman.resume_global_capture() + + +@pytest.hookimpl(tryfirst=True) +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: + if config.option.setuponly: + config.option.setupshow = True + return None diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/setupplan.py b/Backend/venv/lib/python3.12/site-packages/_pytest/setupplan.py new file mode 100644 index 00000000..4e124cce --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/setupplan.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config.argparsing import Parser +from _pytest.fixtures import FixtureDef +from _pytest.fixtures import SubRequest +import pytest + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("debugconfig") + group.addoption( + "--setupplan", + "--setup-plan", + action="store_true", + help="Show what fixtures and tests would be executed but " + "don't execute anything", + ) + + +@pytest.hookimpl(tryfirst=True) +def pytest_fixture_setup( + fixturedef: FixtureDef[object], request: SubRequest +) -> object | None: + # Will return a dummy fixture if the setuponly option is provided. + if request.config.option.setupplan: + my_cache_key = fixturedef.cache_key(request) + fixturedef.cached_result = (None, my_cache_key, None) + return fixturedef.cached_result + return None + + +@pytest.hookimpl(tryfirst=True) +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: + if config.option.setupplan: + config.option.setuponly = True + config.option.setupshow = True + return None diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/skipping.py b/Backend/venv/lib/python3.12/site-packages/_pytest/skipping.py new file mode 100644 index 00000000..3b067629 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/skipping.py @@ -0,0 +1,321 @@ +# mypy: allow-untyped-defs +"""Support for skip/xfail functions and markers.""" + +from __future__ import annotations + +from collections.abc import Generator +from collections.abc import Mapping +import dataclasses +import os +import platform +import sys +import traceback + +from _pytest.config import Config +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.mark.structures import Mark +from _pytest.nodes import Item +from _pytest.outcomes import fail +from _pytest.outcomes import skip +from _pytest.outcomes import xfail +from _pytest.raises import AbstractRaises +from _pytest.reports import BaseReport +from _pytest.reports import TestReport +from _pytest.runner import CallInfo +from _pytest.stash import StashKey + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("general") + group.addoption( + "--runxfail", + action="store_true", + dest="runxfail", + default=False, + help="Report the results of xfail tests as if they were not marked", + ) + + parser.addini( + "strict_xfail", + "Default for the strict parameter of xfail " + "markers when not given explicitly (default: False) (alias: xfail_strict)", + type="bool", + # None => fallback to `strict`. + default=None, + aliases=["xfail_strict"], + ) + + +def pytest_configure(config: Config) -> None: + if config.option.runxfail: + # yay a hack + import pytest + + old = pytest.xfail + config.add_cleanup(lambda: setattr(pytest, "xfail", old)) + + def nop(*args, **kwargs): + pass + + nop.Exception = xfail.Exception # type: ignore[attr-defined] + setattr(pytest, "xfail", nop) + + config.addinivalue_line( + "markers", + "skip(reason=None): skip the given test function with an optional reason. " + 'Example: skip(reason="no way of currently testing this") skips the ' + "test.", + ) + config.addinivalue_line( + "markers", + "skipif(condition, ..., *, reason=...): " + "skip the given test function if any of the conditions evaluate to True. " + "Example: skipif(sys.platform == 'win32') skips the test if we are on the win32 platform. " + "See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-skipif", + ) + config.addinivalue_line( + "markers", + "xfail(condition, ..., *, reason=..., run=True, raises=None, strict=strict_xfail): " + "mark the test function as an expected failure if any of the conditions " + "evaluate to True. Optionally specify a reason for better reporting " + "and run=False if you don't even want to execute the test function. " + "If only specific exception(s) are expected, you can list them in " + "raises, and if the test fails in other ways, it will be reported as " + "a true failure. See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-xfail", + ) + + +def evaluate_condition(item: Item, mark: Mark, condition: object) -> tuple[bool, str]: + """Evaluate a single skipif/xfail condition. + + If an old-style string condition is given, it is eval()'d, otherwise the + condition is bool()'d. If this fails, an appropriately formatted pytest.fail + is raised. + + Returns (result, reason). The reason is only relevant if the result is True. + """ + # String condition. + if isinstance(condition, str): + globals_ = { + "os": os, + "sys": sys, + "platform": platform, + "config": item.config, + } + for dictionary in reversed( + item.ihook.pytest_markeval_namespace(config=item.config) + ): + if not isinstance(dictionary, Mapping): + raise ValueError( + f"pytest_markeval_namespace() needs to return a dict, got {dictionary!r}" + ) + globals_.update(dictionary) + if hasattr(item, "obj"): + globals_.update(item.obj.__globals__) + try: + filename = f"<{mark.name} condition>" + condition_code = compile(condition, filename, "eval") + result = eval(condition_code, globals_) + except SyntaxError as exc: + msglines = [ + f"Error evaluating {mark.name!r} condition", + " " + condition, + " " + " " * (exc.offset or 0) + "^", + "SyntaxError: invalid syntax", + ] + fail("\n".join(msglines), pytrace=False) + except Exception as exc: + msglines = [ + f"Error evaluating {mark.name!r} condition", + " " + condition, + *traceback.format_exception_only(type(exc), exc), + ] + fail("\n".join(msglines), pytrace=False) + + # Boolean condition. + else: + try: + result = bool(condition) + except Exception as exc: + msglines = [ + f"Error evaluating {mark.name!r} condition as a boolean", + *traceback.format_exception_only(type(exc), exc), + ] + fail("\n".join(msglines), pytrace=False) + + reason = mark.kwargs.get("reason", None) + if reason is None: + if isinstance(condition, str): + reason = "condition: " + condition + else: + # XXX better be checked at collection time + msg = ( + f"Error evaluating {mark.name!r}: " + + "you need to specify reason=STRING when using booleans as conditions." + ) + fail(msg, pytrace=False) + + return result, reason + + +@dataclasses.dataclass(frozen=True) +class Skip: + """The result of evaluate_skip_marks().""" + + reason: str = "unconditional skip" + + +def evaluate_skip_marks(item: Item) -> Skip | None: + """Evaluate skip and skipif marks on item, returning Skip if triggered.""" + for mark in item.iter_markers(name="skipif"): + if "condition" not in mark.kwargs: + conditions = mark.args + else: + conditions = (mark.kwargs["condition"],) + + # Unconditional. + if not conditions: + reason = mark.kwargs.get("reason", "") + return Skip(reason) + + # If any of the conditions are true. + for condition in conditions: + result, reason = evaluate_condition(item, mark, condition) + if result: + return Skip(reason) + + for mark in item.iter_markers(name="skip"): + try: + return Skip(*mark.args, **mark.kwargs) + except TypeError as e: + raise TypeError(str(e) + " - maybe you meant pytest.mark.skipif?") from None + + return None + + +@dataclasses.dataclass(frozen=True) +class Xfail: + """The result of evaluate_xfail_marks().""" + + __slots__ = ("raises", "reason", "run", "strict") + + reason: str + run: bool + strict: bool + raises: ( + type[BaseException] + | tuple[type[BaseException], ...] + | AbstractRaises[BaseException] + | None + ) + + +def evaluate_xfail_marks(item: Item) -> Xfail | None: + """Evaluate xfail marks on item, returning Xfail if triggered.""" + for mark in item.iter_markers(name="xfail"): + run = mark.kwargs.get("run", True) + strict = mark.kwargs.get("strict") + if strict is None: + strict = item.config.getini("strict_xfail") + if strict is None: + strict = item.config.getini("strict") + raises = mark.kwargs.get("raises", None) + if "condition" not in mark.kwargs: + conditions = mark.args + else: + conditions = (mark.kwargs["condition"],) + + # Unconditional. + if not conditions: + reason = mark.kwargs.get("reason", "") + return Xfail(reason, run, strict, raises) + + # If any of the conditions are true. + for condition in conditions: + result, reason = evaluate_condition(item, mark, condition) + if result: + return Xfail(reason, run, strict, raises) + + return None + + +# Saves the xfail mark evaluation. Can be refreshed during call if None. +xfailed_key = StashKey[Xfail | None]() + + +@hookimpl(tryfirst=True) +def pytest_runtest_setup(item: Item) -> None: + skipped = evaluate_skip_marks(item) + if skipped: + raise skip.Exception(skipped.reason, _use_item_location=True) + + item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) + if xfailed and not item.config.option.runxfail and not xfailed.run: + xfail("[NOTRUN] " + xfailed.reason) + + +@hookimpl(wrapper=True) +def pytest_runtest_call(item: Item) -> Generator[None]: + xfailed = item.stash.get(xfailed_key, None) + if xfailed is None: + item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) + + if xfailed and not item.config.option.runxfail and not xfailed.run: + xfail("[NOTRUN] " + xfailed.reason) + + try: + return (yield) + finally: + # The test run may have added an xfail mark dynamically. + xfailed = item.stash.get(xfailed_key, None) + if xfailed is None: + item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) + + +@hookimpl(wrapper=True) +def pytest_runtest_makereport( + item: Item, call: CallInfo[None] +) -> Generator[None, TestReport, TestReport]: + rep = yield + xfailed = item.stash.get(xfailed_key, None) + if item.config.option.runxfail: + pass # don't interfere + elif call.excinfo and isinstance(call.excinfo.value, xfail.Exception): + assert call.excinfo.value.msg is not None + rep.wasxfail = call.excinfo.value.msg + rep.outcome = "skipped" + elif not rep.skipped and xfailed: + if call.excinfo: + raises = xfailed.raises + if raises is None or ( + ( + isinstance(raises, type | tuple) + and isinstance(call.excinfo.value, raises) + ) + or ( + isinstance(raises, AbstractRaises) + and raises.matches(call.excinfo.value) + ) + ): + rep.outcome = "skipped" + rep.wasxfail = xfailed.reason + else: + rep.outcome = "failed" + elif call.when == "call": + if xfailed.strict: + rep.outcome = "failed" + rep.longrepr = "[XPASS(strict)] " + xfailed.reason + else: + rep.outcome = "passed" + rep.wasxfail = xfailed.reason + return rep + + +def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str] | None: + if hasattr(report, "wasxfail"): + if report.skipped: + return "xfailed", "x", "XFAIL" + elif report.passed: + return "xpassed", "X", "XPASS" + return None diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/stash.py b/Backend/venv/lib/python3.12/site-packages/_pytest/stash.py new file mode 100644 index 00000000..6a9ff884 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/stash.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from typing import Any +from typing import cast +from typing import Generic +from typing import TypeVar + + +__all__ = ["Stash", "StashKey"] + + +T = TypeVar("T") +D = TypeVar("D") + + +class StashKey(Generic[T]): + """``StashKey`` is an object used as a key to a :class:`Stash`. + + A ``StashKey`` is associated with the type ``T`` of the value of the key. + + A ``StashKey`` is unique and cannot conflict with another key. + + .. versionadded:: 7.0 + """ + + __slots__ = () + + +class Stash: + r"""``Stash`` is a type-safe heterogeneous mutable mapping that + allows keys and value types to be defined separately from + where it (the ``Stash``) is created. + + Usually you will be given an object which has a ``Stash``, for example + :class:`~pytest.Config` or a :class:`~_pytest.nodes.Node`: + + .. code-block:: python + + stash: Stash = some_object.stash + + If a module or plugin wants to store data in this ``Stash``, it creates + :class:`StashKey`\s for its keys (at the module level): + + .. code-block:: python + + # At the top-level of the module + some_str_key = StashKey[str]() + some_bool_key = StashKey[bool]() + + To store information: + + .. code-block:: python + + # Value type must match the key. + stash[some_str_key] = "value" + stash[some_bool_key] = True + + To retrieve the information: + + .. code-block:: python + + # The static type of some_str is str. + some_str = stash[some_str_key] + # The static type of some_bool is bool. + some_bool = stash[some_bool_key] + + .. versionadded:: 7.0 + """ + + __slots__ = ("_storage",) + + def __init__(self) -> None: + self._storage: dict[StashKey[Any], object] = {} + + def __setitem__(self, key: StashKey[T], value: T) -> None: + """Set a value for key.""" + self._storage[key] = value + + def __getitem__(self, key: StashKey[T]) -> T: + """Get the value for key. + + Raises ``KeyError`` if the key wasn't set before. + """ + return cast(T, self._storage[key]) + + def get(self, key: StashKey[T], default: D) -> T | D: + """Get the value for key, or return default if the key wasn't set + before.""" + try: + return self[key] + except KeyError: + return default + + def setdefault(self, key: StashKey[T], default: T) -> T: + """Return the value of key if already set, otherwise set the value + of key to default and return default.""" + try: + return self[key] + except KeyError: + self[key] = default + return default + + def __delitem__(self, key: StashKey[T]) -> None: + """Delete the value for key. + + Raises ``KeyError`` if the key wasn't set before. + """ + del self._storage[key] + + def __contains__(self, key: StashKey[T]) -> bool: + """Return whether key was set.""" + return key in self._storage + + def __len__(self) -> int: + """Return how many items exist in the stash.""" + return len(self._storage) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/stepwise.py b/Backend/venv/lib/python3.12/site-packages/_pytest/stepwise.py new file mode 100644 index 00000000..8901540e --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/stepwise.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +import dataclasses +from datetime import datetime +from datetime import timedelta +from typing import Any +from typing import TYPE_CHECKING + +from _pytest import nodes +from _pytest.cacheprovider import Cache +from _pytest.config import Config +from _pytest.config.argparsing import Parser +from _pytest.main import Session +from _pytest.reports import TestReport + + +if TYPE_CHECKING: + from typing_extensions import Self + +STEPWISE_CACHE_DIR = "cache/stepwise" + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("general") + group.addoption( + "--sw", + "--stepwise", + action="store_true", + default=False, + dest="stepwise", + help="Exit on test failure and continue from last failing test next time", + ) + group.addoption( + "--sw-skip", + "--stepwise-skip", + action="store_true", + default=False, + dest="stepwise_skip", + help="Ignore the first failing test but stop on the next failing test. " + "Implicitly enables --stepwise.", + ) + group.addoption( + "--sw-reset", + "--stepwise-reset", + action="store_true", + default=False, + dest="stepwise_reset", + help="Resets stepwise state, restarting the stepwise workflow. " + "Implicitly enables --stepwise.", + ) + + +def pytest_configure(config: Config) -> None: + # --stepwise-skip/--stepwise-reset implies stepwise. + if config.option.stepwise_skip or config.option.stepwise_reset: + config.option.stepwise = True + if config.getoption("stepwise"): + config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin") + + +def pytest_sessionfinish(session: Session) -> None: + if not session.config.getoption("stepwise"): + assert session.config.cache is not None + if hasattr(session.config, "workerinput"): + # Do not update cache if this process is a xdist worker to prevent + # race conditions (#10641). + return + + +@dataclasses.dataclass +class StepwiseCacheInfo: + # The nodeid of the last failed test. + last_failed: str | None + + # The number of tests in the last time --stepwise was run. + # We use this information as a simple way to invalidate the cache information, avoiding + # confusing behavior in case the cache is stale. + last_test_count: int | None + + # The date when the cache was last updated, for information purposes only. + last_cache_date_str: str + + @property + def last_cache_date(self) -> datetime: + return datetime.fromisoformat(self.last_cache_date_str) + + @classmethod + def empty(cls) -> Self: + return cls( + last_failed=None, + last_test_count=None, + last_cache_date_str=datetime.now().isoformat(), + ) + + def update_date_to_now(self) -> None: + self.last_cache_date_str = datetime.now().isoformat() + + +class StepwisePlugin: + def __init__(self, config: Config) -> None: + self.config = config + self.session: Session | None = None + self.report_status: list[str] = [] + assert config.cache is not None + self.cache: Cache = config.cache + self.skip: bool = config.getoption("stepwise_skip") + self.reset: bool = config.getoption("stepwise_reset") + self.cached_info = self._load_cached_info() + + def _load_cached_info(self) -> StepwiseCacheInfo: + cached_dict: dict[str, Any] | None = self.cache.get(STEPWISE_CACHE_DIR, None) + if cached_dict: + try: + return StepwiseCacheInfo( + cached_dict["last_failed"], + cached_dict["last_test_count"], + cached_dict["last_cache_date_str"], + ) + except (KeyError, TypeError) as e: + error = f"{type(e).__name__}: {e}" + self.report_status.append(f"error reading cache, discarding ({error})") + + # Cache not found or error during load, return a new cache. + return StepwiseCacheInfo.empty() + + def pytest_sessionstart(self, session: Session) -> None: + self.session = session + + def pytest_collection_modifyitems( + self, config: Config, items: list[nodes.Item] + ) -> None: + last_test_count = self.cached_info.last_test_count + self.cached_info.last_test_count = len(items) + + if self.reset: + self.report_status.append("resetting state, not skipping.") + self.cached_info.last_failed = None + return + + if not self.cached_info.last_failed: + self.report_status.append("no previously failed tests, not skipping.") + return + + if last_test_count is not None and last_test_count != len(items): + self.report_status.append( + f"test count changed, not skipping (now {len(items)} tests, previously {last_test_count})." + ) + self.cached_info.last_failed = None + return + + # Check all item nodes until we find a match on last failed. + failed_index = None + for index, item in enumerate(items): + if item.nodeid == self.cached_info.last_failed: + failed_index = index + break + + # If the previously failed test was not found among the test items, + # do not skip any tests. + if failed_index is None: + self.report_status.append("previously failed test not found, not skipping.") + else: + cache_age = datetime.now() - self.cached_info.last_cache_date + # Round up to avoid showing microseconds. + cache_age = timedelta(seconds=int(cache_age.total_seconds())) + self.report_status.append( + f"skipping {failed_index} already passed items (cache from {cache_age} ago," + f" use --sw-reset to discard)." + ) + deselected = items[:failed_index] + del items[:failed_index] + config.hook.pytest_deselected(items=deselected) + + def pytest_runtest_logreport(self, report: TestReport) -> None: + if report.failed: + if self.skip: + # Remove test from the failed ones (if it exists) and unset the skip option + # to make sure the following tests will not be skipped. + if report.nodeid == self.cached_info.last_failed: + self.cached_info.last_failed = None + + self.skip = False + else: + # Mark test as the last failing and interrupt the test session. + self.cached_info.last_failed = report.nodeid + assert self.session is not None + self.session.shouldstop = ( + "Test failed, continuing from this test next run." + ) + + else: + # If the test was actually run and did pass. + if report.when == "call": + # Remove test from the failed ones, if exists. + if report.nodeid == self.cached_info.last_failed: + self.cached_info.last_failed = None + + def pytest_report_collectionfinish(self) -> list[str] | None: + if self.config.get_verbosity() >= 0 and self.report_status: + return [f"stepwise: {x}" for x in self.report_status] + return None + + def pytest_sessionfinish(self) -> None: + if hasattr(self.config, "workerinput"): + # Do not update cache if this process is a xdist worker to prevent + # race conditions (#10641). + return + self.cached_info.update_date_to_now() + self.cache.set(STEPWISE_CACHE_DIR, dataclasses.asdict(self.cached_info)) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/subtests.py b/Backend/venv/lib/python3.12/site-packages/_pytest/subtests.py new file mode 100644 index 00000000..e0ceb27f --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/subtests.py @@ -0,0 +1,411 @@ +"""Builtin plugin that adds subtests support.""" + +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Callable +from collections.abc import Iterator +from collections.abc import Mapping +from contextlib import AbstractContextManager +from contextlib import contextmanager +from contextlib import ExitStack +from contextlib import nullcontext +import dataclasses +import time +from types import TracebackType +from typing import Any +from typing import TYPE_CHECKING + +import pluggy + +from _pytest._code import ExceptionInfo +from _pytest._io.saferepr import saferepr +from _pytest.capture import CaptureFixture +from _pytest.capture import FDCapture +from _pytest.capture import SysCapture +from _pytest.config import Config +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import fixture +from _pytest.fixtures import SubRequest +from _pytest.logging import catching_logs +from _pytest.logging import LogCaptureHandler +from _pytest.logging import LoggingPlugin +from _pytest.reports import TestReport +from _pytest.runner import CallInfo +from _pytest.runner import check_interactive_exception +from _pytest.runner import get_reraise_exceptions +from _pytest.stash import StashKey + + +if TYPE_CHECKING: + from typing_extensions import Self + + +def pytest_addoption(parser: Parser) -> None: + Config._add_verbosity_ini( + parser, + Config.VERBOSITY_SUBTESTS, + help=( + "Specify verbosity level for subtests. " + "Higher levels will generate output for passed subtests. Failed subtests are always reported." + ), + ) + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class SubtestContext: + """The values passed to Subtests.test() that are included in the test report.""" + + msg: str | None + kwargs: Mapping[str, Any] + + def _to_json(self) -> dict[str, Any]: + return dataclasses.asdict(self) + + @classmethod + def _from_json(cls, d: dict[str, Any]) -> Self: + return cls(msg=d["msg"], kwargs=d["kwargs"]) + + +@dataclasses.dataclass(init=False) +class SubtestReport(TestReport): + context: SubtestContext + + @property + def head_line(self) -> str: + _, _, domain = self.location + return f"{domain} {self._sub_test_description()}" + + def _sub_test_description(self) -> str: + parts = [] + if self.context.msg is not None: + parts.append(f"[{self.context.msg}]") + if self.context.kwargs: + params_desc = ", ".join( + f"{k}={saferepr(v)}" for (k, v) in self.context.kwargs.items() + ) + parts.append(f"({params_desc})") + return " ".join(parts) or "()" + + def _to_json(self) -> dict[str, Any]: + data = super()._to_json() + del data["context"] + data["_report_type"] = "SubTestReport" + data["_subtest.context"] = self.context._to_json() + return data + + @classmethod + def _from_json(cls, reportdict: dict[str, Any]) -> SubtestReport: + report = super()._from_json(reportdict) + report.context = SubtestContext._from_json(reportdict["_subtest.context"]) + return report + + @classmethod + def _new( + cls, + test_report: TestReport, + context: SubtestContext, + captured_output: Captured | None, + captured_logs: CapturedLogs | None, + ) -> Self: + result = super()._from_json(test_report._to_json()) + result.context = context + + if captured_output: + if captured_output.out: + result.sections.append(("Captured stdout call", captured_output.out)) + if captured_output.err: + result.sections.append(("Captured stderr call", captured_output.err)) + + if captured_logs and (log := captured_logs.handler.stream.getvalue()): + result.sections.append(("Captured log call", log)) + + return result + + +@fixture +def subtests(request: SubRequest) -> Subtests: + """Provides subtests functionality.""" + capmam = request.node.config.pluginmanager.get_plugin("capturemanager") + suspend_capture_ctx = ( + capmam.global_and_fixture_disabled if capmam is not None else nullcontext + ) + return Subtests(request.node.ihook, suspend_capture_ctx, request, _ispytest=True) + + +class Subtests: + """Subtests fixture, enables declaring subtests inside test functions via the :meth:`test` method.""" + + def __init__( + self, + ihook: pluggy.HookRelay, + suspend_capture_ctx: Callable[[], AbstractContextManager[None]], + request: SubRequest, + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + self._ihook = ihook + self._suspend_capture_ctx = suspend_capture_ctx + self._request = request + + def test( + self, + msg: str | None = None, + **kwargs: Any, + ) -> _SubTestContextManager: + """ + Context manager for subtests, capturing exceptions raised inside the subtest scope and + reporting assertion failures and errors individually. + + Usage + ----- + + .. code-block:: python + + def test(subtests): + for i in range(5): + with subtests.test("custom message", i=i): + assert i % 2 == 0 + + :param msg: + If given, the message will be shown in the test report in case of subtest failure. + + :param kwargs: + Arbitrary values that are also added to the subtest report. + """ + return _SubTestContextManager( + self._ihook, + msg, + kwargs, + request=self._request, + suspend_capture_ctx=self._suspend_capture_ctx, + config=self._request.config, + ) + + +@dataclasses.dataclass +class _SubTestContextManager: + """ + Context manager for subtests, capturing exceptions raised inside the subtest scope and handling + them through the pytest machinery. + """ + + # Note: initially the logic for this context manager was implemented directly + # in Subtests.test() as a @contextmanager, however, it is not possible to control the output fully when + # exiting from it due to an exception when in `--exitfirst` mode, so this was refactored into an + # explicit context manager class (pytest-dev/pytest-subtests#134). + + ihook: pluggy.HookRelay + msg: str | None + kwargs: dict[str, Any] + suspend_capture_ctx: Callable[[], AbstractContextManager[None]] + request: SubRequest + config: Config + + def __enter__(self) -> None: + __tracebackhide__ = True + + self._start = time.time() + self._precise_start = time.perf_counter() + self._exc_info = None + + self._exit_stack = ExitStack() + self._captured_output = self._exit_stack.enter_context( + capturing_output(self.request) + ) + self._captured_logs = self._exit_stack.enter_context( + capturing_logs(self.request) + ) + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + __tracebackhide__ = True + if exc_val is not None: + exc_info = ExceptionInfo.from_exception(exc_val) + else: + exc_info = None + + self._exit_stack.close() + + precise_stop = time.perf_counter() + duration = precise_stop - self._precise_start + stop = time.time() + + call_info = CallInfo[None]( + None, + exc_info, + start=self._start, + stop=stop, + duration=duration, + when="call", + _ispytest=True, + ) + report = self.ihook.pytest_runtest_makereport( + item=self.request.node, call=call_info + ) + sub_report = SubtestReport._new( + report, + SubtestContext(msg=self.msg, kwargs=self.kwargs), + captured_output=self._captured_output, + captured_logs=self._captured_logs, + ) + + if sub_report.failed: + failed_subtests = self.config.stash[failed_subtests_key] + failed_subtests[self.request.node.nodeid] += 1 + + with self.suspend_capture_ctx(): + self.ihook.pytest_runtest_logreport(report=sub_report) + + if check_interactive_exception(call_info, sub_report): + self.ihook.pytest_exception_interact( + node=self.request.node, call=call_info, report=sub_report + ) + + if exc_val is not None: + if isinstance(exc_val, get_reraise_exceptions(self.config)): + return False + if self.request.session.shouldfail: + return False + return True + + +@contextmanager +def capturing_output(request: SubRequest) -> Iterator[Captured]: + option = request.config.getoption("capture", None) + + capman = request.config.pluginmanager.getplugin("capturemanager") + if getattr(capman, "_capture_fixture", None): + # capsys or capfd are active, subtest should not capture. + fixture = None + elif option == "sys": + fixture = CaptureFixture(SysCapture, request, _ispytest=True) + elif option == "fd": + fixture = CaptureFixture(FDCapture, request, _ispytest=True) + else: + fixture = None + + if fixture is not None: + fixture._start() + + captured = Captured() + try: + yield captured + finally: + if fixture is not None: + out, err = fixture.readouterr() + fixture.close() + captured.out = out + captured.err = err + + +@contextmanager +def capturing_logs( + request: SubRequest, +) -> Iterator[CapturedLogs | None]: + logging_plugin: LoggingPlugin | None = request.config.pluginmanager.getplugin( + "logging-plugin" + ) + if logging_plugin is None: + yield None + else: + handler = LogCaptureHandler() + handler.setFormatter(logging_plugin.formatter) + + captured_logs = CapturedLogs(handler) + with catching_logs(handler, level=logging_plugin.log_level): + yield captured_logs + + +@dataclasses.dataclass +class Captured: + out: str = "" + err: str = "" + + +@dataclasses.dataclass +class CapturedLogs: + handler: LogCaptureHandler + + +def pytest_report_to_serializable(report: TestReport) -> dict[str, Any] | None: + if isinstance(report, SubtestReport): + return report._to_json() + return None + + +def pytest_report_from_serializable(data: dict[str, Any]) -> SubtestReport | None: + if data.get("_report_type") == "SubTestReport": + return SubtestReport._from_json(data) + return None + + +# Dict of nodeid -> number of failed subtests. +# Used to fail top-level tests that passed but contain failed subtests. +failed_subtests_key = StashKey[defaultdict[str, int]]() + + +def pytest_configure(config: Config) -> None: + config.stash[failed_subtests_key] = defaultdict(lambda: 0) + + +@hookimpl(tryfirst=True) +def pytest_report_teststatus( + report: TestReport, + config: Config, +) -> tuple[str, str, str | Mapping[str, bool]] | None: + if report.when != "call": + return None + + quiet = config.get_verbosity(Config.VERBOSITY_SUBTESTS) == 0 + if isinstance(report, SubtestReport): + outcome = report.outcome + description = report._sub_test_description() + + if hasattr(report, "wasxfail"): + if quiet: + return "", "", "" + elif outcome == "skipped": + category = "xfailed" + short = "y" # x letter is used for regular xfail, y for subtest xfail + status = "SUBXFAIL" + # outcome == "passed" in an xfail is only possible via a @pytest.mark.xfail mark, which + # is not applicable to a subtest, which only handles pytest.xfail(). + else: # pragma: no cover + # This should not normally happen, unless some plugin is setting wasxfail without + # the correct outcome. Pytest expects the call outcome to be either skipped or + # passed in case of xfail. + # Let's pass this report to the next hook. + return None + return category, short, f"{status}{description}" + + if report.failed: + return outcome, "u", f"SUBFAILED{description}" + else: + if report.passed: + if quiet: + return "", "", "" + else: + return f"subtests {outcome}", "u", f"SUBPASSED{description}" + elif report.skipped: + if quiet: + return "", "", "" + else: + return outcome, "-", f"SUBSKIPPED{description}" + + else: + failed_subtests_count = config.stash[failed_subtests_key][report.nodeid] + # Top-level test, fail if it contains failed subtests and it has passed. + if report.passed and failed_subtests_count > 0: + report.outcome = "failed" + suffix = "s" if failed_subtests_count > 1 else "" + report.longrepr = f"contains {failed_subtests_count} failed subtest{suffix}" + + return None diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/terminal.py b/Backend/venv/lib/python3.12/site-packages/_pytest/terminal.py new file mode 100644 index 00000000..4517b05b --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/terminal.py @@ -0,0 +1,1770 @@ +# mypy: allow-untyped-defs +"""Terminal reporting of the full testing process. + +This is a good source for looking at the various reporting hooks. +""" + +from __future__ import annotations + +import argparse +from collections import Counter +from collections.abc import Callable +from collections.abc import Generator +from collections.abc import Mapping +from collections.abc import Sequence +import dataclasses +import datetime +from functools import partial +import inspect +import os +from pathlib import Path +import platform +import sys +import textwrap +from typing import Any +from typing import ClassVar +from typing import final +from typing import Literal +from typing import NamedTuple +from typing import TextIO +from typing import TYPE_CHECKING +import warnings + +import pluggy + +from _pytest import compat +from _pytest import nodes +from _pytest import timing +from _pytest._code import ExceptionInfo +from _pytest._code.code import ExceptionRepr +from _pytest._io import TerminalWriter +from _pytest._io.wcwidth import wcswidth +import _pytest._version +from _pytest.compat import running_on_ci +from _pytest.config import _PluggyPlugin +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.nodes import Item +from _pytest.nodes import Node +from _pytest.pathlib import absolutepath +from _pytest.pathlib import bestrelpath +from _pytest.reports import BaseReport +from _pytest.reports import CollectReport +from _pytest.reports import TestReport + + +if TYPE_CHECKING: + from _pytest.main import Session + + +REPORT_COLLECTING_RESOLUTION = 0.5 + +KNOWN_TYPES = ( + "failed", + "passed", + "skipped", + "deselected", + "xfailed", + "xpassed", + "warnings", + "error", + "subtests passed", + "subtests failed", + "subtests skipped", +) + +_REPORTCHARS_DEFAULT = "fE" + + +class MoreQuietAction(argparse.Action): + """A modified copy of the argparse count action which counts down and updates + the legacy quiet attribute at the same time. + + Used to unify verbosity handling. + """ + + def __init__( + self, + option_strings: Sequence[str], + dest: str, + default: object = None, + required: bool = False, + help: str | None = None, + ) -> None: + super().__init__( + option_strings=option_strings, + dest=dest, + nargs=0, + default=default, + required=required, + help=help, + ) + + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: str | Sequence[object] | None, + option_string: str | None = None, + ) -> None: + new_count = getattr(namespace, self.dest, 0) - 1 + setattr(namespace, self.dest, new_count) + # todo Deprecate config.quiet + namespace.quiet = getattr(namespace, "quiet", 0) + 1 + + +class TestShortLogReport(NamedTuple): + """Used to store the test status result category, shortletter and verbose word. + For example ``"rerun", "R", ("RERUN", {"yellow": True})``. + + :ivar category: + The class of result, for example ``“passed”``, ``“skipped”``, ``“error”``, or the empty string. + + :ivar letter: + The short letter shown as testing progresses, for example ``"."``, ``"s"``, ``"E"``, or the empty string. + + :ivar word: + Verbose word is shown as testing progresses in verbose mode, for example ``"PASSED"``, ``"SKIPPED"``, + ``"ERROR"``, or the empty string. + """ + + category: str + letter: str + word: str | tuple[str, Mapping[str, bool]] + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("terminal reporting", "Reporting", after="general") + group._addoption( # private to use reserved lower-case short option + "-v", + "--verbose", + action="count", + default=0, + dest="verbose", + help="Increase verbosity", + ) + group.addoption( + "--no-header", + action="store_true", + default=False, + dest="no_header", + help="Disable header", + ) + group.addoption( + "--no-summary", + action="store_true", + default=False, + dest="no_summary", + help="Disable summary", + ) + group.addoption( + "--no-fold-skipped", + action="store_false", + dest="fold_skipped", + default=True, + help="Do not fold skipped tests in short summary.", + ) + group.addoption( + "--force-short-summary", + action="store_true", + dest="force_short_summary", + default=False, + help="Force condensed summary output regardless of verbosity level.", + ) + group._addoption( # private to use reserved lower-case short option + "-q", + "--quiet", + action=MoreQuietAction, + default=0, + dest="verbose", + help="Decrease verbosity", + ) + group.addoption( + "--verbosity", + dest="verbose", + type=int, + default=0, + help="Set verbosity. Default: 0.", + ) + group._addoption( # private to use reserved lower-case short option + "-r", + action="store", + dest="reportchars", + default=_REPORTCHARS_DEFAULT, + metavar="chars", + help="Show extra test summary info as specified by chars: (f)ailed, " + "(E)rror, (s)kipped, (x)failed, (X)passed, " + "(p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. " + "(w)arnings are enabled by default (see --disable-warnings), " + "'N' can be used to reset the list. (default: 'fE').", + ) + group.addoption( + "--disable-warnings", + "--disable-pytest-warnings", + default=False, + dest="disable_warnings", + action="store_true", + help="Disable warnings summary", + ) + group._addoption( # private to use reserved lower-case short option + "-l", + "--showlocals", + action="store_true", + dest="showlocals", + default=False, + help="Show locals in tracebacks (disabled by default)", + ) + group.addoption( + "--no-showlocals", + action="store_false", + dest="showlocals", + help="Hide locals in tracebacks (negate --showlocals passed through addopts)", + ) + group.addoption( + "--tb", + metavar="style", + action="store", + dest="tbstyle", + default="auto", + choices=["auto", "long", "short", "no", "line", "native"], + help="Traceback print mode (auto/long/short/line/native/no)", + ) + group.addoption( + "--xfail-tb", + action="store_true", + dest="xfail_tb", + default=False, + help="Show tracebacks for xfail (as long as --tb != no)", + ) + group.addoption( + "--show-capture", + action="store", + dest="showcapture", + choices=["no", "stdout", "stderr", "log", "all"], + default="all", + help="Controls how captured stdout/stderr/log is shown on failed tests. " + "Default: all.", + ) + group.addoption( + "--fulltrace", + "--full-trace", + action="store_true", + default=False, + help="Don't cut any tracebacks (default is to cut)", + ) + group.addoption( + "--color", + metavar="color", + action="store", + dest="color", + default="auto", + choices=["yes", "no", "auto"], + help="Color terminal output (yes/no/auto)", + ) + group.addoption( + "--code-highlight", + default="yes", + choices=["yes", "no"], + help="Whether code should be highlighted (only if --color is also enabled). " + "Default: yes.", + ) + + parser.addini( + "console_output_style", + help='Console output: "classic", or with additional progress information ' + '("progress" (percentage) | "count" | "progress-even-when-capture-no" (forces ' + "progress even when capture=no)", + default="progress", + ) + Config._add_verbosity_ini( + parser, + Config.VERBOSITY_TEST_CASES, + help=( + "Specify a verbosity level for test case execution, overriding the main level. " + "Higher levels will provide more detailed information about each test case executed." + ), + ) + + +def pytest_configure(config: Config) -> None: + reporter = TerminalReporter(config, sys.stdout) + config.pluginmanager.register(reporter, "terminalreporter") + if config.option.debug or config.option.traceconfig: + + def mywriter(tags, args): + msg = " ".join(map(str, args)) + reporter.write_line("[traceconfig] " + msg) + + config.trace.root.setprocessor("pytest:config", mywriter) + + if reporter.isatty(): + # Some terminals interpret OSC 9;4 as desktop notification, + # skip on those we know (#13896). + should_skip_terminal_progress = ( + # iTerm2 (reported on version 3.6.5). + "ITERM_SESSION_ID" in os.environ + ) + if not should_skip_terminal_progress: + plugin = TerminalProgressPlugin(reporter) + config.pluginmanager.register(plugin, "terminalprogress") + + +def getreportopt(config: Config) -> str: + reportchars: str = config.option.reportchars + + old_aliases = {"F", "S"} + reportopts = "" + for char in reportchars: + if char in old_aliases: + char = char.lower() + if char == "a": + reportopts = "sxXEf" + elif char == "A": + reportopts = "PpsxXEf" + elif char == "N": + reportopts = "" + elif char not in reportopts: + reportopts += char + + if not config.option.disable_warnings and "w" not in reportopts: + reportopts = "w" + reportopts + elif config.option.disable_warnings and "w" in reportopts: + reportopts = reportopts.replace("w", "") + + return reportopts + + +@hookimpl(trylast=True) # after _pytest.runner +def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str]: + letter = "F" + if report.passed: + letter = "." + elif report.skipped: + letter = "s" + + outcome: str = report.outcome + if report.when in ("collect", "setup", "teardown") and outcome == "failed": + outcome = "error" + letter = "E" + + return outcome, letter, outcome.upper() + + +@dataclasses.dataclass +class WarningReport: + """Simple structure to hold warnings information captured by ``pytest_warning_recorded``. + + :ivar str message: + User friendly message about the warning. + :ivar str|None nodeid: + nodeid that generated the warning (see ``get_location``). + :ivar tuple fslocation: + File system location of the source of the warning (see ``get_location``). + """ + + message: str + nodeid: str | None = None + fslocation: tuple[str, int] | None = None + + count_towards_summary: ClassVar = True + + def get_location(self, config: Config) -> str | None: + """Return the more user-friendly information about the location of a warning, or None.""" + if self.nodeid: + return self.nodeid + if self.fslocation: + filename, linenum = self.fslocation + relpath = bestrelpath(config.invocation_params.dir, absolutepath(filename)) + return f"{relpath}:{linenum}" + return None + + +@final +class TerminalReporter: + def __init__(self, config: Config, file: TextIO | None = None) -> None: + import _pytest.config + + self.config = config + self._numcollected = 0 + self._session: Session | None = None + self._showfspath: bool | None = None + + self.stats: dict[str, list[Any]] = {} + self._main_color: str | None = None + self._known_types: list[str] | None = None + self.startpath = config.invocation_params.dir + if file is None: + file = sys.stdout + self._tw = _pytest.config.create_terminal_writer(config, file) + self._screen_width = self._tw.fullwidth + self.currentfspath: None | Path | str | int = None + self.reportchars = getreportopt(config) + self.foldskipped = config.option.fold_skipped + self.hasmarkup = self._tw.hasmarkup + # isatty should be a method but was wrongly implemented as a boolean. + # We use CallableBool here to support both. + self.isatty = compat.CallableBool(file.isatty()) + self._progress_nodeids_reported: set[str] = set() + self._timing_nodeids_reported: set[str] = set() + self._show_progress_info = self._determine_show_progress_info() + self._collect_report_last_write = timing.Instant() + self._already_displayed_warnings: int | None = None + self._keyboardinterrupt_memo: ExceptionRepr | None = None + + def _determine_show_progress_info( + self, + ) -> Literal["progress", "count", "times", False]: + """Return whether we should display progress information based on the current config.""" + # do not show progress if we are not capturing output (#3038) unless explicitly + # overridden by progress-even-when-capture-no + if ( + self.config.getoption("capture", "no") == "no" + and self.config.getini("console_output_style") + != "progress-even-when-capture-no" + ): + return False + # do not show progress if we are showing fixture setup/teardown + if self.config.getoption("setupshow", False): + return False + cfg: str = self.config.getini("console_output_style") + if cfg in {"progress", "progress-even-when-capture-no"}: + return "progress" + elif cfg == "count": + return "count" + elif cfg == "times": + return "times" + else: + return False + + @property + def verbosity(self) -> int: + verbosity: int = self.config.option.verbose + return verbosity + + @property + def showheader(self) -> bool: + return self.verbosity >= 0 + + @property + def no_header(self) -> bool: + return bool(self.config.option.no_header) + + @property + def no_summary(self) -> bool: + return bool(self.config.option.no_summary) + + @property + def showfspath(self) -> bool: + if self._showfspath is None: + return self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) >= 0 + return self._showfspath + + @showfspath.setter + def showfspath(self, value: bool | None) -> None: + self._showfspath = value + + @property + def showlongtestinfo(self) -> bool: + return self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) > 0 + + @property + def reported_progress(self) -> int: + """The amount of items reported in the progress so far. + + :meta private: + """ + return len(self._progress_nodeids_reported) + + def hasopt(self, char: str) -> bool: + char = {"xfailed": "x", "skipped": "s"}.get(char, char) + return char in self.reportchars + + def write_fspath_result(self, nodeid: str, res: str, **markup: bool) -> None: + fspath = self.config.rootpath / nodeid.split("::")[0] + if self.currentfspath is None or fspath != self.currentfspath: + if self.currentfspath is not None and self._show_progress_info: + self._write_progress_information_filling_space() + self.currentfspath = fspath + relfspath = bestrelpath(self.startpath, fspath) + self._tw.line() + self._tw.write(relfspath + " ") + self._tw.write(res, flush=True, **markup) + + def write_ensure_prefix(self, prefix: str, extra: str = "", **kwargs) -> None: + if self.currentfspath != prefix: + self._tw.line() + self.currentfspath = prefix + self._tw.write(prefix) + if extra: + self._tw.write(extra, **kwargs) + self.currentfspath = -2 + + def ensure_newline(self) -> None: + if self.currentfspath: + self._tw.line() + self.currentfspath = None + + def wrap_write( + self, + content: str, + *, + flush: bool = False, + margin: int = 8, + line_sep: str = "\n", + **markup: bool, + ) -> None: + """Wrap message with margin for progress info.""" + width_of_current_line = self._tw.width_of_current_line + wrapped = line_sep.join( + textwrap.wrap( + " " * width_of_current_line + content, + width=self._screen_width - margin, + drop_whitespace=True, + replace_whitespace=False, + ), + ) + wrapped = wrapped[width_of_current_line:] + self._tw.write(wrapped, flush=flush, **markup) + + def write(self, content: str, *, flush: bool = False, **markup: bool) -> None: + self._tw.write(content, flush=flush, **markup) + + def write_raw(self, content: str, *, flush: bool = False) -> None: + self._tw.write_raw(content, flush=flush) + + def flush(self) -> None: + self._tw.flush() + + def write_line(self, line: str | bytes, **markup: bool) -> None: + if not isinstance(line, str): + line = str(line, errors="replace") + self.ensure_newline() + self._tw.line(line, **markup) + + def rewrite(self, line: str, **markup: bool) -> None: + """Rewinds the terminal cursor to the beginning and writes the given line. + + :param erase: + If True, will also add spaces until the full terminal width to ensure + previous lines are properly erased. + + The rest of the keyword arguments are markup instructions. + """ + erase = markup.pop("erase", False) + if erase: + fill_count = self._tw.fullwidth - len(line) - 1 + fill = " " * fill_count + else: + fill = "" + line = str(line) + self._tw.write("\r" + line + fill, **markup) + + def write_sep( + self, + sep: str, + title: str | None = None, + fullwidth: int | None = None, + **markup: bool, + ) -> None: + self.ensure_newline() + self._tw.sep(sep, title, fullwidth, **markup) + + def section(self, title: str, sep: str = "=", **kw: bool) -> None: + self._tw.sep(sep, title, **kw) + + def line(self, msg: str, **kw: bool) -> None: + self._tw.line(msg, **kw) + + def _add_stats(self, category: str, items: Sequence[Any]) -> None: + set_main_color = category not in self.stats + self.stats.setdefault(category, []).extend(items) + if set_main_color: + self._set_main_color() + + def pytest_internalerror(self, excrepr: ExceptionRepr) -> bool: + for line in str(excrepr).split("\n"): + self.write_line("INTERNALERROR> " + line) + return True + + def pytest_warning_recorded( + self, + warning_message: warnings.WarningMessage, + nodeid: str, + ) -> None: + from _pytest.warnings import warning_record_to_str + + fslocation = warning_message.filename, warning_message.lineno + message = warning_record_to_str(warning_message) + + warning_report = WarningReport( + fslocation=fslocation, message=message, nodeid=nodeid + ) + self._add_stats("warnings", [warning_report]) + + def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: + if self.config.option.traceconfig: + msg = f"PLUGIN registered: {plugin}" + # XXX This event may happen during setup/teardown time + # which unfortunately captures our output here + # which garbles our output if we use self.write_line. + self.write_line(msg) + + def pytest_deselected(self, items: Sequence[Item]) -> None: + self._add_stats("deselected", items) + + def pytest_runtest_logstart( + self, nodeid: str, location: tuple[str, int | None, str] + ) -> None: + fspath, lineno, domain = location + # Ensure that the path is printed before the + # 1st test of a module starts running. + if self.showlongtestinfo: + line = self._locationline(nodeid, fspath, lineno, domain) + self.write_ensure_prefix(line, "") + self.flush() + elif self.showfspath: + self.write_fspath_result(nodeid, "") + self.flush() + + def pytest_runtest_logreport(self, report: TestReport) -> None: + self._tests_ran = True + rep = report + + res = TestShortLogReport( + *self.config.hook.pytest_report_teststatus(report=rep, config=self.config) + ) + category, letter, word = res.category, res.letter, res.word + if not isinstance(word, tuple): + markup = None + else: + word, markup = word + self._add_stats(category, [rep]) + if not letter and not word: + # Probably passed setup/teardown. + return + if markup is None: + was_xfail = hasattr(report, "wasxfail") + if rep.passed and not was_xfail: + markup = {"green": True} + elif rep.passed and was_xfail: + markup = {"yellow": True} + elif rep.failed: + markup = {"red": True} + elif rep.skipped: + markup = {"yellow": True} + else: + markup = {} + self._progress_nodeids_reported.add(rep.nodeid) + if self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0: + self._tw.write(letter, **markup) + # When running in xdist, the logreport and logfinish of multiple + # items are interspersed, e.g. `logreport`, `logreport`, + # `logfinish`, `logfinish`. To avoid the "past edge" calculation + # from getting confused and overflowing (#7166), do the past edge + # printing here and not in logfinish, except for the 100% which + # should only be printed after all teardowns are finished. + if self._show_progress_info and not self._is_last_item: + self._write_progress_information_if_past_edge() + else: + line = self._locationline(rep.nodeid, *rep.location) + running_xdist = hasattr(rep, "node") + if not running_xdist: + self.write_ensure_prefix(line, word, **markup) + if rep.skipped or hasattr(report, "wasxfail"): + reason = _get_raw_skip_reason(rep) + if self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) < 2: + available_width = ( + (self._tw.fullwidth - self._tw.width_of_current_line) + - len(" [100%]") + - 1 + ) + formatted_reason = _format_trimmed( + " ({})", reason, available_width + ) + else: + formatted_reason = f" ({reason})" + + if reason and formatted_reason is not None: + self.wrap_write(formatted_reason) + if self._show_progress_info: + self._write_progress_information_filling_space() + else: + self.ensure_newline() + self._tw.write(f"[{rep.node.gateway.id}]") + if self._show_progress_info: + self._tw.write( + self._get_progress_information_message() + " ", cyan=True + ) + else: + self._tw.write(" ") + self._tw.write(word, **markup) + self._tw.write(" " + line) + self.currentfspath = -2 + self.flush() + + @property + def _is_last_item(self) -> bool: + assert self._session is not None + return self.reported_progress == self._session.testscollected + + @hookimpl(wrapper=True) + def pytest_runtestloop(self) -> Generator[None, object, object]: + result = yield + + # Write the final/100% progress -- deferred until the loop is complete. + if ( + self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0 + and self._show_progress_info + and self.reported_progress + ): + self._write_progress_information_filling_space() + + return result + + def _get_progress_information_message(self) -> str: + assert self._session + collected = self._session.testscollected + if self._show_progress_info == "count": + if collected: + progress = self.reported_progress + counter_format = f"{{:{len(str(collected))}d}}" + format_string = f" [{counter_format}/{{}}]" + return format_string.format(progress, collected) + return f" [ {collected} / {collected} ]" + if self._show_progress_info == "times": + if not collected: + return "" + all_reports = ( + self._get_reports_to_display("passed") + + self._get_reports_to_display("xpassed") + + self._get_reports_to_display("failed") + + self._get_reports_to_display("xfailed") + + self._get_reports_to_display("skipped") + + self._get_reports_to_display("error") + + self._get_reports_to_display("") + ) + current_location = all_reports[-1].location[0] + not_reported = [ + r for r in all_reports if r.nodeid not in self._timing_nodeids_reported + ] + tests_in_module = sum( + i.location[0] == current_location for i in self._session.items + ) + tests_completed = sum( + r.when == "setup" + for r in not_reported + if r.location[0] == current_location + ) + last_in_module = tests_completed == tests_in_module + if self.showlongtestinfo or last_in_module: + self._timing_nodeids_reported.update(r.nodeid for r in not_reported) + return format_node_duration( + sum(r.duration for r in not_reported if isinstance(r, TestReport)) + ) + return "" + if collected: + return f" [{self.reported_progress * 100 // collected:3d}%]" + return " [100%]" + + def _write_progress_information_if_past_edge(self) -> None: + w = self._width_of_current_line + if self._show_progress_info == "count": + assert self._session + num_tests = self._session.testscollected + progress_length = len(f" [{num_tests}/{num_tests}]") + elif self._show_progress_info == "times": + progress_length = len(" 99h 59m") + else: + progress_length = len(" [100%]") + past_edge = w + progress_length + 1 >= self._screen_width + if past_edge: + main_color, _ = self._get_main_color() + msg = self._get_progress_information_message() + self._tw.write(msg + "\n", **{main_color: True}) + + def _write_progress_information_filling_space(self) -> None: + color, _ = self._get_main_color() + msg = self._get_progress_information_message() + w = self._width_of_current_line + fill = self._tw.fullwidth - w - 1 + self.write(msg.rjust(fill), flush=True, **{color: True}) + + @property + def _width_of_current_line(self) -> int: + """Return the width of the current line.""" + return self._tw.width_of_current_line + + def pytest_collection(self) -> None: + if self.isatty(): + if self.config.option.verbose >= 0: + self.write("collecting ... ", flush=True, bold=True) + elif self.config.option.verbose >= 1: + self.write("collecting ... ", flush=True, bold=True) + + def pytest_collectreport(self, report: CollectReport) -> None: + if report.failed: + self._add_stats("error", [report]) + elif report.skipped: + self._add_stats("skipped", [report]) + items = [x for x in report.result if isinstance(x, Item)] + self._numcollected += len(items) + if self.isatty(): + self.report_collect() + + def report_collect(self, final: bool = False) -> None: + if self.config.option.verbose < 0: + return + + if not final: + # Only write the "collecting" report every `REPORT_COLLECTING_RESOLUTION`. + if ( + self._collect_report_last_write.elapsed().seconds + < REPORT_COLLECTING_RESOLUTION + ): + return + self._collect_report_last_write = timing.Instant() + + errors = len(self.stats.get("error", [])) + skipped = len(self.stats.get("skipped", [])) + deselected = len(self.stats.get("deselected", [])) + selected = self._numcollected - deselected + line = "collected " if final else "collecting " + line += ( + str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s") + ) + if errors: + line += f" / {errors} error{'s' if errors != 1 else ''}" + if deselected: + line += f" / {deselected} deselected" + if skipped: + line += f" / {skipped} skipped" + if self._numcollected > selected: + line += f" / {selected} selected" + if self.isatty(): + self.rewrite(line, bold=True, erase=True) + if final: + self.write("\n") + else: + self.write_line(line) + + @hookimpl(trylast=True) + def pytest_sessionstart(self, session: Session) -> None: + self._session = session + self._session_start = timing.Instant() + if not self.showheader: + return + self.write_sep("=", "test session starts", bold=True) + verinfo = platform.python_version() + if not self.no_header: + msg = f"platform {sys.platform} -- Python {verinfo}" + pypy_version_info = getattr(sys, "pypy_version_info", None) + if pypy_version_info: + verinfo = ".".join(map(str, pypy_version_info[:3])) + msg += f"[pypy-{verinfo}-{pypy_version_info[3]}]" + msg += f", pytest-{_pytest._version.version}, pluggy-{pluggy.__version__}" + if ( + self.verbosity > 0 + or self.config.option.debug + or getattr(self.config.option, "pastebin", None) + ): + msg += " -- " + str(sys.executable) + self.write_line(msg) + lines = self.config.hook.pytest_report_header( + config=self.config, start_path=self.startpath + ) + self._write_report_lines_from_hooks(lines) + + def _write_report_lines_from_hooks( + self, lines: Sequence[str | Sequence[str]] + ) -> None: + for line_or_lines in reversed(lines): + if isinstance(line_or_lines, str): + self.write_line(line_or_lines) + else: + for line in line_or_lines: + self.write_line(line) + + def pytest_report_header(self, config: Config) -> list[str]: + result = [f"rootdir: {config.rootpath}"] + + if config.inipath: + warning = "" + if config._ignored_config_files: + warning = f" (WARNING: ignoring pytest config in {', '.join(config._ignored_config_files)}!)" + result.append( + "configfile: " + bestrelpath(config.rootpath, config.inipath) + warning + ) + + if config.args_source == Config.ArgsSource.TESTPATHS: + testpaths: list[str] = config.getini("testpaths") + result.append("testpaths: {}".format(", ".join(testpaths))) + + plugininfo = config.pluginmanager.list_plugin_distinfo() + if plugininfo: + result.append( + "plugins: {}".format(", ".join(_plugin_nameversions(plugininfo))) + ) + return result + + def pytest_collection_finish(self, session: Session) -> None: + self.report_collect(True) + + lines = self.config.hook.pytest_report_collectionfinish( + config=self.config, + start_path=self.startpath, + items=session.items, + ) + self._write_report_lines_from_hooks(lines) + + if self.config.getoption("collectonly"): + if session.items: + if self.config.option.verbose > -1: + self._tw.line("") + self._printcollecteditems(session.items) + + failed = self.stats.get("failed") + if failed: + self._tw.sep("!", "collection failures") + for rep in failed: + rep.toterminal(self._tw) + + def _printcollecteditems(self, items: Sequence[Item]) -> None: + test_cases_verbosity = self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) + if test_cases_verbosity < 0: + if test_cases_verbosity < -1: + counts = Counter(item.nodeid.split("::", 1)[0] for item in items) + for name, count in sorted(counts.items()): + self._tw.line(f"{name}: {count}") + else: + for item in items: + self._tw.line(item.nodeid) + return + stack: list[Node] = [] + indent = "" + for item in items: + needed_collectors = item.listchain()[1:] # strip root node + while stack: + if stack == needed_collectors[: len(stack)]: + break + stack.pop() + for col in needed_collectors[len(stack) :]: + stack.append(col) + indent = (len(stack) - 1) * " " + self._tw.line(f"{indent}{col}") + if test_cases_verbosity >= 1: + obj = getattr(col, "obj", None) + doc = inspect.getdoc(obj) if obj else None + if doc: + for line in doc.splitlines(): + self._tw.line("{}{}".format(indent + " ", line)) + + @hookimpl(wrapper=True) + def pytest_sessionfinish( + self, session: Session, exitstatus: int | ExitCode + ) -> Generator[None]: + result = yield + self._tw.line("") + summary_exit_codes = ( + ExitCode.OK, + ExitCode.TESTS_FAILED, + ExitCode.INTERRUPTED, + ExitCode.USAGE_ERROR, + ExitCode.NO_TESTS_COLLECTED, + ) + if exitstatus in summary_exit_codes and not self.no_summary: + self.config.hook.pytest_terminal_summary( + terminalreporter=self, exitstatus=exitstatus, config=self.config + ) + if session.shouldfail: + self.write_sep("!", str(session.shouldfail), red=True) + if exitstatus == ExitCode.INTERRUPTED: + self._report_keyboardinterrupt() + self._keyboardinterrupt_memo = None + elif session.shouldstop: + self.write_sep("!", str(session.shouldstop), red=True) + self.summary_stats() + return result + + @hookimpl(wrapper=True) + def pytest_terminal_summary(self) -> Generator[None]: + self.summary_errors() + self.summary_failures() + self.summary_xfailures() + self.summary_warnings() + self.summary_passes() + self.summary_xpasses() + try: + return (yield) + finally: + self.short_test_summary() + # Display any extra warnings from teardown here (if any). + self.summary_warnings() + + def pytest_keyboard_interrupt(self, excinfo: ExceptionInfo[BaseException]) -> None: + self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) + + def pytest_unconfigure(self) -> None: + if self._keyboardinterrupt_memo is not None: + self._report_keyboardinterrupt() + + def _report_keyboardinterrupt(self) -> None: + excrepr = self._keyboardinterrupt_memo + assert excrepr is not None + assert excrepr.reprcrash is not None + msg = excrepr.reprcrash.message + self.write_sep("!", msg) + if "KeyboardInterrupt" in msg: + if self.config.option.fulltrace: + excrepr.toterminal(self._tw) + else: + excrepr.reprcrash.toterminal(self._tw) + self._tw.line( + "(to show a full traceback on KeyboardInterrupt use --full-trace)", + yellow=True, + ) + + def _locationline( + self, nodeid: str, fspath: str, lineno: int | None, domain: str + ) -> str: + def mkrel(nodeid: str) -> str: + line = self.config.cwd_relative_nodeid(nodeid) + if domain and line.endswith(domain): + line = line[: -len(domain)] + values = domain.split("[") + values[0] = values[0].replace(".", "::") # don't replace '.' in params + line += "[".join(values) + return line + + # fspath comes from testid which has a "/"-normalized path. + if fspath: + res = mkrel(nodeid) + if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace( + "\\", nodes.SEP + ): + res += " <- " + bestrelpath(self.startpath, Path(fspath)) + else: + res = "[location]" + return res + " " + + def _getfailureheadline(self, rep): + head_line = rep.head_line + if head_line: + return head_line + return "test session" # XXX? + + def _getcrashline(self, rep): + try: + return str(rep.longrepr.reprcrash) + except AttributeError: + try: + return str(rep.longrepr)[:50] + except AttributeError: + return "" + + # + # Summaries for sessionfinish. + # + def getreports(self, name: str): + return [x for x in self.stats.get(name, ()) if not hasattr(x, "_pdbshown")] + + def summary_warnings(self) -> None: + if self.hasopt("w"): + all_warnings: list[WarningReport] | None = self.stats.get("warnings") + if not all_warnings: + return + + final = self._already_displayed_warnings is not None + if final: + warning_reports = all_warnings[self._already_displayed_warnings :] + else: + warning_reports = all_warnings + self._already_displayed_warnings = len(warning_reports) + if not warning_reports: + return + + reports_grouped_by_message: dict[str, list[WarningReport]] = {} + for wr in warning_reports: + reports_grouped_by_message.setdefault(wr.message, []).append(wr) + + def collapsed_location_report(reports: list[WarningReport]) -> str: + locations = [] + for w in reports: + location = w.get_location(self.config) + if location: + locations.append(location) + + if len(locations) < 10: + return "\n".join(map(str, locations)) + + counts_by_filename = Counter( + str(loc).split("::", 1)[0] for loc in locations + ) + return "\n".join( + "{}: {} warning{}".format(k, v, "s" if v > 1 else "") + for k, v in counts_by_filename.items() + ) + + title = "warnings summary (final)" if final else "warnings summary" + self.write_sep("=", title, yellow=True, bold=False) + for message, message_reports in reports_grouped_by_message.items(): + maybe_location = collapsed_location_report(message_reports) + if maybe_location: + self._tw.line(maybe_location) + lines = message.splitlines() + indented = "\n".join(" " + x for x in lines) + message = indented.rstrip() + else: + message = message.rstrip() + self._tw.line(message) + self._tw.line() + self._tw.line( + "-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html" + ) + + def summary_passes(self) -> None: + self.summary_passes_combined("passed", "PASSES", "P") + + def summary_xpasses(self) -> None: + self.summary_passes_combined("xpassed", "XPASSES", "X") + + def summary_passes_combined( + self, which_reports: str, sep_title: str, needed_opt: str + ) -> None: + if self.config.option.tbstyle != "no": + if self.hasopt(needed_opt): + reports: list[TestReport] = self.getreports(which_reports) + if not reports: + return + self.write_sep("=", sep_title) + for rep in reports: + if rep.sections: + msg = self._getfailureheadline(rep) + self.write_sep("_", msg, green=True, bold=True) + self._outrep_summary(rep) + self._handle_teardown_sections(rep.nodeid) + + def _get_teardown_reports(self, nodeid: str) -> list[TestReport]: + reports = self.getreports("") + return [ + report + for report in reports + if report.when == "teardown" and report.nodeid == nodeid + ] + + def _handle_teardown_sections(self, nodeid: str) -> None: + for report in self._get_teardown_reports(nodeid): + self.print_teardown_sections(report) + + def print_teardown_sections(self, rep: TestReport) -> None: + showcapture = self.config.option.showcapture + if showcapture == "no": + return + for secname, content in rep.sections: + if showcapture != "all" and showcapture not in secname: + continue + if "teardown" in secname: + self._tw.sep("-", secname) + if content[-1:] == "\n": + content = content[:-1] + self._tw.line(content) + + def summary_failures(self) -> None: + style = self.config.option.tbstyle + self.summary_failures_combined("failed", "FAILURES", style=style) + + def summary_xfailures(self) -> None: + show_tb = self.config.option.xfail_tb + style = self.config.option.tbstyle if show_tb else "no" + self.summary_failures_combined("xfailed", "XFAILURES", style=style) + + def summary_failures_combined( + self, + which_reports: str, + sep_title: str, + *, + style: str, + needed_opt: str | None = None, + ) -> None: + if style != "no": + if not needed_opt or self.hasopt(needed_opt): + reports: list[BaseReport] = self.getreports(which_reports) + if not reports: + return + self.write_sep("=", sep_title) + if style == "line": + for rep in reports: + line = self._getcrashline(rep) + self._outrep_summary(rep) + self.write_line(line) + else: + for rep in reports: + msg = self._getfailureheadline(rep) + self.write_sep("_", msg, red=True, bold=True) + self._outrep_summary(rep) + self._handle_teardown_sections(rep.nodeid) + + def summary_errors(self) -> None: + if self.config.option.tbstyle != "no": + reports: list[BaseReport] = self.getreports("error") + if not reports: + return + self.write_sep("=", "ERRORS") + for rep in self.stats["error"]: + msg = self._getfailureheadline(rep) + if rep.when == "collect": + msg = "ERROR collecting " + msg + else: + msg = f"ERROR at {rep.when} of {msg}" + self.write_sep("_", msg, red=True, bold=True) + self._outrep_summary(rep) + + def _outrep_summary(self, rep: BaseReport) -> None: + rep.toterminal(self._tw) + showcapture = self.config.option.showcapture + if showcapture == "no": + return + for secname, content in rep.sections: + if showcapture != "all" and showcapture not in secname: + continue + self._tw.sep("-", secname) + if content[-1:] == "\n": + content = content[:-1] + self._tw.line(content) + + def summary_stats(self) -> None: + if self.verbosity < -1: + return + + session_duration = self._session_start.elapsed() + (parts, main_color) = self.build_summary_stats_line() + line_parts = [] + + display_sep = self.verbosity >= 0 + if display_sep: + fullwidth = self._tw.fullwidth + for text, markup in parts: + with_markup = self._tw.markup(text, **markup) + if display_sep: + fullwidth += len(with_markup) - len(text) + line_parts.append(with_markup) + msg = ", ".join(line_parts) + + main_markup = {main_color: True} + duration = f" in {format_session_duration(session_duration.seconds)}" + duration_with_markup = self._tw.markup(duration, **main_markup) + if display_sep: + fullwidth += len(duration_with_markup) - len(duration) + msg += duration_with_markup + + if display_sep: + markup_for_end_sep = self._tw.markup("", **main_markup) + if markup_for_end_sep.endswith("\x1b[0m"): + markup_for_end_sep = markup_for_end_sep[:-4] + fullwidth += len(markup_for_end_sep) + msg += markup_for_end_sep + + if display_sep: + self.write_sep("=", msg, fullwidth=fullwidth, **main_markup) + else: + self.write_line(msg, **main_markup) + + def short_test_summary(self) -> None: + if not self.reportchars: + return + + def show_simple(lines: list[str], *, stat: str) -> None: + failed = self.stats.get(stat, []) + if not failed: + return + config = self.config + for rep in failed: + color = _color_for_type.get(stat, _color_for_type_default) + line = _get_line_with_reprcrash_message( + config, rep, self._tw, {color: True} + ) + lines.append(line) + + def show_xfailed(lines: list[str]) -> None: + xfailed = self.stats.get("xfailed", []) + for rep in xfailed: + verbose_word, verbose_markup = rep._get_verbose_word_with_markup( + self.config, {_color_for_type["warnings"]: True} + ) + markup_word = self._tw.markup(verbose_word, **verbose_markup) + nodeid = _get_node_id_with_markup(self._tw, self.config, rep) + line = f"{markup_word} {nodeid}" + reason = rep.wasxfail + if reason: + line += " - " + str(reason) + + lines.append(line) + + def show_xpassed(lines: list[str]) -> None: + xpassed = self.stats.get("xpassed", []) + for rep in xpassed: + verbose_word, verbose_markup = rep._get_verbose_word_with_markup( + self.config, {_color_for_type["warnings"]: True} + ) + markup_word = self._tw.markup(verbose_word, **verbose_markup) + nodeid = _get_node_id_with_markup(self._tw, self.config, rep) + line = f"{markup_word} {nodeid}" + reason = rep.wasxfail + if reason: + line += " - " + str(reason) + lines.append(line) + + def show_skipped_folded(lines: list[str]) -> None: + skipped: list[CollectReport] = self.stats.get("skipped", []) + fskips = _folded_skips(self.startpath, skipped) if skipped else [] + if not fskips: + return + verbose_word, verbose_markup = skipped[0]._get_verbose_word_with_markup( + self.config, {_color_for_type["warnings"]: True} + ) + markup_word = self._tw.markup(verbose_word, **verbose_markup) + prefix = "Skipped: " + for num, fspath, lineno, reason in fskips: + if reason.startswith(prefix): + reason = reason[len(prefix) :] + if lineno is not None: + lines.append(f"{markup_word} [{num}] {fspath}:{lineno}: {reason}") + else: + lines.append(f"{markup_word} [{num}] {fspath}: {reason}") + + def show_skipped_unfolded(lines: list[str]) -> None: + skipped: list[CollectReport] = self.stats.get("skipped", []) + + for rep in skipped: + assert rep.longrepr is not None + assert isinstance(rep.longrepr, tuple), (rep, rep.longrepr) + assert len(rep.longrepr) == 3, (rep, rep.longrepr) + + verbose_word, verbose_markup = rep._get_verbose_word_with_markup( + self.config, {_color_for_type["warnings"]: True} + ) + markup_word = self._tw.markup(verbose_word, **verbose_markup) + nodeid = _get_node_id_with_markup(self._tw, self.config, rep) + line = f"{markup_word} {nodeid}" + reason = rep.longrepr[2] + if reason: + line += " - " + str(reason) + lines.append(line) + + def show_skipped(lines: list[str]) -> None: + if self.foldskipped: + show_skipped_folded(lines) + else: + show_skipped_unfolded(lines) + + REPORTCHAR_ACTIONS: Mapping[str, Callable[[list[str]], None]] = { + "x": show_xfailed, + "X": show_xpassed, + "f": partial(show_simple, stat="failed"), + "s": show_skipped, + "p": partial(show_simple, stat="passed"), + "E": partial(show_simple, stat="error"), + } + + lines: list[str] = [] + for char in self.reportchars: + action = REPORTCHAR_ACTIONS.get(char) + if action: # skipping e.g. "P" (passed with output) here. + action(lines) + + if lines: + self.write_sep("=", "short test summary info", cyan=True, bold=True) + for line in lines: + self.write_line(line) + + def _get_main_color(self) -> tuple[str, list[str]]: + if self._main_color is None or self._known_types is None or self._is_last_item: + self._set_main_color() + assert self._main_color + assert self._known_types + return self._main_color, self._known_types + + def _determine_main_color(self, unknown_type_seen: bool) -> str: + stats = self.stats + if "failed" in stats or "error" in stats: + main_color = "red" + elif "warnings" in stats or "xpassed" in stats or unknown_type_seen: + main_color = "yellow" + elif "passed" in stats or not self._is_last_item: + main_color = "green" + else: + main_color = "yellow" + return main_color + + def _set_main_color(self) -> None: + unknown_types: list[str] = [] + for found_type in self.stats: + if found_type: # setup/teardown reports have an empty key, ignore them + if found_type not in KNOWN_TYPES and found_type not in unknown_types: + unknown_types.append(found_type) + self._known_types = list(KNOWN_TYPES) + unknown_types + self._main_color = self._determine_main_color(bool(unknown_types)) + + def build_summary_stats_line(self) -> tuple[list[tuple[str, dict[str, bool]]], str]: + """ + Build the parts used in the last summary stats line. + + The summary stats line is the line shown at the end, "=== 12 passed, 2 errors in Xs===". + + This function builds a list of the "parts" that make up for the text in that line, in + the example above it would be:: + + [ + ("12 passed", {"green": True}), + ("2 errors", {"red": True} + ] + + That last dict for each line is a "markup dictionary", used by TerminalWriter to + color output. + + The final color of the line is also determined by this function, and is the second + element of the returned tuple. + """ + if self.config.getoption("collectonly"): + return self._build_collect_only_summary_stats_line() + else: + return self._build_normal_summary_stats_line() + + def _get_reports_to_display(self, key: str) -> list[Any]: + """Get test/collection reports for the given status key, such as `passed` or `error`.""" + reports = self.stats.get(key, []) + return [x for x in reports if getattr(x, "count_towards_summary", True)] + + def _build_normal_summary_stats_line( + self, + ) -> tuple[list[tuple[str, dict[str, bool]]], str]: + main_color, known_types = self._get_main_color() + parts = [] + + for key in known_types: + reports = self._get_reports_to_display(key) + if reports: + count = len(reports) + color = _color_for_type.get(key, _color_for_type_default) + markup = {color: True, "bold": color == main_color} + parts.append(("%d %s" % pluralize(count, key), markup)) # noqa: UP031 + + if not parts: + parts = [("no tests ran", {_color_for_type_default: True})] + + return parts, main_color + + def _build_collect_only_summary_stats_line( + self, + ) -> tuple[list[tuple[str, dict[str, bool]]], str]: + deselected = len(self._get_reports_to_display("deselected")) + errors = len(self._get_reports_to_display("error")) + + if self._numcollected == 0: + parts = [("no tests collected", {"yellow": True})] + main_color = "yellow" + + elif deselected == 0: + main_color = "green" + collected_output = "%d %s collected" % pluralize(self._numcollected, "test") # noqa: UP031 + parts = [(collected_output, {main_color: True})] + else: + all_tests_were_deselected = self._numcollected == deselected + if all_tests_were_deselected: + main_color = "yellow" + collected_output = f"no tests collected ({deselected} deselected)" + else: + main_color = "green" + selected = self._numcollected - deselected + collected_output = f"{selected}/{self._numcollected} tests collected ({deselected} deselected)" + + parts = [(collected_output, {main_color: True})] + + if errors: + main_color = _color_for_type["error"] + parts += [("%d %s" % pluralize(errors, "error"), {main_color: True})] # noqa: UP031 + + return parts, main_color + + +def _get_node_id_with_markup(tw: TerminalWriter, config: Config, rep: BaseReport): + nodeid = config.cwd_relative_nodeid(rep.nodeid) + path, *parts = nodeid.split("::") + if parts: + parts_markup = tw.markup("::".join(parts), bold=True) + return path + "::" + parts_markup + else: + return path + + +def _format_trimmed(format: str, msg: str, available_width: int) -> str | None: + """Format msg into format, ellipsizing it if doesn't fit in available_width. + + Returns None if even the ellipsis can't fit. + """ + # Only use the first line. + i = msg.find("\n") + if i != -1: + msg = msg[:i] + + ellipsis = "..." + format_width = wcswidth(format.format("")) + if format_width + len(ellipsis) > available_width: + return None + + if format_width + wcswidth(msg) > available_width: + available_width -= len(ellipsis) + msg = msg[:available_width] + while format_width + wcswidth(msg) > available_width: + msg = msg[:-1] + msg += ellipsis + + return format.format(msg) + + +def _get_line_with_reprcrash_message( + config: Config, rep: BaseReport, tw: TerminalWriter, word_markup: dict[str, bool] +) -> str: + """Get summary line for a report, trying to add reprcrash message.""" + verbose_word, verbose_markup = rep._get_verbose_word_with_markup( + config, word_markup + ) + word = tw.markup(verbose_word, **verbose_markup) + node = _get_node_id_with_markup(tw, config, rep) + + line = f"{word} {node}" + line_width = wcswidth(line) + + msg: str | None + try: + if isinstance(rep.longrepr, str): + msg = rep.longrepr + else: + # Type ignored intentionally -- possible AttributeError expected. + msg = rep.longrepr.reprcrash.message # type: ignore[union-attr] + except AttributeError: + pass + else: + if ( + running_on_ci() or config.option.verbose >= 2 + ) and not config.option.force_short_summary: + msg = f" - {msg}" + else: + available_width = tw.fullwidth - line_width + msg = _format_trimmed(" - {}", msg, available_width) + if msg is not None: + line += msg + + return line + + +def _folded_skips( + startpath: Path, + skipped: Sequence[CollectReport], +) -> list[tuple[int, str, int | None, str]]: + d: dict[tuple[str, int | None, str], list[CollectReport]] = {} + for event in skipped: + assert event.longrepr is not None + assert isinstance(event.longrepr, tuple), (event, event.longrepr) + assert len(event.longrepr) == 3, (event, event.longrepr) + fspath, lineno, reason = event.longrepr + # For consistency, report all fspaths in relative form. + fspath = bestrelpath(startpath, Path(fspath)) + keywords = getattr(event, "keywords", {}) + # Folding reports with global pytestmark variable. + # This is a workaround, because for now we cannot identify the scope of a skip marker + # TODO: Revisit after marks scope would be fixed. + if ( + event.when == "setup" + and "skip" in keywords + and "pytestmark" not in keywords + ): + key: tuple[str, int | None, str] = (fspath, None, reason) + else: + key = (fspath, lineno, reason) + d.setdefault(key, []).append(event) + values: list[tuple[int, str, int | None, str]] = [] + for key, events in d.items(): + values.append((len(events), *key)) + return values + + +_color_for_type = { + "failed": "red", + "error": "red", + "warnings": "yellow", + "passed": "green", + "subtests passed": "green", + "subtests failed": "red", +} +_color_for_type_default = "yellow" + + +def pluralize(count: int, noun: str) -> tuple[int, str]: + # No need to pluralize words such as `failed` or `passed`. + if noun not in ["error", "warnings", "test"]: + return count, noun + + # The `warnings` key is plural. To avoid API breakage, we keep it that way but + # set it to singular here so we can determine plurality in the same way as we do + # for `error`. + noun = noun.replace("warnings", "warning") + + return count, noun + "s" if count != 1 else noun + + +def _plugin_nameversions(plugininfo) -> list[str]: + values: list[str] = [] + for plugin, dist in plugininfo: + # Gets us name and version! + name = f"{dist.project_name}-{dist.version}" + # Questionable convenience, but it keeps things short. + if name.startswith("pytest-"): + name = name[7:] + # We decided to print python package names they can have more than one plugin. + if name not in values: + values.append(name) + return values + + +def format_session_duration(seconds: float) -> str: + """Format the given seconds in a human readable manner to show in the final summary.""" + if seconds < 60: + return f"{seconds:.2f}s" + else: + dt = datetime.timedelta(seconds=int(seconds)) + return f"{seconds:.2f}s ({dt})" + + +def format_node_duration(seconds: float) -> str: + """Format the given seconds in a human readable manner to show in the test progress.""" + # The formatting is designed to be compact and readable, with at most 7 characters + # for durations below 100 hours. + if seconds < 0.00001: + return f" {seconds * 1000000:.3f}us" + if seconds < 0.0001: + return f" {seconds * 1000000:.2f}us" + if seconds < 0.001: + return f" {seconds * 1000000:.1f}us" + if seconds < 0.01: + return f" {seconds * 1000:.3f}ms" + if seconds < 0.1: + return f" {seconds * 1000:.2f}ms" + if seconds < 1: + return f" {seconds * 1000:.1f}ms" + if seconds < 60: + return f" {seconds:.3f}s" + if seconds < 3600: + return f" {seconds // 60:.0f}m {seconds % 60:.0f}s" + return f" {seconds // 3600:.0f}h {(seconds % 3600) // 60:.0f}m" + + +def _get_raw_skip_reason(report: TestReport) -> str: + """Get the reason string of a skip/xfail/xpass test report. + + The string is just the part given by the user. + """ + if hasattr(report, "wasxfail"): + reason = report.wasxfail + if reason.startswith("reason: "): + reason = reason[len("reason: ") :] + return reason + else: + assert report.skipped + assert isinstance(report.longrepr, tuple) + _, _, reason = report.longrepr + if reason.startswith("Skipped: "): + reason = reason[len("Skipped: ") :] + elif reason == "Skipped": + reason = "" + return reason + + +class TerminalProgressPlugin: + """Terminal progress reporting plugin using OSC 9;4 ANSI sequences. + + Emits OSC 9;4 sequences to indicate test progress to terminal + tabs/windows/etc. + + Not all terminal emulators support this feature. + + Ref: https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC + """ + + def __init__(self, tr: TerminalReporter) -> None: + self._tr = tr + self._session: Session | None = None + self._has_failures = False + + def _emit_progress( + self, + state: Literal["remove", "normal", "error", "indeterminate", "paused"], + progress: int | None = None, + ) -> None: + """Emit OSC 9;4 sequence for indicating progress to the terminal. + + :param state: + Progress state to set. + :param progress: + Progress value 0-100. Required for "normal", optional for "error" + and "paused", otherwise ignored. + """ + assert progress is None or 0 <= progress <= 100 + + # OSC 9;4 sequence: ESC ] 9 ; 4 ; state ; progress ST + # ST can be ESC \ or BEL. ESC \ seems better supported. + match state: + case "remove": + sequence = "\x1b]9;4;0;\x1b\\" + case "normal": + assert progress is not None + sequence = f"\x1b]9;4;1;{progress}\x1b\\" + case "error": + if progress is not None: + sequence = f"\x1b]9;4;2;{progress}\x1b\\" + else: + sequence = "\x1b]9;4;2;\x1b\\" + case "indeterminate": + sequence = "\x1b]9;4;3;\x1b\\" + case "paused": + if progress is not None: + sequence = f"\x1b]9;4;4;{progress}\x1b\\" + else: + sequence = "\x1b]9;4;4;\x1b\\" + + self._tr.write_raw(sequence, flush=True) + + @hookimpl + def pytest_sessionstart(self, session: Session) -> None: + self._session = session + # Show indeterminate progress during collection. + self._emit_progress("indeterminate") + + @hookimpl + def pytest_collection_finish(self) -> None: + assert self._session is not None + if self._session.testscollected > 0: + # Switch from indeterminate to 0% progress. + self._emit_progress("normal", 0) + + @hookimpl + def pytest_runtest_logreport(self, report: TestReport) -> None: + if report.failed: + self._has_failures = True + + # Let's consider the "call" phase for progress. + if report.when != "call": + return + + # Calculate and emit progress. + assert self._session is not None + collected = self._session.testscollected + if collected > 0: + reported = self._tr.reported_progress + progress = min(reported * 100 // collected, 100) + self._emit_progress("error" if self._has_failures else "normal", progress) + + @hookimpl + def pytest_sessionfinish(self) -> None: + self._emit_progress("remove") diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/threadexception.py b/Backend/venv/lib/python3.12/site-packages/_pytest/threadexception.py new file mode 100644 index 00000000..eb57783b --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/threadexception.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +import collections +from collections.abc import Callable +import functools +import sys +import threading +import traceback +from typing import NamedTuple +from typing import TYPE_CHECKING +import warnings + +from _pytest.config import Config +from _pytest.nodes import Item +from _pytest.stash import StashKey +from _pytest.tracemalloc import tracemalloc_message +import pytest + + +if TYPE_CHECKING: + pass + +if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup + + +class ThreadExceptionMeta(NamedTuple): + msg: str + cause_msg: str + exc_value: BaseException | None + + +thread_exceptions: StashKey[collections.deque[ThreadExceptionMeta | BaseException]] = ( + StashKey() +) + + +def collect_thread_exception(config: Config) -> None: + pop_thread_exception = config.stash[thread_exceptions].pop + errors: list[pytest.PytestUnhandledThreadExceptionWarning | RuntimeError] = [] + meta = None + hook_error = None + try: + while True: + try: + meta = pop_thread_exception() + except IndexError: + break + + if isinstance(meta, BaseException): + hook_error = RuntimeError("Failed to process thread exception") + hook_error.__cause__ = meta + errors.append(hook_error) + continue + + msg = meta.msg + try: + warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg)) + except pytest.PytestUnhandledThreadExceptionWarning as e: + # This except happens when the warning is treated as an error (e.g. `-Werror`). + if meta.exc_value is not None: + # Exceptions have a better way to show the traceback, but + # warnings do not, so hide the traceback from the msg and + # set the cause so the traceback shows up in the right place. + e.args = (meta.cause_msg,) + e.__cause__ = meta.exc_value + errors.append(e) + + if len(errors) == 1: + raise errors[0] + if errors: + raise ExceptionGroup("multiple thread exception warnings", errors) + finally: + del errors, meta, hook_error + + +def cleanup( + *, config: Config, prev_hook: Callable[[threading.ExceptHookArgs], object] +) -> None: + try: + try: + # We don't join threads here, so exceptions raised from any + # threads still running by the time _threading_atexits joins them + # do not get captured (see #13027). + collect_thread_exception(config) + finally: + threading.excepthook = prev_hook + finally: + del config.stash[thread_exceptions] + + +def thread_exception_hook( + args: threading.ExceptHookArgs, + /, + *, + append: Callable[[ThreadExceptionMeta | BaseException], object], +) -> None: + try: + # we need to compute these strings here as they might change after + # the excepthook finishes and before the metadata object is + # collected by a pytest hook + thread_name = "" if args.thread is None else args.thread.name + summary = f"Exception in thread {thread_name}" + traceback_message = "\n\n" + "".join( + traceback.format_exception( + args.exc_type, + args.exc_value, + args.exc_traceback, + ) + ) + tracemalloc_tb = "\n" + tracemalloc_message(args.thread) + msg = summary + traceback_message + tracemalloc_tb + cause_msg = summary + tracemalloc_tb + + append( + ThreadExceptionMeta( + # Compute these strings here as they might change later + msg=msg, + cause_msg=cause_msg, + exc_value=args.exc_value, + ) + ) + except BaseException as e: + append(e) + # Raising this will cause the exception to be logged twice, once in our + # collect_thread_exception and once by sys.excepthook + # which is fine - this should never happen anyway and if it does + # it should probably be reported as a pytest bug. + raise + + +def pytest_configure(config: Config) -> None: + prev_hook = threading.excepthook + deque: collections.deque[ThreadExceptionMeta | BaseException] = collections.deque() + config.stash[thread_exceptions] = deque + config.add_cleanup(functools.partial(cleanup, config=config, prev_hook=prev_hook)) + threading.excepthook = functools.partial(thread_exception_hook, append=deque.append) + + +@pytest.hookimpl(trylast=True) +def pytest_runtest_setup(item: Item) -> None: + collect_thread_exception(item.config) + + +@pytest.hookimpl(trylast=True) +def pytest_runtest_call(item: Item) -> None: + collect_thread_exception(item.config) + + +@pytest.hookimpl(trylast=True) +def pytest_runtest_teardown(item: Item) -> None: + collect_thread_exception(item.config) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/timing.py b/Backend/venv/lib/python3.12/site-packages/_pytest/timing.py new file mode 100644 index 00000000..51c3db23 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/timing.py @@ -0,0 +1,95 @@ +"""Indirection for time functions. + +We intentionally grab some "time" functions internally to avoid tests mocking "time" to affect +pytest runtime information (issue #185). + +Fixture "mock_timing" also interacts with this module for pytest's own tests. +""" + +from __future__ import annotations + +import dataclasses +from datetime import datetime +from datetime import timezone +from time import perf_counter +from time import sleep +from time import time +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from pytest import MonkeyPatch + + +@dataclasses.dataclass(frozen=True) +class Instant: + """ + Represents an instant in time, used to both get the timestamp value and to measure + the duration of a time span. + + Inspired by Rust's `std::time::Instant`. + """ + + # Creation time of this instant, using time.time(), to measure actual time. + # Note: using a `lambda` to correctly get the mocked time via `MockTiming`. + time: float = dataclasses.field(default_factory=lambda: time(), init=False) + + # Performance counter tick of the instant, used to measure precise elapsed time. + # Note: using a `lambda` to correctly get the mocked time via `MockTiming`. + perf_count: float = dataclasses.field( + default_factory=lambda: perf_counter(), init=False + ) + + def elapsed(self) -> Duration: + """Measure the duration since `Instant` was created.""" + return Duration(start=self, stop=Instant()) + + def as_utc(self) -> datetime: + """Instant as UTC datetime.""" + return datetime.fromtimestamp(self.time, timezone.utc) + + +@dataclasses.dataclass(frozen=True) +class Duration: + """A span of time as measured by `Instant.elapsed()`.""" + + start: Instant + stop: Instant + + @property + def seconds(self) -> float: + """Elapsed time of the duration in seconds, measured using a performance counter for precise timing.""" + return self.stop.perf_count - self.start.perf_count + + +@dataclasses.dataclass +class MockTiming: + """Mocks _pytest.timing with a known object that can be used to control timing in tests + deterministically. + + pytest itself should always use functions from `_pytest.timing` instead of `time` directly. + + This then allows us more control over time during testing, if testing code also + uses `_pytest.timing` functions. + + Time is static, and only advances through `sleep` calls, thus tests might sleep over large + numbers and obtain accurate time() calls at the end, making tests reliable and instant.""" + + _current_time: float = datetime(2020, 5, 22, 14, 20, 50).timestamp() + + def sleep(self, seconds: float) -> None: + self._current_time += seconds + + def time(self) -> float: + return self._current_time + + def patch(self, monkeypatch: MonkeyPatch) -> None: + # pylint: disable-next=import-self + from _pytest import timing # noqa: PLW0406 + + monkeypatch.setattr(timing, "sleep", self.sleep) + monkeypatch.setattr(timing, "time", self.time) + monkeypatch.setattr(timing, "perf_counter", self.time) + + +__all__ = ["perf_counter", "sleep", "time"] diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/tmpdir.py b/Backend/venv/lib/python3.12/site-packages/_pytest/tmpdir.py new file mode 100644 index 00000000..dcd5784f --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/tmpdir.py @@ -0,0 +1,312 @@ +# mypy: allow-untyped-defs +"""Support for providing temporary directories to test functions.""" + +from __future__ import annotations + +from collections.abc import Generator +import dataclasses +import os +from pathlib import Path +import re +from shutil import rmtree +import tempfile +from typing import Any +from typing import final +from typing import Literal + +from .pathlib import cleanup_dead_symlinks +from .pathlib import LOCK_TIMEOUT +from .pathlib import make_numbered_dir +from .pathlib import make_numbered_dir_with_cleanup +from .pathlib import rm_rf +from _pytest.compat import get_user_id +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import fixture +from _pytest.fixtures import FixtureRequest +from _pytest.monkeypatch import MonkeyPatch +from _pytest.nodes import Item +from _pytest.reports import TestReport +from _pytest.stash import StashKey + + +tmppath_result_key = StashKey[dict[str, bool]]() +RetentionType = Literal["all", "failed", "none"] + + +@final +@dataclasses.dataclass +class TempPathFactory: + """Factory for temporary directories under the common base temp directory, + as discussed at :ref:`temporary directory location and retention`. + """ + + _given_basetemp: Path | None + # pluggy TagTracerSub, not currently exposed, so Any. + _trace: Any + _basetemp: Path | None + _retention_count: int + _retention_policy: RetentionType + + def __init__( + self, + given_basetemp: Path | None, + retention_count: int, + retention_policy: RetentionType, + trace, + basetemp: Path | None = None, + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + if given_basetemp is None: + self._given_basetemp = None + else: + # Use os.path.abspath() to get absolute path instead of resolve() as it + # does not work the same in all platforms (see #4427). + # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012). + self._given_basetemp = Path(os.path.abspath(str(given_basetemp))) + self._trace = trace + self._retention_count = retention_count + self._retention_policy = retention_policy + self._basetemp = basetemp + + @classmethod + def from_config( + cls, + config: Config, + *, + _ispytest: bool = False, + ) -> TempPathFactory: + """Create a factory according to pytest configuration. + + :meta private: + """ + check_ispytest(_ispytest) + count = int(config.getini("tmp_path_retention_count")) + if count < 0: + raise ValueError( + f"tmp_path_retention_count must be >= 0. Current input: {count}." + ) + + policy = config.getini("tmp_path_retention_policy") + if policy not in ("all", "failed", "none"): + raise ValueError( + f"tmp_path_retention_policy must be either all, failed, none. Current input: {policy}." + ) + + return cls( + given_basetemp=config.option.basetemp, + trace=config.trace.get("tmpdir"), + retention_count=count, + retention_policy=policy, + _ispytest=True, + ) + + def _ensure_relative_to_basetemp(self, basename: str) -> str: + basename = os.path.normpath(basename) + if (self.getbasetemp() / basename).resolve().parent != self.getbasetemp(): + raise ValueError(f"{basename} is not a normalized and relative path") + return basename + + def mktemp(self, basename: str, numbered: bool = True) -> Path: + """Create a new temporary directory managed by the factory. + + :param basename: + Directory base name, must be a relative path. + + :param numbered: + If ``True``, ensure the directory is unique by adding a numbered + suffix greater than any existing one: ``basename="foo-"`` and ``numbered=True`` + means that this function will create directories named ``"foo-0"``, + ``"foo-1"``, ``"foo-2"`` and so on. + + :returns: + The path to the new directory. + """ + basename = self._ensure_relative_to_basetemp(basename) + if not numbered: + p = self.getbasetemp().joinpath(basename) + p.mkdir(mode=0o700) + else: + p = make_numbered_dir(root=self.getbasetemp(), prefix=basename, mode=0o700) + self._trace("mktemp", p) + return p + + def getbasetemp(self) -> Path: + """Return the base temporary directory, creating it if needed. + + :returns: + The base temporary directory. + """ + if self._basetemp is not None: + return self._basetemp + + if self._given_basetemp is not None: + basetemp = self._given_basetemp + if basetemp.exists(): + rm_rf(basetemp) + basetemp.mkdir(mode=0o700) + basetemp = basetemp.resolve() + else: + from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT") + temproot = Path(from_env or tempfile.gettempdir()).resolve() + user = get_user() or "unknown" + # use a sub-directory in the temproot to speed-up + # make_numbered_dir() call + rootdir = temproot.joinpath(f"pytest-of-{user}") + try: + rootdir.mkdir(mode=0o700, exist_ok=True) + except OSError: + # getuser() likely returned illegal characters for the platform, use unknown back off mechanism + rootdir = temproot.joinpath("pytest-of-unknown") + rootdir.mkdir(mode=0o700, exist_ok=True) + # Because we use exist_ok=True with a predictable name, make sure + # we are the owners, to prevent any funny business (on unix, where + # temproot is usually shared). + # Also, to keep things private, fixup any world-readable temp + # rootdir's permissions. Historically 0o755 was used, so we can't + # just error out on this, at least for a while. + uid = get_user_id() + if uid is not None: + rootdir_stat = rootdir.stat() + if rootdir_stat.st_uid != uid: + raise OSError( + f"The temporary directory {rootdir} is not owned by the current user. " + "Fix this and try again." + ) + if (rootdir_stat.st_mode & 0o077) != 0: + os.chmod(rootdir, rootdir_stat.st_mode & ~0o077) + keep = self._retention_count + if self._retention_policy == "none": + keep = 0 + basetemp = make_numbered_dir_with_cleanup( + prefix="pytest-", + root=rootdir, + keep=keep, + lock_timeout=LOCK_TIMEOUT, + mode=0o700, + ) + assert basetemp is not None, basetemp + self._basetemp = basetemp + self._trace("new basetemp", basetemp) + return basetemp + + +def get_user() -> str | None: + """Return the current user name, or None if getuser() does not work + in the current environment (see #1010).""" + try: + # In some exotic environments, getpass may not be importable. + import getpass + + return getpass.getuser() + except (ImportError, OSError, KeyError): + return None + + +def pytest_configure(config: Config) -> None: + """Create a TempPathFactory and attach it to the config object. + + This is to comply with existing plugins which expect the handler to be + available at pytest_configure time, but ideally should be moved entirely + to the tmp_path_factory session fixture. + """ + mp = MonkeyPatch() + config.add_cleanup(mp.undo) + _tmp_path_factory = TempPathFactory.from_config(config, _ispytest=True) + mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False) + + +def pytest_addoption(parser: Parser) -> None: + parser.addini( + "tmp_path_retention_count", + help="How many sessions should we keep the `tmp_path` directories, according to `tmp_path_retention_policy`.", + default=3, + ) + + parser.addini( + "tmp_path_retention_policy", + help="Controls which directories created by the `tmp_path` fixture are kept around, based on test outcome. " + "(all/failed/none)", + default="all", + ) + + +@fixture(scope="session") +def tmp_path_factory(request: FixtureRequest) -> TempPathFactory: + """Return a :class:`pytest.TempPathFactory` instance for the test session.""" + # Set dynamically by pytest_configure() above. + return request.config._tmp_path_factory # type: ignore + + +def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path: + name = request.node.name + name = re.sub(r"[\W]", "_", name) + MAXVAL = 30 + name = name[:MAXVAL] + return factory.mktemp(name, numbered=True) + + +@fixture +def tmp_path( + request: FixtureRequest, tmp_path_factory: TempPathFactory +) -> Generator[Path]: + """Return a temporary directory (as :class:`pathlib.Path` object) + which is unique to each test function invocation. + The temporary directory is created as a subdirectory + of the base temporary directory, with configurable retention, + as discussed in :ref:`temporary directory location and retention`. + """ + path = _mk_tmp(request, tmp_path_factory) + yield path + + # Remove the tmpdir if the policy is "failed" and the test passed. + policy = tmp_path_factory._retention_policy + result_dict = request.node.stash[tmppath_result_key] + + if policy == "failed" and result_dict.get("call", True): + # We do a "best effort" to remove files, but it might not be possible due to some leaked resource, + # permissions, etc, in which case we ignore it. + rmtree(path, ignore_errors=True) + + del request.node.stash[tmppath_result_key] + + +def pytest_sessionfinish(session, exitstatus: int | ExitCode): + """After each session, remove base directory if all the tests passed, + the policy is "failed", and the basetemp is not specified by a user. + """ + tmp_path_factory: TempPathFactory = session.config._tmp_path_factory + basetemp = tmp_path_factory._basetemp + if basetemp is None: + return + + policy = tmp_path_factory._retention_policy + if ( + exitstatus == 0 + and policy == "failed" + and tmp_path_factory._given_basetemp is None + ): + if basetemp.is_dir(): + # We do a "best effort" to remove files, but it might not be possible due to some leaked resource, + # permissions, etc, in which case we ignore it. + rmtree(basetemp, ignore_errors=True) + + # Remove dead symlinks. + if basetemp.is_dir(): + cleanup_dead_symlinks(basetemp) + + +@hookimpl(wrapper=True, tryfirst=True) +def pytest_runtest_makereport( + item: Item, call +) -> Generator[None, TestReport, TestReport]: + rep = yield + assert rep.when is not None + empty: dict[str, bool] = {} + item.stash.setdefault(tmppath_result_key, empty)[rep.when] = rep.passed + return rep diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/tracemalloc.py b/Backend/venv/lib/python3.12/site-packages/_pytest/tracemalloc.py new file mode 100644 index 00000000..5d0b1985 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/tracemalloc.py @@ -0,0 +1,24 @@ +from __future__ import annotations + + +def tracemalloc_message(source: object) -> str: + if source is None: + return "" + + try: + import tracemalloc + except ImportError: + return "" + + tb = tracemalloc.get_object_traceback(source) + if tb is not None: + formatted_tb = "\n".join(tb.format()) + # Use a leading new line to better separate the (large) output + # from the traceback to the previous warning text. + return f"\nObject allocated at:\n{formatted_tb}" + # No need for a leading new line. + url = "https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings" + return ( + "Enable tracemalloc to get traceback where the object was allocated.\n" + f"See {url} for more info." + ) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/unittest.py b/Backend/venv/lib/python3.12/site-packages/_pytest/unittest.py new file mode 100644 index 00000000..7498f1b0 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/unittest.py @@ -0,0 +1,614 @@ +# mypy: allow-untyped-defs +"""Discover and run std-library "unittest" style tests.""" + +from __future__ import annotations + +from collections.abc import Callable +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import Iterator +from enum import auto +from enum import Enum +import inspect +import sys +import traceback +import types +from typing import Any +from typing import TYPE_CHECKING +from unittest import TestCase + +import _pytest._code +from _pytest._code import ExceptionInfo +from _pytest.compat import assert_never +from _pytest.compat import is_async_function +from _pytest.config import hookimpl +from _pytest.fixtures import FixtureRequest +from _pytest.monkeypatch import MonkeyPatch +from _pytest.nodes import Collector +from _pytest.nodes import Item +from _pytest.outcomes import exit +from _pytest.outcomes import fail +from _pytest.outcomes import skip +from _pytest.outcomes import xfail +from _pytest.python import Class +from _pytest.python import Function +from _pytest.python import Module +from _pytest.runner import CallInfo +from _pytest.runner import check_interactive_exception +from _pytest.subtests import SubtestContext +from _pytest.subtests import SubtestReport + + +if sys.version_info[:2] < (3, 11): + from exceptiongroup import ExceptionGroup + +if TYPE_CHECKING: + from types import TracebackType + import unittest + + import twisted.trial.unittest + + +_SysExcInfoType = ( + tuple[type[BaseException], BaseException, types.TracebackType] + | tuple[None, None, None] +) + + +def pytest_pycollect_makeitem( + collector: Module | Class, name: str, obj: object +) -> UnitTestCase | None: + try: + # Has unittest been imported? + ut = sys.modules["unittest"] + # Is obj a subclass of unittest.TestCase? + # Type ignored because `ut` is an opaque module. + if not issubclass(obj, ut.TestCase): # type: ignore + return None + except Exception: + return None + # Is obj a concrete class? + # Abstract classes can't be instantiated so no point collecting them. + if inspect.isabstract(obj): + return None + # Yes, so let's collect it. + return UnitTestCase.from_parent(collector, name=name, obj=obj) + + +class UnitTestCase(Class): + # Marker for fixturemanger.getfixtureinfo() + # to declare that our children do not support funcargs. + nofuncargs = True + + def newinstance(self): + # TestCase __init__ takes the method (test) name. The TestCase + # constructor treats the name "runTest" as a special no-op, so it can be + # used when a dummy instance is needed. While unittest.TestCase has a + # default, some subclasses omit the default (#9610), so always supply + # it. + return self.obj("runTest") + + def collect(self) -> Iterable[Item | Collector]: + from unittest import TestLoader + + cls = self.obj + if not getattr(cls, "__test__", True): + return + + skipped = _is_skipped(cls) + if not skipped: + self._register_unittest_setup_method_fixture(cls) + self._register_unittest_setup_class_fixture(cls) + self._register_setup_class_fixture() + + self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid) + + loader = TestLoader() + foundsomething = False + for name in loader.getTestCaseNames(self.obj): + x = getattr(self.obj, name) + if not getattr(x, "__test__", True): + continue + yield TestCaseFunction.from_parent(self, name=name) + foundsomething = True + + if not foundsomething: + runtest = getattr(self.obj, "runTest", None) + if runtest is not None: + ut = sys.modules.get("twisted.trial.unittest", None) + if ut is None or runtest != ut.TestCase.runTest: + yield TestCaseFunction.from_parent(self, name="runTest") + + def _register_unittest_setup_class_fixture(self, cls: type) -> None: + """Register an auto-use fixture to invoke setUpClass and + tearDownClass (#517).""" + setup = getattr(cls, "setUpClass", None) + teardown = getattr(cls, "tearDownClass", None) + if setup is None and teardown is None: + return None + cleanup = getattr(cls, "doClassCleanups", lambda: None) + + def process_teardown_exceptions() -> None: + # tearDown_exceptions is a list set in the class containing exc_infos for errors during + # teardown for the class. + exc_infos = getattr(cls, "tearDown_exceptions", None) + if not exc_infos: + return + exceptions = [exc for (_, exc, _) in exc_infos] + # If a single exception, raise it directly as this provides a more readable + # error (hopefully this will improve in #12255). + if len(exceptions) == 1: + raise exceptions[0] + else: + raise ExceptionGroup("Unittest class cleanup errors", exceptions) + + def unittest_setup_class_fixture( + request: FixtureRequest, + ) -> Generator[None]: + cls = request.cls + if _is_skipped(cls): + reason = cls.__unittest_skip_why__ + raise skip.Exception(reason, _use_item_location=True) + if setup is not None: + try: + setup() + # unittest does not call the cleanup function for every BaseException, so we + # follow this here. + except Exception: + cleanup() + process_teardown_exceptions() + raise + yield + try: + if teardown is not None: + teardown() + finally: + cleanup() + process_teardown_exceptions() + + self.session._fixturemanager._register_fixture( + # Use a unique name to speed up lookup. + name=f"_unittest_setUpClass_fixture_{cls.__qualname__}", + func=unittest_setup_class_fixture, + nodeid=self.nodeid, + scope="class", + autouse=True, + ) + + def _register_unittest_setup_method_fixture(self, cls: type) -> None: + """Register an auto-use fixture to invoke setup_method and + teardown_method (#517).""" + setup = getattr(cls, "setup_method", None) + teardown = getattr(cls, "teardown_method", None) + if setup is None and teardown is None: + return None + + def unittest_setup_method_fixture( + request: FixtureRequest, + ) -> Generator[None]: + self = request.instance + if _is_skipped(self): + reason = self.__unittest_skip_why__ + raise skip.Exception(reason, _use_item_location=True) + if setup is not None: + setup(self, request.function) + yield + if teardown is not None: + teardown(self, request.function) + + self.session._fixturemanager._register_fixture( + # Use a unique name to speed up lookup. + name=f"_unittest_setup_method_fixture_{cls.__qualname__}", + func=unittest_setup_method_fixture, + nodeid=self.nodeid, + scope="function", + autouse=True, + ) + + +class TestCaseFunction(Function): + nofuncargs = True + failfast = False + _excinfo: list[_pytest._code.ExceptionInfo[BaseException]] | None = None + + def _getinstance(self): + assert isinstance(self.parent, UnitTestCase) + return self.parent.obj(self.name) + + # Backward compat for pytest-django; can be removed after pytest-django + # updates + some slack. + @property + def _testcase(self): + return self.instance + + def setup(self) -> None: + # A bound method to be called during teardown() if set (see 'runtest()'). + self._explicit_tearDown: Callable[[], None] | None = None + super().setup() + + def teardown(self) -> None: + if self._explicit_tearDown is not None: + self._explicit_tearDown() + self._explicit_tearDown = None + self._obj = None + del self._instance + super().teardown() + + def startTest(self, testcase: unittest.TestCase) -> None: + pass + + def _addexcinfo(self, rawexcinfo: _SysExcInfoType) -> None: + rawexcinfo = _handle_twisted_exc_info(rawexcinfo) + try: + excinfo = _pytest._code.ExceptionInfo[BaseException].from_exc_info( + rawexcinfo # type: ignore[arg-type] + ) + # Invoke the attributes to trigger storing the traceback + # trial causes some issue there. + _ = excinfo.value + _ = excinfo.traceback + except TypeError: + try: + try: + values = traceback.format_exception(*rawexcinfo) + values.insert( + 0, + "NOTE: Incompatible Exception Representation, " + "displaying natively:\n\n", + ) + fail("".join(values), pytrace=False) + except (fail.Exception, KeyboardInterrupt): + raise + except BaseException: + fail( + "ERROR: Unknown Incompatible Exception " + f"representation:\n{rawexcinfo!r}", + pytrace=False, + ) + except KeyboardInterrupt: + raise + except fail.Exception: + excinfo = _pytest._code.ExceptionInfo.from_current() + self.__dict__.setdefault("_excinfo", []).append(excinfo) + + def addError( + self, testcase: unittest.TestCase, rawexcinfo: _SysExcInfoType + ) -> None: + try: + if isinstance(rawexcinfo[1], exit.Exception): + exit(rawexcinfo[1].msg) + except TypeError: + pass + self._addexcinfo(rawexcinfo) + + def addFailure( + self, testcase: unittest.TestCase, rawexcinfo: _SysExcInfoType + ) -> None: + self._addexcinfo(rawexcinfo) + + def addSkip( + self, testcase: unittest.TestCase, reason: str, *, handle_subtests: bool = True + ) -> None: + from unittest.case import _SubTest # type: ignore[attr-defined] + + def add_skip() -> None: + try: + raise skip.Exception(reason, _use_item_location=True) + except skip.Exception: + self._addexcinfo(sys.exc_info()) + + if not handle_subtests: + add_skip() + return + + if isinstance(testcase, _SubTest): + add_skip() + if self._excinfo is not None: + exc_info = self._excinfo[-1] + self.addSubTest(testcase.test_case, testcase, exc_info) + else: + # For python < 3.11: the non-subtest skips have to be added by `add_skip` only after all subtest + # failures are processed by `_addSubTest`: `self.instance._outcome` has no attribute + # `skipped/errors` anymore. + # We also need to check if `self.instance._outcome` is `None` (this happens if the test + # class/method is decorated with `unittest.skip`, see pytest-dev/pytest-subtests#173). + if sys.version_info < (3, 11) and self.instance._outcome is not None: + subtest_errors = [ + x + for x, y in self.instance._outcome.errors + if isinstance(x, _SubTest) and y is not None + ] + if len(subtest_errors) == 0: + add_skip() + else: + add_skip() + + def addExpectedFailure( + self, + testcase: unittest.TestCase, + rawexcinfo: _SysExcInfoType, + reason: str = "", + ) -> None: + try: + xfail(str(reason)) + except xfail.Exception: + self._addexcinfo(sys.exc_info()) + + def addUnexpectedSuccess( + self, + testcase: unittest.TestCase, + reason: twisted.trial.unittest.Todo | None = None, + ) -> None: + msg = "Unexpected success" + if reason: + msg += f": {reason.reason}" + # Preserve unittest behaviour - fail the test. Explicitly not an XPASS. + try: + fail(msg, pytrace=False) + except fail.Exception: + self._addexcinfo(sys.exc_info()) + + def addSuccess(self, testcase: unittest.TestCase) -> None: + pass + + def stopTest(self, testcase: unittest.TestCase) -> None: + pass + + def addDuration(self, testcase: unittest.TestCase, elapsed: float) -> None: + pass + + def runtest(self) -> None: + from _pytest.debugging import maybe_wrap_pytest_function_for_tracing + + testcase = self.instance + assert testcase is not None + + maybe_wrap_pytest_function_for_tracing(self) + + # Let the unittest framework handle async functions. + if is_async_function(self.obj): + testcase(result=self) + else: + # When --pdb is given, we want to postpone calling tearDown() otherwise + # when entering the pdb prompt, tearDown() would have probably cleaned up + # instance variables, which makes it difficult to debug. + # Arguably we could always postpone tearDown(), but this changes the moment where the + # TestCase instance interacts with the results object, so better to only do it + # when absolutely needed. + # We need to consider if the test itself is skipped, or the whole class. + assert isinstance(self.parent, UnitTestCase) + skipped = _is_skipped(self.obj) or _is_skipped(self.parent.obj) + if self.config.getoption("usepdb") and not skipped: + self._explicit_tearDown = testcase.tearDown + setattr(testcase, "tearDown", lambda *args: None) + + # We need to update the actual bound method with self.obj, because + # wrap_pytest_function_for_tracing replaces self.obj by a wrapper. + setattr(testcase, self.name, self.obj) + try: + testcase(result=self) + finally: + delattr(testcase, self.name) + + def _traceback_filter( + self, excinfo: _pytest._code.ExceptionInfo[BaseException] + ) -> _pytest._code.Traceback: + traceback = super()._traceback_filter(excinfo) + ntraceback = traceback.filter( + lambda x: not x.frame.f_globals.get("__unittest"), + ) + if not ntraceback: + ntraceback = traceback + return ntraceback + + def addSubTest( + self, + test_case: Any, + test: TestCase, + exc_info: ExceptionInfo[BaseException] + | tuple[type[BaseException], BaseException, TracebackType] + | None, + ) -> None: + exception_info: ExceptionInfo[BaseException] | None + match exc_info: + case tuple(): + exception_info = ExceptionInfo(exc_info, _ispytest=True) + case ExceptionInfo() | None: + exception_info = exc_info + case unreachable: + assert_never(unreachable) + + call_info = CallInfo[None]( + None, + exception_info, + start=0, + stop=0, + duration=0, + when="call", + _ispytest=True, + ) + msg = test._message if isinstance(test._message, str) else None # type: ignore[attr-defined] + report = self.ihook.pytest_runtest_makereport(item=self, call=call_info) + sub_report = SubtestReport._new( + report, + SubtestContext(msg=msg, kwargs=dict(test.params)), # type: ignore[attr-defined] + captured_output=None, + captured_logs=None, + ) + self.ihook.pytest_runtest_logreport(report=sub_report) + if check_interactive_exception(call_info, sub_report): + self.ihook.pytest_exception_interact( + node=self, call=call_info, report=sub_report + ) + + # For python < 3.11: add non-subtest skips once all subtest failures are processed by # `_addSubTest`. + if sys.version_info < (3, 11): + from unittest.case import _SubTest # type: ignore[attr-defined] + + non_subtest_skip = [ + (x, y) + for x, y in self.instance._outcome.skipped + if not isinstance(x, _SubTest) + ] + subtest_errors = [ + (x, y) + for x, y in self.instance._outcome.errors + if isinstance(x, _SubTest) and y is not None + ] + # Check if we have non-subtest skips: if there are also sub failures, non-subtest skips are not treated in + # `_addSubTest` and have to be added using `add_skip` after all subtest failures are processed. + if len(non_subtest_skip) > 0 and len(subtest_errors) > 0: + # Make sure we have processed the last subtest failure + last_subset_error = subtest_errors[-1] + if exc_info is last_subset_error[-1]: + # Add non-subtest skips (as they could not be treated in `_addSkip`) + for testcase, reason in non_subtest_skip: + self.addSkip(testcase, reason, handle_subtests=False) + + +@hookimpl(tryfirst=True) +def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: + if isinstance(item, TestCaseFunction): + if item._excinfo: + call.excinfo = item._excinfo.pop(0) + try: + del call.result + except AttributeError: + pass + + # Convert unittest.SkipTest to pytest.skip. + # This covers explicit `raise unittest.SkipTest`. + unittest = sys.modules.get("unittest") + if unittest and call.excinfo and isinstance(call.excinfo.value, unittest.SkipTest): + excinfo = call.excinfo + call2 = CallInfo[None].from_call(lambda: skip(str(excinfo.value)), call.when) + call.excinfo = call2.excinfo + + +def _is_skipped(obj) -> bool: + """Return True if the given object has been marked with @unittest.skip.""" + return bool(getattr(obj, "__unittest_skip__", False)) + + +def pytest_configure() -> None: + """Register the TestCaseFunction class as an IReporter if twisted.trial is available.""" + if _get_twisted_version() is not TwistedVersion.NotInstalled: + from twisted.trial.itrial import IReporter + from zope.interface import classImplements + + classImplements(TestCaseFunction, IReporter) + + +class TwistedVersion(Enum): + """ + The Twisted version installed in the environment. + + We have different workarounds in place for different versions of Twisted. + """ + + # Twisted version 24 or prior. + Version24 = auto() + # Twisted version 25 or later. + Version25 = auto() + # Twisted version is not available. + NotInstalled = auto() + + +def _get_twisted_version() -> TwistedVersion: + # We need to check if "twisted.trial.unittest" is specifically present in sys.modules. + # This is because we intend to integrate with Trial only when it's actively running + # the test suite, but not needed when only other Twisted components are in use. + if "twisted.trial.unittest" not in sys.modules: + return TwistedVersion.NotInstalled + + import importlib.metadata + + import packaging.version + + version_str = importlib.metadata.version("twisted") + version = packaging.version.parse(version_str) + if version.major <= 24: + return TwistedVersion.Version24 + else: + return TwistedVersion.Version25 + + +# Name of the attribute in `twisted.python.Failure` instances that stores +# the `sys.exc_info()` tuple. +# See twisted.trial support in `pytest_runtest_protocol`. +TWISTED_RAW_EXCINFO_ATTR = "_twisted_raw_excinfo" + + +@hookimpl(wrapper=True) +def pytest_runtest_protocol(item: Item) -> Iterator[None]: + if _get_twisted_version() is TwistedVersion.Version24: + import twisted.python.failure as ut + + # Monkeypatch `Failure.__init__` to store the raw exception info. + original__init__ = ut.Failure.__init__ + + def store_raw_exception_info( + self, exc_value=None, exc_type=None, exc_tb=None, captureVars=None + ): # pragma: no cover + if exc_value is None: + raw_exc_info = sys.exc_info() + else: + if exc_type is None: + exc_type = type(exc_value) + if exc_tb is None: + exc_tb = sys.exc_info()[2] + raw_exc_info = (exc_type, exc_value, exc_tb) + setattr(self, TWISTED_RAW_EXCINFO_ATTR, tuple(raw_exc_info)) + try: + original__init__( + self, exc_value, exc_type, exc_tb, captureVars=captureVars + ) + except TypeError: # pragma: no cover + original__init__(self, exc_value, exc_type, exc_tb) + + with MonkeyPatch.context() as patcher: + patcher.setattr(ut.Failure, "__init__", store_raw_exception_info) + return (yield) + else: + return (yield) + + +def _handle_twisted_exc_info( + rawexcinfo: _SysExcInfoType | BaseException, +) -> _SysExcInfoType: + """ + Twisted passes a custom Failure instance to `addError()` instead of using `sys.exc_info()`. + Therefore, if `rawexcinfo` is a `Failure` instance, convert it into the equivalent `sys.exc_info()` tuple + as expected by pytest. + """ + twisted_version = _get_twisted_version() + if twisted_version is TwistedVersion.NotInstalled: + # Unfortunately, because we cannot import `twisted.python.failure` at the top of the file + # and use it in the signature, we need to use `type:ignore` here because we cannot narrow + # the type properly in the `if` statement above. + return rawexcinfo # type:ignore[return-value] + elif twisted_version is TwistedVersion.Version24: + # Twisted calls addError() passing its own classes (like `twisted.python.Failure`), which violates + # the `addError()` signature, so we extract the original `sys.exc_info()` tuple which is stored + # in the object. + if hasattr(rawexcinfo, TWISTED_RAW_EXCINFO_ATTR): + saved_exc_info = getattr(rawexcinfo, TWISTED_RAW_EXCINFO_ATTR) + # Delete the attribute from the original object to avoid leaks. + delattr(rawexcinfo, TWISTED_RAW_EXCINFO_ATTR) + return saved_exc_info # type:ignore[no-any-return] + return rawexcinfo # type:ignore[return-value] + elif twisted_version is TwistedVersion.Version25: + if isinstance(rawexcinfo, BaseException): + import twisted.python.failure + + if isinstance(rawexcinfo, twisted.python.failure.Failure): + tb = rawexcinfo.__traceback__ + if tb is None: + tb = sys.exc_info()[2] + return type(rawexcinfo.value), rawexcinfo.value, tb + + return rawexcinfo # type:ignore[return-value] + else: + # Ideally we would use assert_never() here, but it is not available in all Python versions + # we support, plus we do not require `type_extensions` currently. + assert False, f"Unexpected Twisted version: {twisted_version}" diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/unraisableexception.py b/Backend/venv/lib/python3.12/site-packages/_pytest/unraisableexception.py new file mode 100644 index 00000000..0faca36a --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/unraisableexception.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +import collections +from collections.abc import Callable +import functools +import gc +import sys +import traceback +from typing import NamedTuple +from typing import TYPE_CHECKING +import warnings + +from _pytest.config import Config +from _pytest.nodes import Item +from _pytest.stash import StashKey +from _pytest.tracemalloc import tracemalloc_message +import pytest + + +if TYPE_CHECKING: + pass + +if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup + + +# This is a stash item and not a simple constant to allow pytester to override it. +gc_collect_iterations_key = StashKey[int]() + + +def gc_collect_harder(iterations: int) -> None: + for _ in range(iterations): + gc.collect() + + +class UnraisableMeta(NamedTuple): + msg: str + cause_msg: str + exc_value: BaseException | None + + +unraisable_exceptions: StashKey[collections.deque[UnraisableMeta | BaseException]] = ( + StashKey() +) + + +def collect_unraisable(config: Config) -> None: + pop_unraisable = config.stash[unraisable_exceptions].pop + errors: list[pytest.PytestUnraisableExceptionWarning | RuntimeError] = [] + meta = None + hook_error = None + try: + while True: + try: + meta = pop_unraisable() + except IndexError: + break + + if isinstance(meta, BaseException): + hook_error = RuntimeError("Failed to process unraisable exception") + hook_error.__cause__ = meta + errors.append(hook_error) + continue + + msg = meta.msg + try: + warnings.warn(pytest.PytestUnraisableExceptionWarning(msg)) + except pytest.PytestUnraisableExceptionWarning as e: + # This except happens when the warning is treated as an error (e.g. `-Werror`). + if meta.exc_value is not None: + # Exceptions have a better way to show the traceback, but + # warnings do not, so hide the traceback from the msg and + # set the cause so the traceback shows up in the right place. + e.args = (meta.cause_msg,) + e.__cause__ = meta.exc_value + errors.append(e) + + if len(errors) == 1: + raise errors[0] + if errors: + raise ExceptionGroup("multiple unraisable exception warnings", errors) + finally: + del errors, meta, hook_error + + +def cleanup( + *, config: Config, prev_hook: Callable[[sys.UnraisableHookArgs], object] +) -> None: + # A single collection doesn't necessarily collect everything. + # Constant determined experimentally by the Trio project. + gc_collect_iterations = config.stash.get(gc_collect_iterations_key, 5) + try: + try: + gc_collect_harder(gc_collect_iterations) + collect_unraisable(config) + finally: + sys.unraisablehook = prev_hook + finally: + del config.stash[unraisable_exceptions] + + +def unraisable_hook( + unraisable: sys.UnraisableHookArgs, + /, + *, + append: Callable[[UnraisableMeta | BaseException], object], +) -> None: + try: + # we need to compute these strings here as they might change after + # the unraisablehook finishes and before the metadata object is + # collected by a pytest hook + err_msg = ( + "Exception ignored in" if unraisable.err_msg is None else unraisable.err_msg + ) + summary = f"{err_msg}: {unraisable.object!r}" + traceback_message = "\n\n" + "".join( + traceback.format_exception( + unraisable.exc_type, + unraisable.exc_value, + unraisable.exc_traceback, + ) + ) + tracemalloc_tb = "\n" + tracemalloc_message(unraisable.object) + msg = summary + traceback_message + tracemalloc_tb + cause_msg = summary + tracemalloc_tb + + append( + UnraisableMeta( + msg=msg, + cause_msg=cause_msg, + exc_value=unraisable.exc_value, + ) + ) + except BaseException as e: + append(e) + # Raising this will cause the exception to be logged twice, once in our + # collect_unraisable and once by the unraisablehook calling machinery + # which is fine - this should never happen anyway and if it does + # it should probably be reported as a pytest bug. + raise + + +def pytest_configure(config: Config) -> None: + prev_hook = sys.unraisablehook + deque: collections.deque[UnraisableMeta | BaseException] = collections.deque() + config.stash[unraisable_exceptions] = deque + config.add_cleanup(functools.partial(cleanup, config=config, prev_hook=prev_hook)) + sys.unraisablehook = functools.partial(unraisable_hook, append=deque.append) + + +@pytest.hookimpl(trylast=True) +def pytest_runtest_setup(item: Item) -> None: + collect_unraisable(item.config) + + +@pytest.hookimpl(trylast=True) +def pytest_runtest_call(item: Item) -> None: + collect_unraisable(item.config) + + +@pytest.hookimpl(trylast=True) +def pytest_runtest_teardown(item: Item) -> None: + collect_unraisable(item.config) diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/warning_types.py b/Backend/venv/lib/python3.12/site-packages/_pytest/warning_types.py new file mode 100644 index 00000000..93071b4a --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/warning_types.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +import dataclasses +import inspect +from types import FunctionType +from typing import Any +from typing import final +from typing import Generic +from typing import TypeVar +import warnings + + +class PytestWarning(UserWarning): + """Base class for all warnings emitted by pytest.""" + + __module__ = "pytest" + + +@final +class PytestAssertRewriteWarning(PytestWarning): + """Warning emitted by the pytest assert rewrite module.""" + + __module__ = "pytest" + + +@final +class PytestCacheWarning(PytestWarning): + """Warning emitted by the cache plugin in various situations.""" + + __module__ = "pytest" + + +@final +class PytestConfigWarning(PytestWarning): + """Warning emitted for configuration issues.""" + + __module__ = "pytest" + + +@final +class PytestCollectionWarning(PytestWarning): + """Warning emitted when pytest is not able to collect a file or symbol in a module.""" + + __module__ = "pytest" + + +class PytestDeprecationWarning(PytestWarning, DeprecationWarning): + """Warning class for features that will be removed in a future version.""" + + __module__ = "pytest" + + +class PytestRemovedIn9Warning(PytestDeprecationWarning): + """Warning class for features that will be removed in pytest 9.""" + + __module__ = "pytest" + + +class PytestRemovedIn10Warning(PytestDeprecationWarning): + """Warning class for features that will be removed in pytest 10.""" + + __module__ = "pytest" + + +@final +class PytestExperimentalApiWarning(PytestWarning, FutureWarning): + """Warning category used to denote experiments in pytest. + + Use sparingly as the API might change or even be removed completely in a + future version. + """ + + __module__ = "pytest" + + @classmethod + def simple(cls, apiname: str) -> PytestExperimentalApiWarning: + return cls(f"{apiname} is an experimental api that may change over time") + + +@final +class PytestReturnNotNoneWarning(PytestWarning): + """ + Warning emitted when a test function returns a value other than ``None``. + + See :ref:`return-not-none` for details. + """ + + __module__ = "pytest" + + +@final +class PytestUnknownMarkWarning(PytestWarning): + """Warning emitted on use of unknown markers. + + See :ref:`mark` for details. + """ + + __module__ = "pytest" + + +@final +class PytestUnraisableExceptionWarning(PytestWarning): + """An unraisable exception was reported. + + Unraisable exceptions are exceptions raised in :meth:`__del__ ` + implementations and similar situations when the exception cannot be raised + as normal. + """ + + __module__ = "pytest" + + +@final +class PytestUnhandledThreadExceptionWarning(PytestWarning): + """An unhandled exception occurred in a :class:`~threading.Thread`. + + Such exceptions don't propagate normally. + """ + + __module__ = "pytest" + + +_W = TypeVar("_W", bound=PytestWarning) + + +@final +@dataclasses.dataclass +class UnformattedWarning(Generic[_W]): + """A warning meant to be formatted during runtime. + + This is used to hold warnings that need to format their message at runtime, + as opposed to a direct message. + """ + + category: type[_W] + template: str + + def format(self, **kwargs: Any) -> _W: + """Return an instance of the warning category, formatted with given kwargs.""" + return self.category(self.template.format(**kwargs)) + + +@final +class PytestFDWarning(PytestWarning): + """When the lsof plugin finds leaked fds.""" + + __module__ = "pytest" + + +def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None: + """ + Issue the warning :param:`message` for the definition of the given :param:`method` + + this helps to log warnings for functions defined prior to finding an issue with them + (like hook wrappers being marked in a legacy mechanism) + """ + lineno = method.__code__.co_firstlineno + filename = inspect.getfile(method) + module = method.__module__ + mod_globals = method.__globals__ + try: + warnings.warn_explicit( + message, + type(message), + filename=filename, + module=module, + registry=mod_globals.setdefault("__warningregistry__", {}), + lineno=lineno, + ) + except Warning as w: + # If warnings are errors (e.g. -Werror), location information gets lost, so we add it to the message. + raise type(w)(f"{w}\n at {filename}:{lineno}") from None diff --git a/Backend/venv/lib/python3.12/site-packages/_pytest/warnings.py b/Backend/venv/lib/python3.12/site-packages/_pytest/warnings.py new file mode 100644 index 00000000..1dbf0025 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/_pytest/warnings.py @@ -0,0 +1,151 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +from collections.abc import Generator +from contextlib import contextmanager +from contextlib import ExitStack +import sys +from typing import Literal +import warnings + +from _pytest.config import apply_warning_filters +from _pytest.config import Config +from _pytest.config import parse_warning_filter +from _pytest.main import Session +from _pytest.nodes import Item +from _pytest.terminal import TerminalReporter +from _pytest.tracemalloc import tracemalloc_message +import pytest + + +@contextmanager +def catch_warnings_for_item( + config: Config, + ihook, + when: Literal["config", "collect", "runtest"], + item: Item | None, + *, + record: bool = True, +) -> Generator[None]: + """Context manager that catches warnings generated in the contained execution block. + + ``item`` can be None if we are not in the context of an item execution. + + Each warning captured triggers the ``pytest_warning_recorded`` hook. + """ + config_filters = config.getini("filterwarnings") + cmdline_filters = config.known_args_namespace.pythonwarnings or [] + with warnings.catch_warnings(record=record) as log: + if not sys.warnoptions: + # If user is not explicitly configuring warning filters, show deprecation warnings by default (#2908). + warnings.filterwarnings("always", category=DeprecationWarning) + warnings.filterwarnings("always", category=PendingDeprecationWarning) + + warnings.filterwarnings("error", category=pytest.PytestRemovedIn9Warning) + + apply_warning_filters(config_filters, cmdline_filters) + + # apply filters from "filterwarnings" marks + nodeid = "" if item is None else item.nodeid + if item is not None: + for mark in item.iter_markers(name="filterwarnings"): + for arg in mark.args: + warnings.filterwarnings(*parse_warning_filter(arg, escape=False)) + + try: + yield + finally: + if record: + # mypy can't infer that record=True means log is not None; help it. + assert log is not None + + for warning_message in log: + ihook.pytest_warning_recorded.call_historic( + kwargs=dict( + warning_message=warning_message, + nodeid=nodeid, + when=when, + location=None, + ) + ) + + +def warning_record_to_str(warning_message: warnings.WarningMessage) -> str: + """Convert a warnings.WarningMessage to a string.""" + return warnings.formatwarning( + str(warning_message.message), + warning_message.category, + warning_message.filename, + warning_message.lineno, + warning_message.line, + ) + tracemalloc_message(warning_message.source) + + +@pytest.hookimpl(wrapper=True, tryfirst=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: + with catch_warnings_for_item( + config=item.config, ihook=item.ihook, when="runtest", item=item + ): + return (yield) + + +@pytest.hookimpl(wrapper=True, tryfirst=True) +def pytest_collection(session: Session) -> Generator[None, object, object]: + config = session.config + with catch_warnings_for_item( + config=config, ihook=config.hook, when="collect", item=None + ): + return (yield) + + +@pytest.hookimpl(wrapper=True) +def pytest_terminal_summary( + terminalreporter: TerminalReporter, +) -> Generator[None]: + config = terminalreporter.config + with catch_warnings_for_item( + config=config, ihook=config.hook, when="config", item=None + ): + return (yield) + + +@pytest.hookimpl(wrapper=True) +def pytest_sessionfinish(session: Session) -> Generator[None]: + config = session.config + with catch_warnings_for_item( + config=config, ihook=config.hook, when="config", item=None + ): + return (yield) + + +@pytest.hookimpl(wrapper=True) +def pytest_load_initial_conftests( + early_config: Config, +) -> Generator[None]: + with catch_warnings_for_item( + config=early_config, ihook=early_config.hook, when="config", item=None + ): + return (yield) + + +def pytest_configure(config: Config) -> None: + with ExitStack() as stack: + stack.enter_context( + catch_warnings_for_item( + config=config, + ihook=config.hook, + when="config", + item=None, + # this disables recording because the terminalreporter has + # finished by the time it comes to reporting logged warnings + # from the end of config cleanup. So for now, this is only + # useful for setting a warning filter with an 'error' action. + record=False, + ) + ) + config.addinivalue_line( + "markers", + "filterwarnings(warning): add a warning filter to the given test. " + "see https://docs.pytest.org/en/stable/how-to/capture-warnings.html#pytest-mark-filterwarnings ", + ) + config.add_cleanup(stack.pop_all().close) diff --git a/Backend/venv/lib/python3.12/site-packages/anyio-3.7.1.dist-info/RECORD b/Backend/venv/lib/python3.12/site-packages/anyio-3.7.1.dist-info/RECORD index fd8454f0..b98083b2 100644 --- a/Backend/venv/lib/python3.12/site-packages/anyio-3.7.1.dist-info/RECORD +++ b/Backend/venv/lib/python3.12/site-packages/anyio-3.7.1.dist-info/RECORD @@ -2,6 +2,7 @@ anyio-3.7.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvF anyio-3.7.1.dist-info/LICENSE,sha256=U2GsncWPLvX9LpsJxoKXwX8ElQkJu8gCO9uC6s8iwrA,1081 anyio-3.7.1.dist-info/METADATA,sha256=mOhfXPB7qKVQh3dUtp2NgLysa10jHWeDBNnRg-93A_c,4708 anyio-3.7.1.dist-info/RECORD,, +anyio-3.7.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 anyio-3.7.1.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92 anyio-3.7.1.dist-info/entry_points.txt,sha256=_d6Yu6uiaZmNe0CydowirE9Cmg7zUL2g08tQpoS3Qvc,39 anyio-3.7.1.dist-info/top_level.txt,sha256=QglSMiWX8_5dpoVAEIHdEYzvqFMdSYWmCj6tYw2ITkQ,6 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio-3.7.1.dist-info/REQUESTED b/Backend/venv/lib/python3.12/site-packages/anyio-3.7.1.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/__init__.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/__init__.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90dc41d6c301b720a20d56d5ac43cd2637167461 GIT binary patch literal 3640 zcmb_eOK%ic7OrYPy4~$p`)$lC24XsZ8w?@5@e?qZr%iwxn45CdZM*P2RjpgqhR!0& zD6?RZ#YD5ncD5rn^FKzjh!C3ON|{;AB4P)FB5Sgmb8dIFZIneKO-udNIp4i?pWivB z{?^fvweUA~Y}9|%Wm*3wC;7G34Et@IvaD~cyOw8pwzR-zwoP_QrdX4CHbaY>vMp$3 zX>4g?8DyJf8)R7){T8oP=AfOmLkH_Hep+@y7wdv<)(v@}I=R58DHK*VK~B$z&IO+A}hjCb`*}WV=%!c;5a)DC)f$o zze9cqC)r73cgm086gvf{*=gf<$un@4orQDk9Gqw8;R3q=7uiLaWRoz(reK;)!wj2& zSvCuEYz{85OK_Q8hAZp}%(HnjMz_2Q*Vr{!U<+`aU56X&2Ha#f;TF3Ex7lseCok{7 zB3pzdwgh+CUHF)NZ1^5|5AL)3@PIu4#+c!Hk@5TF?|?H7pR!NEVGb;_ zW#s#X>kXX4i}nW58X_xY2@n6`d0hvO|6Vt?Jd(EOc8#319=&f_4^byTRRY}-Ng>L*!#sE?RAOByovXQ(AoithcPmPXAm*_TLU*r- zm^)JPWyf6;ffwmPVq7P1g_OeM;;GAB=|qw4omL@sxg(;mqFgbjRHzd2UZ~cDx}`!_ zV9)$?C91yDZ$^fZ@V1lrSs{J!W1((@@ujd5cse&1csEOnu@VmG&bgYtFThm zo%4YkLRpGf+!2nahnK2l;Z4P{@|P>ISO~+lO1ZYK?7grey=fV`Yr1WU4(yUIg>Js( z#H+e3EQ^4bh-iDFoN~>~J1(t}PR$jiij-B|XF5jyO5jMYge!b=oNgEd!o?GG%Xpl1 zrxN&2b+3yHj|H`G=|`~$@c8@cB=y*2^9k#fd4$Upu^y>Y^&#=}E1QiuycfswlpmBr zO-|r|FizhS$B*k{;3^%@S;n1hW1lELHf%Bl2nbb`hs%#?BT?Ps^R+BM^p9oLVm3$@$R!|xD_B-$wU-AK31gn{SN9qQhOxuv~jZXLy_yUb$?-@HIq z*GagAZCe!Jlvh#Ly0>9&Y46@V!TO}_bp`_gr%BqQ#xR4)8}%*W1sO%^yxD&(5P0OG zr?Vx;m)t3#V08zC>wI;6w|t6Rq(TqxH-Z+VgbRCPb6rsfW%}h~ixZez3xciS&Tc00pw~`7&LU zz!0G;CHZqd;7?4H@_3a*qWh`I5V1nd0vh+f2?7}!o>j#>9Tp)fr*O$F6gl!^-})r=3- zLUvkFZC5#hb^WjDbn!5)J52=)@}BiK)HfB>rpjW9-!lqwK)knApXi0s1zM+n9V ziUdaqjuA`{949zIaEjnVf|CRv5PXDy2Xdb53j`MlsNAVD1ZN3OBjDyH$(|yZCYV7e zG^qu$uMu1&BBd^qJxg}8x+=~6s41KB`B+53U5$4 zop4!vnYwdF=`=S5j_X!b8;Y;Cunz8G78yr`vJ-FWM|am7Ou~?1SpF4y()r}2q+mtA zU0^y{5)~#t{3W$D7N_b(I5O(fo%QK!l`2xC#ZA%_m*}@Eb8`e!9wMVt-J8r+S*{lc z5p7CW4Ot_@9~Ym;a-nb$fjZlj(kixEuQ*MUVg{@p#^mdgEkGMxuoDj`Vd~ z9M)6hW(?@umK^zu;n0QR+x&<_^*7UJfo{e0?8r#BpnxG#w`f7P>C)0v<#BeMH5oH{ z;^X&9hI1=3FgaFQ|2#i;`^g zUxe39wr&5@8vW6-{$RbfuG+cO7wMnwr!0H$N9*K9Dr2WMEQB`%8$CI48!%+qQ}zpM z@&~Kse_3ZXQr+~@Hh5#)(RT)Ku~Tm?gpGE~eqeuV-QUPq_I-OJ zl{CEWwp!c1%zT&5ewqEgXW;AgKRx~C^RGXDF*x?3=iq;=l)eA=SHGBf*8hFF^I7GO m^3T&R(qsQfPkfu6`0J^c>2u$W?0eSoGBfn11$(L(cmF>pghh}5 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/__init__.cpython-312.pyc index 17ad12029732250322c677db85f9e2ba5a199aae..eb02f314b5417433abe1c9bef9334228287115ca 100644 GIT binary patch delta 20 acmX>meN3ABG%qg~0}!YiDsAN6%?kiF`2@QF delta 20 acmX>meN3ABG%qg~0}wokP}s=5n->5*1O=S{ diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/from_thread.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/from_thread.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b694e2dec424e869a8662f8d45993d40c3aa59cd GIT binary patch literal 22982 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/from_thread.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/from_thread.cpython-312.pyc index 3855481dcfa5d7d463463cec5e332fed08cbd355..11032773f333b7e6b946744846d3f289ae767ae4 100644 GIT binary patch delta 22 ccmcbziSf!NM()$Ryj%=Gpl+zNk=rv808(lN;Q#;t delta 22 ccmcbziSf!NM()$Ryj%=G@FYTEBe!QH09bVf!vFvP diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/lowlevel.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/lowlevel.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b67c3cd7f56258cbab6e0b22b5b412df7e7c9d95 GIT binary patch literal 7385 zcmcgxTWlLwdOmX@#j8l&Oxcp9v8)>%hq9%_skL@|kKg6;!;hXnI{qxPy=J`N0 zFdvKtdA}pkGT$0)g}PI3O0>OH#ewjA9BZ$G@d;N1-GZoO9zz-cg=cBxi}f<2KPl4@G-$WsW_Z)(Nzo)HQp$GSre?-dNn;Y)Ze26ASVA+5 zuxv>uk{PS%EKO1tk6BGqnFV@LW0os*ow7tq(}BO`HLA^hi)K#8V>pM#GN0mE0>yWY znyO}GlCebmYFPXfxBaQhrZr8(P0F-{B}`eu=@@iY9%Q>kh*11CRK7(7z>`^$4t!sU zZ5Fv2o-*wgVDI;V{;s$x{jtLVGh~Dp$lH#eON&BS%oaTgJf~)8Y)+X@F(sxYV>FTA zC!lCaUC|PW)S_a}P{oMNP(7WXOfgf6u^2aFGg#B$z-F*hGv*>5KHZt`q~q7M1WlUA zFnl#_D)T8u6*@f~kHvZSjFt?KkK63H18q@@n+mKDn_Ec5lcpMuLWJ!vGOfLII^f(|U>!a+(@*W@=&N<&;Si&z?-B=Hkg~BPX@k98KyY*J<+l2#9iIA!E*@l4FsBqay~0 z@!17n(5_KqL`!DksS!B61ielZk%bHcLqT}a4i%#tivJ*AER)S$#2fmv$cATc&a-#j zGq52KJdy{tiaua?vwKkZvCuDn;2acU(5^fv@>~Ch!1&0PWDXf=XS;?o=r6o8yE z;6zH00eGYqs4}A&%2i5}Wgt<#go&w{xS{ANYAAr0uYf8=k55li#=%lW)>n~gs4CSu z6tOlqGKNRbI#;KbL_KLu3{ zG;<~eZ+;9Yphms^&04wuP*0k1(D}@lYAi0JoZ^aj0YyJ-Yva)=mZstHio(_?ZAH1i zmXhwW&A@ib-YC&sC{Zj9)JwFiUt>AX*t+NW4f**;^7GqB6rB&~l~5do%GWit7r@GA zFP^7nnk7MFuG)4&F&1bn4rUK5A=nKib#<1;z%0&aARsLXx}u~Fs-v9*2f;^S9qlbx z4KSdIRB9noHdy{xnxRUle6q&RM}xw!I1WDp(Y{Rn4J^xx8{VOucWB)kUX#OI0kVTn z`Ke3b-1dXg#mq~F$@VMIymG~It1O*VuWQW6c5)i$Qm61+USrzA0%cdMfV~}c(e5y& zz}-2gSt<)Q0ya^3;Ars?inzyBLP=E6NmPXRNPu`qq_Qp@0ZK*=J1l?Q8e!3L+KX6T zRb2!h%jmSKT2A{oSsx6Wgi$V4O=|NLC>~XvPocMhx=&TVlhzU?id$8UM9MUP5Kz?` zf3K>I2f7EQWmueppMmE1yW~l5X!%^;*Lr)>HYG@|`^r)TBNQV*seUEIJkl)s`-Z3N zr=yMx_+$9I$<#$Hk){`@IT?1c0bpl?SPVg7$uyasw`47CrYzs&E9%7)6R)08FPxcb z-1;*(1dCVTX9S?QP4aEscb;24eE0AVzVSyB_U@5m``x2;tnQvmNY+J&@#KsIC+$6- zP68}A5FSXDj2HnKf!9w{L=Dv`ou_)NBy*Sy!@iPw*(b9kED@CxuvikIRf%kLq?Jw* z5JAG?dpH9}A~R|R@*-2weh?5ECY!A{Kh$k$+$ff400DR{_^U4UUvzOxrF?B}qFRg?*MWcUGUgyP>qWjoJy zlS|#KsN9l(3DgC$mzX$~KmCM-1!6Xp+e^a3(i%5GUAHrCv)(dq^<5n$kk?7F%d8+3 z=7*FvPHqS{#oC=9E|dqUSPub5RBPiRf4IkppYN19x2SAHM%;uIrh6sQ3GUf&&O!g*FmsGca&{$F+mJJ945| z`nahB%N_yBSVeNcfe)89if$FmCqFB0dLD=zZtN6L3>;H7n-YW{Sx*ULQ2#@q6&+A4 zlP50k(})7fZ(Kv*YZwR)+bFZ&hHCBXx1h}?vA}q|Zu7V%-@FsXuYw^0XhB!w1HFx( z3@qD%_LKm+XqIke$hJUD3rt=TY>2_k{BEay#V;M-y!|+PP5`=GhOX%^&x~GfyRUc; z?udRQm6hvFAuLU1n?W=Rz%i~}O;DvTyQh!mTFDf2g3sl-Iy6f5LjijQy75g|iM@!$ zSu9YIxIJUXpk{d|Ag4_)*cvkdJ+**%N@Rb642TM_H%_N}Am0|+XdB424cym%I{TB^ zT-(7l-@z{)2ST3{8K{9B?@w>^9L&N0z`=Z|=ljk1mX6ikcXxj{y)k$=2mf0R=R;kq zH}Bqj6zu;@BCX?vCK$N2=_7$%`R49>Qy)Zs7`->KHhgZ)_syat20R5Kx;+LW=FQG7 z^4{(fL(*RlL-FyT0A(GD&`5C#oJ;C%nb0@&k~rJS;St$10yTQsgle}B)1(_ zDz{iAgx-|E7eIWo-AvrLNf&Gtg@BKXj7J5iGq&(5a)Yo0odu6YEMKjWVlP9#f%*=C zP$?3U00b4KgZjJ3ataCveL`Iup>Qq~UhzE+cHKLDfB#0`Xs&Pcqv!r={4dA<=K995 z^SNW^*ZVH42dBWY?Hyk6-I-dA-i_v5NUzb6 zyeYs4oTK5X;})wr0o-3(E@{FPZci#Mr@9zOr@7iH*N(y+?2=F$iEh6xjQ+}VqBI+4 z1veWkuL*9@gRZfWr))pM@?l8kYXUhb$07$_ttW9Xioo-gdKfHdF#mg-`MCH z%fWx|SV5Hh9r^aojrPG@``~@^r`exm*V;!5KGM~@;&=yqC5epv#pMT=S59yEdUC#= zd*%n(A7*pDeNT4vAj21%UeXReS%7JR?FCnPoEJK_csTWObLeCz`J~f*@)_xqeFBtq zkrWyX4)Z>!tU~GvqK8`J^PY$%9PR|bbo-CkN1QW8%?2ex!Cpv#Pzn0QR-An%L#9AP-=8AA-#GK_Ma99%L@7%w$acE-d%=o25)>qB$ zr>+1ungWKfV^^Tqa$s(A8pMh%hACr-Cp&QZt7JX)xZktKVYbBQu;)ha1ZuqU(J+px!ZM7_SFyl|oCnL6Yk|R)6f-ln zJ#`_0+tc60+9fO+5;1OLvB1TRkD&N2`AifYF2G1j|MIyfhfZu9n#>)VT%K5K9b1hNpnlV0q=s7w7yWpoO&i+|5M?w2M8mqoe2xIC?kx1jh(R0r&Cc zb9@87qFnFC`XJtmOsCk5>4ifIs zBaALGxZ$?kl`gBP+~jl)|o`_A51y48myAMQ2LWLquvoGPSJFkYQAerN#9wS?>Blexri;MvahPTGwK~azdLHH%< z`**x#x#j(5;`s&H^Dks{os9m14E&nxdm=eEq_&*YcIWydY4 zdhhiAgAn>JQ54!BI1<_bOk!sNo0}d%=)Kdw8onFeBv9RSIiWptZ_nL*n*^$xt>*<{ SWaYIxzw=h~KMB@3e*Ooszihhz literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/lowlevel.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/lowlevel.cpython-312.pyc index 34cf225ea8beb70b6009725405c6c3f03fad7449..e85045a7e4ca69741515dc7b383f2a7a211578d0 100644 GIT binary patch delta 20 acmaE1@xp@pG%qg~0}!YiDsAMBl>q=hYy|26 delta 20 acmaE1@xp@pG%qg~0}wokP}s;FD+2&UcLmG< diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/pytest_plugin.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/pytest_plugin.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c3948cbe1b8069e71f08a063a72e94e53c2e44d7 GIT binary patch literal 7688 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/pytest_plugin.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/pytest_plugin.cpython-312.pyc index f2d8887be6a3cc35786d51090b53f563e4f5295a..4f100e4dd6e474844a94076143cf0e1435e3d34c 100644 GIT binary patch delta 20 acmbPcJTPsNewNLmt7B}q?2?V_MEeMy9fedV+8ZKijWW`d-5qVW>QC-2P z7Awc)O2kUklI3JARZhu!)k@bgj?SC5bwJMvsxc6f5WcRU-o$JB(hV>yldTgKIZjcQ%!L_O93DcZ> zUhxwrsb$fM#r*i0W}Ur8IVh=fl-qYjOmLH_GEbv!)7CAs0{1;9X`Sk(*L>NmnI7YQ z;*9nx^Da>9xh2I<>(tg++oct#=;86;vx$6JuD&y~xOccW2s8a;(RP_IYka6Upm0w*Z~L!6R$ zGmKQt3(O|d4OFLF3sXsNf>7Y|n z#HmYvrc0PD;4}!e4XNo-cb3di(=+WVnRYla!M*O(>K1F8gszw|CbtlA275w$5Vz}z znK$gvw833QDh*HGa^^L-O#uD|BL~1ewVQ-f(}fV&MV-mG@`0lYlm5-B?QqsUP^nHi ztw~5qaArN^gVYSy02a4I>W&MCaBOOk2RMlXiFmg&+k{YwOii8P4F*Vg4uLJ4IWP&B zB2jPxSe-iKlAN2j4_QzN!#U&Ruwuq7Gck=k4f z`NPvdYN$L8EeH7n5S1osOEYaPpqmz?eo6IJVahY0$Ge2GD)Qi}Ad2|!pDWHdHCC+J z7mKHuJL@_1;tP()tnm|$GYcmzo}l_HvyCD!{z4Iow^(m_Gmd?@aPUwOQpv^v%~@Jy zZV@TqSgq?l|I|^>Cj5{=*{fW)kLNhQF5`> zo*nn(b??}X>K{V-6DgTk3n@`CWn%YT;pgo~$MDy9M7Va?s~frGJK&D@w5R=I?FpD9+Upq zZ43b&v`f5p2!zYqHC|Zl9POrj8eRzVpU8rMmM{8FQ2U)_Das-&A5v)ohCyX*sZ+aA zt3Y#wD$yNVZ*UHM7u?BW4Qr*eQFITNzEe5RbT8=1?P?kd-Du&4A)U3bT`ZwBaj*dF z-9i%6Vfec{VOm7%-6*sB{o^a?BRA7WZlz1hYKafR$`$}c{Ye0y1Yt9P|HaTvASx5{ z&CXc2RDV0<|HH&ihXIzGJcaxWsmc2xeu)4d5Ry2kgvNiY)6=%0@jYPh4E)_8m=@7( zHPKRV@_GCV@HDOhPg5u}aw|Q$td2_O`4rEB=KQXc;Vb6#LZL85+FuWE@EolAu^_Rs z?fGiOaV**V=KDK7JIDf=aBSws0*-L;EeAY!1TH)&4R#9ETR^CpRAX@%FP67z=fcazExZ>!PLwE4XMz zenNaa!6P?l0KJ+HFWZ8~pbLAJ^w2krX}p-X554c|cnzoL;li2Rh!eO$Vg9$HMaGH76Eb^pk{((;QC`a+ls=q6+^lHR$NF4)IHo1I;|9nb+4s`RBD^1@GDr_3PBnRA+(laU@pbL SUWjXhaeQpOFNVh?2mS^;=pbVN literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/to_thread.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/__pycache__/to_thread.cpython-312.pyc index da4466ec019699e93df5e4dd9e1404508f0f30ab..24dcba9b1ad0824a4e8c9c8312d0bcc615df4919 100644 GIT binary patch delta 20 acmZn`YZl`^&CAQh00ioWN*lRNq delta 20 acmZn`YZl`^&CAQh00d7W6gG1I-~s?L)dfcY diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/__init__.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/__init__.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ed58217ca17efed041621138d3c29fc850c1c18 GIT binary patch literal 198 zcmX@j%ge<81nP!LnIQTxh(HIQS%4zb87dhx8U0o=6fpsLpFwJVrRrzo=cekX=T+#t zq!wqFx8=sM-+XJ_W6>pLYTXQ$?+=$EDDmFeeXCg~ScmSp7T8S5Du=@(~~ zr0Ny`6(pvo7V9VGRc7Yv$0tEF6zj*wXXa&=#K-FuRQ}?y$<0qG%}KQ@Vg=gD2*kx8 O#z$sGM#ds$APWFVAT%BT literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/__init__.cpython-312.pyc index 88d4ad55a10c675bebdfa61626a9fb9b4d479a83..2ed58217ca17efed041621138d3c29fc850c1c18 100644 GIT binary patch delta 19 ZcmX@cc#M(zG%qg~0}!YiDoy0x4FE1`1X=(9 delta 19 ZcmX@cc#M(zG%qg~0}wokP?*TQ8vrwt1v~%% diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1ccda236abb19f63f912c5bff3285a58b528092 GIT binary patch literal 102923 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312.pyc index 19f93f91fe802d2fa8c3f6c20283c6f91f7b9cc7..4e8f26b392168dd8470eb9ed6610f07fb34e55cf 100644 GIT binary patch delta 25 fcmX>-jqUU_Hty5Byj%=Gpl+zt$i0=D(V-gvUh@W9 delta 25 fcmX>-jqUU_Hty5Byj%=G@FYT^k$WpQqeC|UXQT%` diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_trio.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_trio.cpython-312.pyc index 19edf9ad9e1d2849894b687f8d8bf0d7a042f7a4..815acc214b5601ecf3229f513d55cc910bae8e65 100644 GIT binary patch delta 22 ccmeBL#@w}xnfo*^FBbz4s2eJ6Bk08H-(SpWb4 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/__init__.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/__init__.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..78b221cfd53dbafc26da55af764c111f127aa9d0 GIT binary patch literal 194 zcmX@j%ge<81nP!LnIQTxh(HIQS%4zb87dhx8U0o=6fpsLpFwJVCF*D7=cekX=T+#t zq!wqFx8=sM-+XJ_W6>pLYTXQ$?+=$EDDmFeeXCg~ScmSp7T8S5Du=@(~~ zr0Ny`6(pvo7V9VGRc7Yv$0z3(rRvAWXXa&=#K-FuRQ}?y$<0qG%}KQ@Vg=gB2*kx8 O#z$sGM#ds$APWFoax#bj literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/__init__.cpython-312.pyc index fa6a3f8f9785295002003ce9e9502c10f722ee7b..78b221cfd53dbafc26da55af764c111f127aa9d0 100644 GIT binary patch delta 19 ZcmX@ac!-hvG%qg~0}!YiDoy0x4gf9I1Wo_| delta 19 ZcmX@ac!-hvG%qg~0}wokP?*TQ9RM=_1uy^r diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_compat.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_compat.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76e721bad222be107b9ad61b24aa91316e14361a GIT binary patch literal 9966 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 OM!*YK`TqfEJ{tJ| literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_compat.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_compat.cpython-312.pyc index 14389ba2cc23ed6d658a3d52063db7583a1dfbe4..5fc25e08236ae0db52495ccd207ac8fea5afc859 100644 GIT binary patch delta 20 acmez4^T&t#G%qg~0}!YiDsAL0R09A-W(8OP delta 20 acmez4^T&t#G%qg~0}wokP}s;_s0IK_aRxd7 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_eventloop.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_eventloop.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0905f0054b8cb13c8f2e0f85dd3fdd121403b7b GIT binary patch literal 6027 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_eventloop.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_eventloop.cpython-312.pyc index 8e54324500c9a9015cb45d90ddea2ae4d8ebdfb8..ce7bb6b729c9a82d0a587f872c42e4d4bb6eae4a 100644 GIT binary patch delta 20 acmbQGH%pKEG%qg~0}!YiDsANE7Y6__&jaTG delta 20 acmbQGH%pKEG%qg~0}wokP}s=LFAe}V+62h} diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_exceptions.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_exceptions.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d9d6a8695221a25d7c1cc84f9f1a531a9a317b52 GIT binary patch literal 5654 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 E0CC2EU;qFB literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_exceptions.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_exceptions.cpython-312.pyc index 8665bb5c9dd31114adc2582ba7168d68f0ee83ef..40f6fd140a215d90402a7065ed1afd3a44ca835b 100644 GIT binary patch delta 20 acmZ3Yy+oV)G%qg~0}!YiDsAMRDGC5H@dSMU delta 20 acmZ3Yy+oV)G%qg~0}wokP}s;lQxpI>`~_bC diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_fileio.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_fileio.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..92566c4ffda729e128e86489827b9f6bde90b0c5 GIT binary patch literal 35007 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 delta 22 ccmX>%f$7`?ChpU`yj%=G@FYTEBe!!008*I-o&W#< diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_resources.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_resources.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bcccb4bba4e1209bdb032f71a069e8c41c1e5cca GIT binary patch literal 1061 zcmY*YO=#3W6rR~^ve{oyP!yF7RgoOJp^6Aa)NLz>LJxZCUP78ochk*gmYJm6)e2&Z zP^|Q3J$aYvK?SeUvll_c5-12Bw6{V9OYz{F>}D4yF!|=a@4ffEm&vDMF^6DX7#nfl zV}yQ)L$XpmfcI|$*gy_)u!mZ>hOxjYFIAJ`ce*9pDU4c5Edw6ONqcH5Tgw8L9mUJF z^0ho*)yb@+YlUt~&+dxEI*#&|@6*5vTeG;GtlzTcfP3scNz#t-}Cni#?xTv0r)I@9~$MU!sgQgwY zMuWD9QTG>(DZ-lpZ5y{}K)lfjN}HgkHn0O2k*Oq()dm`OQaX?LR|JsIX4g$ptldR~{6puCXEZ^HHq9Xvw6kD}bMr)Tz1 zO4eSS-&Tw3h1J4ZX}ffMovzX^rSdnm{8=skk-;C6zN_Vf;ps2eA=Bf0wTPdct77Rb zzOKAW1Ne{y(?<+w3(M79W=qL{K~J+h@R_(lk-|t2GCxXBQ=jO?C})~;Ay|lMGI0eX zbvAT8_&_`=+-P^r!Re$4(;z&kSez3kev1kt9R_2GvIA3;No+X?7If2Mkz~!<{px05 z@g|SITs)UoXdj9qR)p&5IOA6tA9{o!R*^2qG8=JeEK*#O=-+5FJDkBg8$#95xUbF+H{2<#c7MtD35s zSvv&r;4L9n{|51r|HF$OB%whF1QBlwo0F%!s_q#Gfz&|NtA1a<-+Qm-*?2r4xIWt3 z;c=UgzuaZLTD1_IeviUql9DtKBoAgm;N+HQ&D#F#T2nApPc-Zl}FRo!ObPHR-=~$tFQHq*5wNEmzXq#{2jSQ_5uDGEJ@0 zZ-d(EgPW|-ETNXAUjL<%mfg2sQ%M(0quXe~Y)qZ2;D?Eey<)RD*tizp3q&k8NE#*= zv9@5Cg%zhT&6&-n^9R1%T!liZBBW^=*T!)Zao*sZv@{MDsm|df$hA#9AATl#O%CG* z4&KoyQ#p$k@_uxanIo%;=$5if?A}o72#Xcnpve)FX@oFW5sqv<^paH$;l2GMZaA8`TFm}@Wn9vt@nI*<$34I zDJsdNqut*@)#lPx-Fo1D-230RcE`WJ_4~Yoj(6`)-m3<2d~b>K%HmkN7guc`F1f(h zVXD#RMHz2y7h+_Evl3Ecn09RsH9OWwuj+3;RlW7M0XjSLnChx)%L419J1%|?+2;kd zy6bL(m|@kdYnbneFTxX|N2t_8+}>?p%T>A*?7Ds*udXBJ6WmVPK@hwmJ8uYiL|zhj eO)kA6AD(p223P+$bM~Z-=bu}b|K7sW2m1$BrIfw^ literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_signals.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_signals.cpython-312.pyc index 4bc28dfc60a33b99f86d5edd9281b753eb22266a..a6aa146199b98c5850ec144d1549ccea20ebb28b 100644 GIT binary patch delta 20 acmX@ld7hK|G%qg~0}!YiDsALG!U6y_&;+Rf delta 20 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 Nmbv(j8NZCI{y#F+n+a diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_streams.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_streams.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ec8f9dead3faa006047cc42a355061e94cacdf6 GIT binary patch literal 2177 zcmaJ?&5ImG6tC`{kDia6{RsIcS}q2+^D#pd5tq2?#%Qu3t8qiX4xzVax@M-c{V`P6 zY%@Q{NyFKaf3&_oOwEf{>H%Rrjp3Bcz6T{rY{?d#`@= z`p2=cB7ybNscH9*EFr(*%V1>^M%n%znA^l5j^YttF%<=Q)l*GPJ~Mu{p(@1BnfX+s zn>uJTuizKWqF*vge%UO8F5_gqF~4F~{Bd*KpD-sBq7qhea(9(vFLRQW3&bfF2>zUs zli$eRP4h9Qqz?3hpEKs@!+P+j40OPcJB5v`In`5*i6QkbD?0~$@wr`af4QE-fpZwF11w_tkyiYUhDKkI}Bc_o_e_! zxq_YSfPvj&QOyo|ZdkJ#A!ju!l6gkePA@*(fbs;hd|9~_%F9A$o6!VDv>OQ5qsz+?;A6&EUWULn7ZI&{VFR%<9DnC@ zW+|T0Anpm;3^{Dh(O$+*fkSV&qAdm7ZM6k$v4C;6kvM~ojEyzIo+6QBgc-Ptv>P#p z+Jf>fr0e?3ND|>pba^lf?GW9LCsUZg)YE>Ppgg&b>AR8iXYJLI)=A7`-*ECLi0=*bB5kAO7#2ngJ-14Q5d<%?ge|^7yrNO zp5a~TI$aC_o47fD9*D6gE?QQz3t(fG#qmgDz20>_0Vf-m-|F;W45x1gEtsY(5>-2j z7{_l&m9rZhE=ye45ykm|tO(9%ZrPmg$GADj1Ypl`f^i&394Ap+8BE{g-b=V7z>yHb z8Iue~9H8>-Bx0WGr~T0BdTfpxpui7AbO%UZQxxTAGW|Ot8|2f{FU0tX82@B4iuxx( zQYOX8&kk%A4&BQ2b3`pZK0^DNrsxm%9+J<;+4ug+!uz%c%DxI*zl>(5-`r*P@Z{^M h`NdK53Yk~lRr=bLazT+%C3Ac(kwE;vvnY|J{0skuLaP7( literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_streams.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_streams.cpython-312.pyc index 4e43f2ec4bad589fff376619c5594f97c9ac06c4..63b46bf3a26dd5d96555a0f5184a7dbc7a6a83ed 100644 GIT binary patch delta 20 acmbOrFhPL(G%qg~0}!YiDsALuyGcx*-*fMK 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_subprocesses.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_subprocesses.cpython-312.pyc index db0365ecaec9b3e1309eec75dda6cc60a17101d1..6adfeea332208dc39f697ac4a5bc9f303e3a9a49 100644 GIT binary patch delta 20 acmcai5Zeh?gN@x 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tasks.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tasks.cpython-312.pyc index 7440649b2a47d389fa66bac8995eb4f9e6077db2..471767bc450212d885b10cb9e45934fb5b1107d1 100644 GIT binary patch delta 20 acmaEB^VWv@G%qg~0}!YiDsALWk^=xh#svca delta 20 acmaEB^VWv@G%qg~0}wokP}sj^^sj+4$Og$QlE3&<)liAfz&p(RQYk2sN1RE$zZ zHX@W%Vx_pCM4dz>SxQz?rBo$dO2e9D%1)-zSL&%Q%|d`681(Ti?9NM2mj zZC{^ttRQ*SaxJF&9{Y--L?jr!VpW-C=)Prk*?wqqKN!>&b>?bjn+!X`{+W5p*K}TY z4ac4}Ld`D|V&8(S#F;!2h13}`3&KIWLIZvMk?NbVTZ=!nKa(%Aot|BxmGtc#ET=9XqkP=+y z?7KQsY}Z$8^PJ+BEv4q#cWM?)LDZhJEv9&Lf`C#zGF6=cht?50$R9F@!}5wf2U^ra zaX8VhS1qMyhig@IjPBe7O>XrZ!hNQ=dc_J=Gc9f~Td?*jrJM|RS9=71VR(*X8N%HK zeby*k_Lx`mZP!u^z{33y3)j5a+rZqrsn|=E16jlKgNSVg$*#hIBo-yyz>s^K9R(cz zX=_FJ+V7I?$xHI>b|iEc0r?1$1Ssv)*-QC2xIpb5VWV3|DYCj-igv~~2@qwI&-=IF^ax;DN!d46N^d{cd?dE~;1 z^j>OfbPR_0){6xg`nQyE7?NABTwalWnQXx}#OQS)Izu@7r+};yAM1wrmZ;z5>r^Q= zwnPqd^AbI5b>So-*NNYC>2o6s-h@t43}b>X59f(WqVA83*$iKF9KL5pQVqyZ~gkMr|F5U;nCHz56^yJ zJt}`#-WZ-}W+uMK9&aX&vu^^Y(Y-EAFiM3f@__6y1vxD0AGc+%6LuT~px8=fotMRJ z)~V2*BTHLra50kixj0;!1U8P*37Thz-@meY_2JbA_nL`g2bDx?3MI>ctP=1CjQhXC z)D=m3sP{gKn2?CKyMEf;4Jgr%W7ml)%`~Kwwt1lu&x=xaVTY>V$)u(kj?OtG9$c@T zbRyqGstJeeH35TyzRv{V(kVEV7XU1it)Y?C!o$Mmkh(FXHU}ry#@GDcFRm}HE&O5h z)7+oN{y6q&-=BZf%wB6It{pTGdhuIWy(R$p0ipXm+3^~M7i$T^^LNoj8v2j2BuKFR zO2Zv>d|*NAp`L(iuYCtZ_)!4=ApZjACB{A&f0}rHD?P9sBLlgW^uM_T#J979bWfMW$C91G}e(D`!ELV~GTlWrtVN!WJ`XBFv{Kyh5VvHB$I8 zE-5n}^akjcMhah~_5zl}hT82LZbCI(=k?-}M1!|Y{61Q<@jD57qJwFdV_Kl8T$+$Ud05^`cYM+VO?Pj5>Z z`3n78Cc7;o0v^mgw;dN~f@H_GlLAeV?Dtz~L|exs`MFkx%EwzVD!)NnF!)%q zJQR;I;J^D}5fffVEAhyL$J?kVhzE0_~+;j@eXBoeg~GD?o0}Q^GjsZ97qhxvoZqxq?A#McQX+D8d>1oWMq17PFr6&&~ls zfY$)wCjbaD_KiK5+05iOGWjQ&!t!gMrBD3!%+unlPtzBh(#3-|#rSv!i`yH3>@RMe zNcmq;^bWLYKZjj@6+~#nPsTC`_B`_wsC3Xh`?jO#j-!ajw{1pfl@%<6 zXxKTADIA(2zl;scOgnyp5m6{XFtmm?>v+%^nfpWP0w~WA;4L`ju&AD2q(&YTHdDoo zRPjk_YEzo}t2DL4F^k?lHbH+w$E8QH3ChmELZ_m<@Lzu3-0aqrHfJDV#Zix7vp83S zl7;OdS~Ez@)@%nZ#e>YN)w8VyOK`+r~BN2L9 H1lWH7rRIea literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_testing.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_testing.cpython-312.pyc index 9656ffcaf904ebad77d0f8650f245844d75e0022..a31b4541d16235efffc09e9e04893b57c9d357ee 100644 GIT binary patch delta 20 acmbO$J6D$bG%qg~0}!YiDsAMR#0LN{+5|-a delta 20 acmbO$J6D$bG%qg~0}wokP}s;li4Ooa7UpV^=L+l7Mz$2S22x5=?iVi`{yH#_CD8UA*kwP>5b`ga#4QmHr0yCFt`mz`l0zC&Rg!QlJ91SK z|A|J@lqJ$gRnyUqT2*03aWai;HQUHlbB%m8-x#P4GzP1KK$EbN&QN2xIxG>YUfLB4 z?EhP-xnB^%(!eDRTng2KMBJ2^6~mcK=zyi(lEO39En=SO&9}mQ^j5^My3HE0vdf9; zcA30EtlToO^0(yZx4ZYrd_NxCTVxml@u4O+SA zUam}2KIePQ$|=vM&i)gi2Pj@SVVHB&wJP(}ov%1{todz_(u=T}(`+L>09GSe|Q z*JeGo**Kc%IS!Z*9if~R2TsZulC7S8r?0R?V@^zC|@}HrnB>TFAO?aCj9| zSnIo1S@1J^ZwcC@;0NkS-Opk0TWS4EF9H^Aaw}5)1Idp@ge;O61JmUEK0-c_7v)Py zV6drKa!kjhPqr?~i_*NrrB8PF>6p+`7Uj=mEAdAqro190h>xx++DFPF`4pZh_Bx5Q zNhvuK4C#7bEw1Zi`KL7SIWj9A7IK(RJIkeLXg>uEPr}ro$Q9Bv6r$Ycyz((RE>eUFR6t;JKIB z78o!My3X!z_(b^|j9&&qZb0{Ji9Fc5{|j^F(DawJ+k4-m>+?)tit<6IL2Z7rR&qJnJDF~$O1WTkbEGc9?>eywMpNTB zEDMB!+i1rjc8Zvf;P&fO+sqMVno)v?t`a_^poZ|rC~tbMg$S58oEDAa337ikc14 z@WEhM7_cAxd1v2?pS!%7a1uC1DPnC487^GSgefA*?yws+@uKLj*C9&XlxD)K$gB)Y zL8fm;JO>xWxDX_Tu+B^0c9kPQ*v0U=L>{WSP&UZg&^E|s2VhS~zo`i6>>w-zX)8?h z91o+ZL}WSylW&)Q6uG^Moyd&qMWePt^gIl%lMIm|Ib4GA=C&w}!BUL3Gh{dMd*<6i z*qB5XWk13?AuYo4>ym^NCkIQu0;$H7-vhubkju(@WI-w^#^>vSBMdsxv-YGGY5k*? z3nSa5Rw>M@U;!ZK`Q7qtQqClyKahXl%yKS`S?Itmb-=B9Q-PJy1C; zGC;rHwfX{>VRHy$ABvKxyzyS^3dku97^8_eHJDk~W)0iHf)1F2xafY7*ad_4_@jY^ zA^Z036p#0kGz6RQiZi7ItAKX|>S@{z^KPd1SPZ`%*kS1YU$;GmXWoEriLC8NWybFn z4_$lpZgFC@IPt~UYVpwR;%h7U*PcBX+J+f*{NB*$A4;o3yB~~JR$h7i_UKRlGQBeV z&ckgl|7LJ41N+Z;9(Y|Tj*{PvA5s1+9Z_aVGTQ@do$p2E`MI#Bp48&P{oJ|plUlfV z{yb=jFl2++;W;rXrPi(f5Lbud{Z1h~sx1PK=@7dyIYNaA@{yRF;i!~ldvO7(GQuXX zD?t~i5a}UY`)y%phV>Og;Ijejrm(}f8RlRS7BJ{B?C?B)7rJF~Kk@d%5@Wn97%JJWf+Abg`m)y3{@-b#NuPN1z#6X zd_+}Ntz|nt#QY$Ctl8FkvoRgM1t|B+P*Q`6P&tPWc26i(Xih7F*^lxSS9ca(5)$wB>j!-`VT(BFSGw3NAHlM zUz39mmDF9OxT+Md&3~nQ=kJM^zE14>XJ+`?_S=~qOR4+$;X=L8b~ delta 20 acmdlcw@r@wG%qg~0}wokP}s<=%?|)M@&xq& diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/__init__.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/__init__.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..51da05242dfe254a1df6d7d79dd0f18bca2c19ba GIT binary patch literal 2201 zcmb_dOK%iM5boLazQ+6TbN#}`_`Lz|VnYZ?h=XmSgoF?aV+FMmjmA5TJ$QO%)!l1Y z4hTdJ95@8!z%^IIC5QZroFaq*Es-3eDB?zfkR?v3nptBLCx=KG$zQ#?s(bsZYX6YS znFPMcnGrWg6Y?t>+g~zLjOX7Qgxn=7#37F15pGb!Kt1NgXgriHJW-7qgePeVB5|4q zZ;O}VCN;rMI7zRSXK6OnQ(lheX`UBofw$2%-cH+j2kqdUw3BzyF5XSMd65>wwP~-1 z_tIY8NBej`?dJn@fbXOG_#hqRLv)Dmr~COZ9p(q<0X{-U_$VFaV|0v<({VmQC-@|t z4C`dPDPE$bP&d6d_(6J*AEJjsyVaZKWm*P3>*Tx&KTHqv89Kvf=`24&kMN`PXc)_T zZ}MaGSg04gxA<{-9P~E!w$pwBF3RZutrN8vVei1-yZ4*-c&X7;c8TrSeYjctPLVGs z2>BvL2z`&vLH7G>&gpWx@5iI~r5lmljZSepTN<~A**u%G7PjNJr&>rOR_b|*j*_8U zZQl=+t=zzuXG`NcH!mB0^&FEyT~wLQonD%E9KobK->5O2KfSaZR5#ES+6AxMX9wuyn^_;QOqqn6r~K zxZ(@ux%R5ZK3ly6!E>z2+)dWpuK(}hW#&80SaDA*I%4NP6>5u|yVA2wPn6WY&`|94 zz4PeWm0*T;w0vmZZ|rQpH+Nu9d{?2Ky}_tz7XdZ3Zc5OWnOcxgInq75OnXN*$q+3lG9m{;BoOe4 z5h;WW0=z&(WD)RH60HEGHqnpz0Kz_m9)w-1{f^_1&NojqA=Sg));s|JlGRoRx3 z3FN9Q*ku8f=*%mZPHp>4NXF%sEp*DVY|n$uld4ezBBH~Y7EE_I>@s9Qr!5%{wfs+P zU|QH;^8aQ<=PX%YtwpWMWJr-tS_;}s?hpn0j7t#HUCQG6q6e&s8v1+6!WGRf&j;+ zvVd=+?^Iw^H!GgIS^;L)1OG^QX0{?><7t@B4SS8rO4wNy=&XumxxT9`t6XcmOrMCl zZ%%v)`{CS4{AcqlZWzYTWaM{3z9P@aL~HDN>cx^ljGo`f_*Tp`Vp{~@1;SRb9nII^ zIhrrVh%x#LDLo;>TWMlkFdmZ6pOA?sWa?RxBr`YDk5cAM^KnP<&gM5)?q0id?Lp7v zgN~^`Nz53#@$vP=TiuUS`CIj`y>AyDq$Yn#&Hk90{qDrW)X7H!qqhk_TL&I4^fzY#u(7i0Kg#$NI*jpQI}H0TBx}>HS3Y#Ym`K$n^&4#Xhj9L5Eb2` zlJ&3@mEE$!Mp%ieZj};^@M>Tl)A1edoJL+JLH>XQ7B75VIL=Mh=lfc{_$vx^nw3}_ zCn-zYNh~+&T9#j3TYrnEEUU7nOa=4Ph^MV20Mp7Qh`h=C&St;MVIhz6yG&$s(;uHb z9w!&k(Hh8ygcFwr#MJ@{!I^<}pu2j3%>IIWkb`xDn}HSR2m0~kJU8Dr>V>SbD*Lhj zHkV0H_d^rS5&C&)%o9r0cw zIucB57S9^`Nu2V%)Q(upwz;r(TW!Au+tZ$irJc68-Hv6-Vjn;%dAMaKvCUzRxB_Pd zwhvB7b;Wh$@A@sKJi4XK_S$Jf>HJBmRIpGgWS;?XxYC zamz^1`)klOukHD~i@pDWecRqhVlFV~L2X7iRXwFtGmpSjbq=r6@qoG}P!P*7)UC|& zyqHDY^RSqrBV@aP{ zFOLa+MLZ88_LW31%uz~D$jZ;i)dO<*gj_ly%RlL*d+Yme-l4ynXXwgT+kX(46aoJL Dw;RIc literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_resources.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_resources.cpython-312.pyc index 994e724ed27de1f05777efc0ccbc555b334a394d..d43b3259bfbdae1376ff5697d9f28bec5bc115dd 100644 GIT binary patch delta 20 acmX@dbB>4mG%qg~0}!YiDsAL;W&;2<76dK; delta 20 acmX@dbB>4mG%qg~0}wokP}s=r%mx5EAq5Zs diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_sockets.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_sockets.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..62e4aa5adef55fcfdc32feda8ee0d1a508ab2e85 GIT binary patch literal 8259 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_sockets.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_sockets.cpython-312.pyc index 36bdc863ef086d0f8c391a230f0f63e202bc7273..b07563370c2c0d21687105b9548744b672eaa7f6 100644 GIT binary patch delta 20 acmca)f61QvG%qg~0}!YiDsALGE)M`b`vo}w delta 20 acmca)f61QvG%qg~0}wokP}sK1_m1d diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_streams.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_streams.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d1508fc69ef62149a24df00ebc75e22391d66d45 GIT binary patch literal 8531 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 P;WUmt{oe}Sg#!K`Ge@=G literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_streams.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_streams.cpython-312.pyc index e2e8cde3cb9bce6a08daa5648b75b76a8cb9828c..c6035583d871bc302dfdeef36097ab151c51c37c 100644 GIT binary patch delta 20 acmaFp_|TF2G%qg~0}!YiDsAMxrT_pyoCRwD delta 20 acmaFp_|TF2G%qg~0}wokP}s^*_pOI@;sLA zUOT(u63BsnklzqR-1rxqI3U4D5l9e0Zc7#<$cgHCdDd|r7Ge&y)%EDA{<@~7_S<^B zg5WuI?u0#GLg*K{NS=Je;E4d{KC+O79pvH;#*$YYrIQPPRX1-c7`cT`G1TOol3VVS zLtb?%ZnaYdK5rGAnp^MGL%!%V+?mddJKLGXNI^@;D&0d?nbsfXqNts@q0+28k&7k{ zs)pzJ!VtFa@fBD%j2;)vFojFSrf-3yxMpv7hQpuW$fLf%hn{(pa(}=~+EjwM#i5`# zsY&f^x+WMk+@K*vYt*x%wHZZ@s2cs?$*;iNM-+9ig*u9bJ2|RaHA}gtK8)=D=1?xU-Qx>*1$%^>xwg}OPbypOIP>ni)` z8pCSyP*7ZDzDYSBU0CplA@SQ}zUL1(*`|yG_QYQ&ZPPJ0Z+H2i*N@D#RliTYu4YQ; zlScvOON8*G2Fx{t0mC8wI=6$H%IMPyiJ#ksEy%zVwnM}wCGm!Y-mwMODydnUQ88d% zJ2a9?V)_=9*<`#@>y-!S0S;77opml#BG0VWrti|$hIglRner{+_ghzeL7lUUzQ1LA z8?8md+=4o2ZBuW%<=DMee+YHuy{nx&-{Q8QXZv7aY*5}ZyrJ#4jGo!jcM+gm>kk8U z2Tn7p#UN^ORfq7r0OWV{_g(Z4msRzJqT(s2K!%{|@kDAROaRd&JlBdzQb$&w!`RiD@_vC*5wkW1Q*fX1A-Q@xX2M z#@g~Hgb%r(ZsN=V9_hTcymafz&5ycWBK&MZnZTMxIHm9|`t3Gp%Qy7-YSBb;tZo{4 zg-N(r14wjlY!=v{oSKg25^Ph^5wMuZTDyxTK+!9K{Vol6&6aRv3Mg}g{|&oq%h#{} zXY95h^H;*AN~_&!lq&)NvBQ!`*h~pq_Ssv5-=t8<9!U(s!xo!d{BHSYfP1aGm$jrv zn{_iUdEa1$8wZJA^*Eq6aC?-I(^T8gNZT@m0RRjKpM%5%bEG`@@ z>P~uM89wTd`*lT)EKhYwvT1Mv-%>K9f~2pJ6}N~TStUK1?%nOK_#Vx+hZh{D5@jIU z^5{@2@NB!(UIxFYmfHL=O(qpU?wQ(x@Wjl*Mo|# zyS_DWBwy3@&jyAQYl^yV`KGQj`8{QGlFUo;h9t*;1f@QMk%fsNdrQiWOF|?$A<5g4 zgr6eWCc~z|s*;mLj?4TpkbCGy^~0a@^G}t0zBb9B!r`&x#tq<}Dy38g-1zVdWyRDE zxbY!WX#60z#QC$~mXb8aDlEr^M5o3su@08wTB5`GOL*Gx65jR1`Gq~&g(>Y)M*9Z@ zUBcmqB0M*Yhk*F{0Ol}2oIw)=7<&WT5w^#J+NJ(bPgh5E7@sH?8VrhnG7OFyqdN@d zsGyvRf?^`8fU`8$4Ho2JvWo64ttg1g;xV4HVi;GCdIM+hmj$u1B;f^3U)ruFeg-m$*e*yo5QiA{h literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_subprocesses.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_subprocesses.cpython-312.pyc index 2002f8a189a164a3f52e6d7b4568b5dd44211219..0938df15921865840738e14fdc55c6a1c64f6e34 100644 GIT binary patch delta 20 acmdlexlxk)G%qg~0}!YiDsALm#sdH|Xasiv delta 20 acmdlexlxk)G%qg~0}wokP}s=5j0XTYa|Kxd diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_tasks.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_tasks.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f809d2ce1eb819a8a331a97d04f3e5e4be3c759 GIT binary patch literal 4987 zcmd@YU6T}7_4Z8nOwY%DFVym}a9v5*NnnPLRH-Az0NDbHVvS)@Gf4IJbl;tAwx@f% zw`X@}iwBadrM@U{;lUU6B^1fK|3M#2uqv@j3@OV}-UbFO!;|OS?)d_tEZ;19Yfqm( z_niB2zVG~IWF$-AIe2u=ximt^pYfynq#{J=>{bYQkJ!XkTvAsmih_8;O;nQdt=2Uw zp^$p2l8#68iVh=5H&f46vh`dgC+Vu2ua8tlB(Axm^|8uWeY`SWpQudKCo7ZnsmfG+ zx-u=tQ|^xX&dN@OF#T$=yP^KuPOp8R5T1rrbeL(TGNTYLCBJq>Uu=xycgEda-&5J6 zkOW~uW>o~-w1PSdR(%^-N0#7|RZVMc zsnuYG6cFBeW5GCmcH#8-bBkxf*Ogcr1-@hQQ1{mvcYPD5 zQ#VZR6%zMxyZ7~IH~Jgg4}8maac8GZ&tmRH%WuG3OU9clST|k7EZ>8jE#a0Tblv2R z=>-;^sr$rQP&#Fxyhj+RC^o4iSkl&PWm~-+^Ho%!r0m2X#ZKG0t!`@-wUsPn!t9bM z)-HmMn!<{0(OrbVDoRI{E40WhW$_k(P!-Hw5y(m(Un#Bnbyli*o2A#8SPT3{>8u|x z_wXs-Uvs=#=@jUbd3I@?dFv(Dsg@co(6@KIc=T9FH~~A{00y(hM9K78j$bmXR>=rp zN21thg=rC(JYe?LWPg*zn6?u((9r-~C-?MR2M}V#euKDthW5{AfX2~>ZYB60c!e5h zyvYPgQOft?@GO9Tx7uzT+=pO4fW_yoq!bccn^_t^7oD1Cx-?id0|1QT;pnOAL zJ33lHmAcs)?`!<>qVKUQVT!Y$$-Pj;Phgt^AwGrR1q3f5Ko!SBTVq{Urq?Kn^w|np zs2hf7)|p|1S;MINcGE>XZx}ylnr<}0^Ee|KBsvl*J~9%To+H=+|Kc!!ZStu$@-U%j z`A(9gM%#$BQ-D1|`+G{$^3sG-eki2S&*4!zzXsS3sY{S#A+08uY9~*?h^^Y%&y@@r z{Ihd*YCBcYSn3(0>Frc6B-78}%G>$?mkwMbc4i=OGb}Z@CgS#}ogLuFw$#E{m_f^( z;eNBR^(*NX5(j@{ke$F2P0{kKRqlI!Q%G+F=+__(G9LS4*@2WpEt#MUff=eS9WA0z zNuiDh0uaIAnk@>ckXoybYey5xWy>&00GaWLi3akXWaAT{)G$A|+9Kvpbb?-(VYb0o z{}I~fdw`rL3fL&O%4k% zfujzT7GE?VufTHD^lWOgYO_{@RNYTQ#UfA}oLMGVw_K*ji${8ELQ=aV7%gMjEHBGM zWC-8)mSJiUFw-v5b1SsvH^F~=n}Ob#Ehv=EbKj!JfK+clF|InU6SNK?+u+3D0(6bB z25k!1Po(Ci7dUP#|H^=-x6X)$x#7(hDA=OETYC5~NV6!n%8dcXJZcRLA+<4QVg;WA zxoE!})5l^V$)VRG;pFS_!9hLWg#r%y2($fD-infAsG>onWT>JE1^O7MCMqJKCieyj zup5X)NjL&b0>R(NKd+NV2S{e*)BL4B=J!3)l9_BO9z{b8nT(@Zwx$OR zhH)<)=q!xz0|>AH4Osv)j?73{siKV3Qe+lMn7@6lB{1^P4{7AzfC$+p2pP2;;s~;a z5CaY$Cyf8i)PkXbPdREKqG}@BoNjvq*=~6`fOs(=+MpHfc6sxK<>g+ED09=nMD`|X ztOH&NeLryM{207+#4|l2jcI*f4{;hGpR>Q~j{*5gV}Y{z(ct zKL&)yv)wz}j+*$fY+p`PnOprJO@XB zUA~xt-1bBAAfah`dz4J=yQ9vw^JHf4gUn9t$i2~*+Zw!~{T|=hP9vt1u^nw3YiCJz zw39>Z2uV%DCkl;fc_{?%SO~F52r(3Tm;(%Z`{{N9X|yjs9*e^}7Dp@+M-0SA9_9dx z#gTRZ#PMoyPKk+#;)MaGPig0sdy|LS8X`cOe6gLDs7@wcXlEn}bMAPULsZH?kV28Z zq{)>q109lSxo~+aPz=14CS0E2j>D6n!BDR@9T&nvg!$JREu;6f%Ygea6G0I#eH>k+ z$k_(A*08>z?BK&f}Suwi3%dVbJ z;=J;TD6eeNi-tYxv3?@D{k_UB0W&xj5k7!+Qc;wr&%B&CAQh00ioWN*lTV3jqKyDg>ec delta 20 acmeBD>r&%B&CAQh00d7W6gG1I7Xkn_H3ftK diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_testing.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_testing.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..346e9df3782b373b0f83dc7838dad4cd6e521167 GIT binary patch literal 3146 zcmd5;&2QX96d!xnUfb(@Zz5VgYD#HX+U`=QgcJ&D6RH9dIY5Mvqsa2wlVlrzEMspH zuX-qI4n;yj;t~#=xK-sp;mCo?5^4|!#BJL}9D3rtv3IkZh5|RdtC=_Ny?OKI_ukCB zAI8Qs0^j__)6KmSA;03F|4A7k^u7jSpSZ*oJmL$cARsMzqFLa5(U%-iAiivta*1Lp zASrmNubG-(Hp`q}^mKpB9P`J`aXyy334hX@^ry@zf7+b(XUrLah=k6#@;xD2+pLJ> zI&n*T#8s&Fy?U>Z^Jf#Ws{M|Gs*slLAP8eSZiYd$Tou#O)y1U`X>6zCb|Z?J?ZiHf zH$xXh<5#03aIVvUGCK}gs=OZq)$nMwm=>-ENvbT_o`;N7T?$#)j++5ZODjo>eqys! zv{szVgRITn5qjUkc%M*W3NA54R{&^KSKKR_MU{;HT+J;2U!t;Gc8hz(d$~QcMCD;k zS9hf$sd7X*=E_4-^@wyFAUm|IB}#Q7)mCT}f7A|O>m8;U@LdZWyA`!PJEoCg8w_O4 zV8fu>G>8o^3|lqh+BRj0;Q%IvZ@bipHvyB4P3W7E5jJj6CpLDPAZG_$~$ z;8r;020LGc)Xn&t1LRIX{E}>66jzdYX%n$}P z4NVpo?g@Ja#+}I_C-{b*2W}5R`J7~H=0^lL*~h^;T+=3i7f%q}Bc!Kc!ygZ+vC%)J z#(Hx%ZZlfb28s(Uo7sL}V6C?UXMy{js~X6`3*qRDtsR?fL<`0iO?E=&8Zc`6kOPqs zhgqIu79X_&IUa8wnqm~(2^P>*NHTtt7)g_QE*>A85Akc$j742zorOMUAKBNyV21GB zLY519zB=DkDrX)?h~pp&DsWaUZVf=Pa{bP!pXTn<@Sj7 zz+iq9{#{~^m>A9K4327^gz8{zcKQi`6a)wrMlPW~^X(C6@IjWdxUg}MR26mty3R?w z?N%4oYdy_{1O z8S>I1ny$(WPeJEw|CwK{)oSOB(W+q8S^-v&tgZ3AKOT3RPP_`0M8@UjI(oly0PVHE zF*~RrR>2ydeBJ=<%q%+xj&NH{2MSEgqi6G@o*if6(6u2OucD1|s#sP4Pq1aBnq~Q+ z+xC#wE$g$k?d6h^Wx1haS!@UP9_+Y+k{JU;TJpWS2fhkcY7hMD!Ch zd*q>5kd&S#NR_T!k}3z&=erX0U71Xu?Us;I$ke&6ij+pQ*e9HR+b?lJ@ zV{ZGQ2o%2}vxF5=73xRZ@$4w7&LOVrZG4UZK)gazrO|GBP*Fswex;RIgV6~Ksu4IO zHJC>kCU}j}9R@E(st#Cbsn643GCy=sXG(ZDqO<_@3d6UO$0wa|LeGPW3+vn(im-ZO zSe~RZOn}FSDacW=!MdJdlz4XWog4|~W94#|An<poeu!^cpnXf*=S#lheOpaBu4m j$ovCx3I0ElOTS3^ot4{bx5Pi>w*>v0H~%Eiaj5x8=sM-+XJ_W6>pLYTXQ$?+=$EDDmFeeXCg~ScmSp7T8S5Du=@(~~ zr0Ny`6(pvo7V9VGRc7Yv7nc;JCgv9F$H!;pWtPOp>lIY~;;_lhPbtkwwJTx;+Q|sS Q#URE>o21XKV3 delta 19 ZcmX@Yc!ZJrG%qg~0}wokP?*TQ696+w1vUTx diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/buffered.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/buffered.cpython-312.pyc index b8f382c14c8d2b98b6c38d4745eae0a6eb30523e..216e3abf8eab9c3547212ee96c704b1f31435b62 100644 GIT binary patch delta 20 acmcble@UPFG%qg~0}!YiDsALGE)D=Yvjq_V delta 20 acmcble@UPFG%qg~0}#|)libLCTpR#Fw*~?L diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/file.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/file.cpython-312.pyc index 7adf87bf52335ecdcf7bb2e4bce2eb8d736585cf..eb41e5b6a4f2c831904a4efb5482edd49826f342 100644 GIT binary patch delta 20 acmca?b=ivhG%qg~0}!YiDsANUkOcrdZ3M6Y delta 20 acmca?b=ivhG%qg~0}#|)libMdAqxOPaRs3O diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/memory.cpython-312-pytest-9.0.1.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/memory.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9e57e9d2cf23a0be3dcc74a2ba90e55ced77cb11 GIT binary patch literal 13036 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/memory.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/memory.cpython-312.pyc index f31872f123201a5977b2106c3e1dda6a1e42d95d..c442d5858c69e9e06f6bd0e49cf5fbf4101f1d44 100644 GIT binary patch delta 20 acmeyB@+*b=G%qg~0}!YiDsAM>Hv#}if(4EM delta 20 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 M06wx|mh6!K3!1+>-T(jq literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/tls.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/tls.cpython-312.pyc index 6be85123d8fe475432387737356d236f3e96d3a4..d69a02d9cb46ddd8625d4c7103f6ab310efc87d6 100644 GIT binary patch delta 22 ccmZ48!nnGHk^3|+FBbz4s2eJ6WdZ;?f&`cV delta 20 acmeyt@`Hu@G%qg~0}$kVliJ9g%LD*G+Xb2c diff --git a/Backend/venv/lib/python3.12/site-packages/certifi/__pycache__/core.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/certifi/__pycache__/core.cpython-312.pyc index e77d8ea9f8fd5525b4c9e47ca8deb3e83f8d4d1f..c50579ca0a5835f277273547a552decd44306a1f 100644 GIT binary patch delta 20 acmbOrFhPL(G%qg~0}!YiDsALu=3.10 +Description-Content-Type: text/x-rst +License-File: LICENSE.txt +Provides-Extra: toml +Requires-Dist: tomli; python_full_version <= "3.11.0a6" and extra == "toml" +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: description-content-type +Dynamic: home-page +Dynamic: keywords +Dynamic: license +Dynamic: license-file +Dynamic: project-url +Dynamic: provides-extra +Dynamic: requires-python +Dynamic: summary + +.. 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 +=========== + +Code coverage measurement for Python. + +.. image:: https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg + :target: https://vshymanskyy.github.io/StandWithUkraine + :alt: Stand with Ukraine + +------------- + +| |kit| |license| |versions| +| |test-status| |quality-status| |docs| |metacov| +| |tidelift| |sponsor| |stars| |mastodon-coveragepy| |mastodon-nedbat| + |bluesky-nedbat| + +Coverage.py measures code coverage, typically during test execution. It uses +the code analysis tools and tracing hooks provided in the Python standard +library to determine which lines are executable, and which have been executed. + +Coverage.py runs on these versions of Python: + +.. PYVERSIONS + +* Python 3.10 through 3.15 alpha, including free-threading. +* PyPy3 versions 3.10 and 3.11. + +Documentation is on `Read the Docs`_. Code repository and issue tracker are on +`GitHub`_. + +.. _Read the Docs: https://coverage.readthedocs.io/en/7.12.0/ +.. _GitHub: https://github.com/coveragepy/coveragepy + +**New in 7.x:** +``[run] patch`` setting; +``--save-signal`` option; +``[run] core`` setting; +``[run] source_dirs`` setting; +``Coverage.branch_stats()``; +multi-line exclusion patterns; +function/class reporting; +experimental support for sys.monitoring; +dropped support for Python up to 3.9; +added ``Coverage.collect()`` context manager; +improved data combining; +``[run] exclude_also`` setting; +``report --format=``; +type annotations. + +**New in 6.x:** +dropped support for Python 2.7, 3.5, and 3.6; +write data on SIGTERM; +added support for 3.10 match/case statements. + + +For Enterprise +-------------- + +.. |tideliftlogo| image:: https://nedbatchelder.com/pix/Tidelift_Logo_small.png + :alt: Tidelift + :target: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=readme + +.. list-table:: + :widths: 10 100 + + * - |tideliftlogo| + - `Available as part of the Tidelift Subscription. `_ + Coverage and thousands of other packages are working with + Tidelift to deliver one enterprise subscription that covers all of the open + source you use. If you want the flexibility of open source and the confidence + of commercial-grade software, this is for you. + `Learn more. `_ + + +Getting Started +--------------- + +Looking to run ``coverage`` on your test suite? See the `Quick Start section`_ +of the docs. + +.. _Quick Start section: https://coverage.readthedocs.io/en/7.12.0/#quick-start + + +Change history +-------------- + +The complete history of changes is on the `change history page`_. + +.. _change history page: https://coverage.readthedocs.io/en/7.12.0/changes.html + + +Code of Conduct +--------------- + +Everyone participating in the coverage.py project is expected to treat other +people with respect and to follow the guidelines articulated in the `Python +Community Code of Conduct`_. + +.. _Python Community Code of Conduct: https://www.python.org/psf/codeofconduct/ + + +Contributing +------------ + +Found a bug? Want to help improve the code or documentation? See the +`Contributing section`_ of the docs. + +.. _Contributing section: https://coverage.readthedocs.io/en/7.12.0/contributing.html + + +Security +-------- + +To report a security vulnerability, please use the `Tidelift security +contact`_. Tidelift will coordinate the fix and disclosure. + +.. _Tidelift security contact: https://tidelift.com/security + + +License +------- + +Licensed under the `Apache 2.0 License`_. For details, see `NOTICE.txt`_. + +.. _Apache 2.0 License: http://www.apache.org/licenses/LICENSE-2.0 +.. _NOTICE.txt: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt + + +.. |test-status| image:: https://github.com/coveragepy/coveragepy/actions/workflows/testsuite.yml/badge.svg?branch=main&event=push + :target: https://github.com/coveragepy/coveragepy/actions/workflows/testsuite.yml + :alt: Test suite status +.. |quality-status| image:: https://github.com/coveragepy/coveragepy/actions/workflows/quality.yml/badge.svg?branch=main&event=push + :target: https://github.com/coveragepy/coveragepy/actions/workflows/quality.yml + :alt: Quality check status +.. |docs| image:: https://readthedocs.org/projects/coverage/badge/?version=latest&style=flat + :target: https://coverage.readthedocs.io/en/7.12.0/ + :alt: Documentation +.. |kit| image:: https://img.shields.io/pypi/v/coverage + :target: https://pypi.org/project/coverage/ + :alt: PyPI status +.. |versions| image:: https://img.shields.io/pypi/pyversions/coverage.svg?logo=python&logoColor=FBE072 + :target: https://pypi.org/project/coverage/ + :alt: Python versions supported +.. |license| image:: https://img.shields.io/pypi/l/coverage.svg + :target: https://github.com/coveragepy/coveragepy/blob/main/LICENSE.txt + :alt: License +.. |metacov| image:: https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/nedbat/8c6980f77988a327348f9b02bbaf67f5/raw/metacov.json + :target: https://coveragepy.github.io/metacov-reports/latest.html + :alt: Coverage reports +.. |tidelift| image:: https://tidelift.com/badges/package/pypi/coverage + :target: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=readme + :alt: Tidelift +.. |stars| image:: https://img.shields.io/github/stars/coveragepy/coveragepy.svg?logo=github&style=flat + :target: https://github.com/coveragepy/coveragepy/stargazers + :alt: GitHub stars +.. |mastodon-nedbat| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&label=@nedbat&query=followers_count&url=https%3A%2F%2Fhachyderm.io%2Fapi%2Fv1%2Faccounts%2Flookup%3Facct=nedbat + :target: https://hachyderm.io/@nedbat + :alt: nedbat on Mastodon +.. |mastodon-coveragepy| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&label=@coveragepy&query=followers_count&url=https%3A%2F%2Fhachyderm.io%2Fapi%2Fv1%2Faccounts%2Flookup%3Facct=coveragepy + :target: https://hachyderm.io/@coveragepy + :alt: coveragepy on Mastodon +.. |bluesky-nedbat| image:: https://img.shields.io/badge/dynamic/json?style=flat&color=96a3b0&labelColor=3686f7&logo=icloud&logoColor=white&label=@nedbat&url=https%3A%2F%2Fpublic.api.bsky.app%2Fxrpc%2Fapp.bsky.actor.getProfile%3Factor=nedbat.com&query=followersCount + :target: https://bsky.app/profile/nedbat.com + :alt: nedbat on Bluesky +.. |sponsor| image:: https://img.shields.io/badge/%E2%9D%A4-Sponsor%20me-brightgreen?style=flat&logo=GitHub + :target: https://github.com/sponsors/nedbat + :alt: Sponsor me on GitHub diff --git a/Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/RECORD b/Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/RECORD new file mode 100644 index 00000000..57c45d30 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/RECORD @@ -0,0 +1,106 @@ +../../../bin/coverage,sha256=-a_nmUwoLehtytdxHAGo0yFcyFc5cF3jJ6fLex4VOy8,227 +../../../bin/coverage-3.12,sha256=-a_nmUwoLehtytdxHAGo0yFcyFc5cF3jJ6fLex4VOy8,227 +../../../bin/coverage3,sha256=-a_nmUwoLehtytdxHAGo0yFcyFc5cF3jJ6fLex4VOy8,227 +coverage-7.12.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +coverage-7.12.0.dist-info/METADATA,sha256=PGD12wCXYeynkpPbVge35bwwiNBgbO6zkLCeeAozPuE,9074 +coverage-7.12.0.dist-info/RECORD,, +coverage-7.12.0.dist-info/WHEEL,sha256=mX4U4odf6w47aVjwZUmTYd1MF9BbrhVLKlaWSvZwHEk,186 +coverage-7.12.0.dist-info/entry_points.txt,sha256=s7x_4Bg6sI_AjEov0yLrWDOVR__vCWpFoIGw-MZk2qA,123 +coverage-7.12.0.dist-info/licenses/LICENSE.txt,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174 +coverage-7.12.0.dist-info/top_level.txt,sha256=BjhyiIvusb5OJkqCXjRncTF3soKF-mDOby-hxkWwwv0,9 +coverage/__init__.py,sha256=deRlSPNGXQa-6Mr9q3FpSXvS51dc-xpHxdLS58TDE3k,1065 +coverage/__main__.py,sha256=rAq5mnzJvTfjnZxufsY-YoKZkHM81vdhkUsAmOU4wt8,297 +coverage/__pycache__/__init__.cpython-312.pyc,, +coverage/__pycache__/__main__.cpython-312.pyc,, +coverage/__pycache__/annotate.cpython-312.pyc,, +coverage/__pycache__/bytecode.cpython-312.pyc,, +coverage/__pycache__/cmdline.cpython-312.pyc,, +coverage/__pycache__/collector.cpython-312.pyc,, +coverage/__pycache__/config.cpython-312.pyc,, +coverage/__pycache__/context.cpython-312.pyc,, +coverage/__pycache__/control.cpython-312.pyc,, +coverage/__pycache__/core.cpython-312.pyc,, +coverage/__pycache__/data.cpython-312.pyc,, +coverage/__pycache__/debug.cpython-312.pyc,, +coverage/__pycache__/disposition.cpython-312.pyc,, +coverage/__pycache__/env.cpython-312.pyc,, +coverage/__pycache__/exceptions.cpython-312.pyc,, +coverage/__pycache__/execfile.cpython-312.pyc,, +coverage/__pycache__/files.cpython-312.pyc,, +coverage/__pycache__/html.cpython-312.pyc,, +coverage/__pycache__/inorout.cpython-312.pyc,, +coverage/__pycache__/jsonreport.cpython-312.pyc,, +coverage/__pycache__/lcovreport.cpython-312.pyc,, +coverage/__pycache__/misc.cpython-312.pyc,, +coverage/__pycache__/multiproc.cpython-312.pyc,, +coverage/__pycache__/numbits.cpython-312.pyc,, +coverage/__pycache__/parser.cpython-312.pyc,, +coverage/__pycache__/patch.cpython-312.pyc,, +coverage/__pycache__/phystokens.cpython-312.pyc,, +coverage/__pycache__/plugin.cpython-312.pyc,, +coverage/__pycache__/plugin_support.cpython-312.pyc,, +coverage/__pycache__/python.cpython-312.pyc,, +coverage/__pycache__/pytracer.cpython-312.pyc,, +coverage/__pycache__/regions.cpython-312.pyc,, +coverage/__pycache__/report.cpython-312.pyc,, +coverage/__pycache__/report_core.cpython-312.pyc,, +coverage/__pycache__/results.cpython-312.pyc,, +coverage/__pycache__/sqldata.cpython-312.pyc,, +coverage/__pycache__/sqlitedb.cpython-312.pyc,, +coverage/__pycache__/sysmon.cpython-312.pyc,, +coverage/__pycache__/templite.cpython-312.pyc,, +coverage/__pycache__/tomlconfig.cpython-312.pyc,, +coverage/__pycache__/types.cpython-312.pyc,, +coverage/__pycache__/version.cpython-312.pyc,, +coverage/__pycache__/xmlreport.cpython-312.pyc,, +coverage/annotate.py,sha256=vI_P4Qj9W7OqdJaMJyvSp57hvT6ljCsnEf5ZyfaKvkM,3751 +coverage/bytecode.py,sha256=n_4YzE8Gas37i0mRgwvbTgu4v6yfekCh4qWZ7YVq0tk,6666 +coverage/cmdline.py,sha256=t7l_LoWAUhUuEmMogiVEAOducHdrudqmwezrZxVhaYE,36819 +coverage/collector.py,sha256=doMi0mv8Z-zDY3kf7NJifLjH3Kz7QuXr3o9aSrHzMTQ,18541 +coverage/config.py,sha256=tXVjZ0EwLI9oxEcRCVJZRiDktCXoUpD1d7L0JNvBM5Y,25964 +coverage/context.py,sha256=Ef1NlMuuD5g2Z3vJhK9fr6yg_NxOYTJmGTACRLU1uno,2434 +coverage/control.py,sha256=ZH_GxR7uc9uJBOkj1IKv0xsJP-63g3JuEDe21QQHz88,54818 +coverage/core.py,sha256=wQG--Xm1Hvyt_jYO7VgfP-CT2vE1o4zIbFI2CtfTsXw,5404 +coverage/data.py,sha256=b-4KXkMlpocqS-T_HHa1LlPOxnz-I35OpcH4ztfvYP4,8127 +coverage/debug.py,sha256=5f1JSbSVeQnulJTOu0E_kelo29cih5A9gS2o27OX3h8,21753 +coverage/disposition.py,sha256=T6p5yH1b6dnnsXq7YI9nbP8UAqFk6V9PyFOivkV5Qr8,1897 +coverage/env.py,sha256=_0HqQJiQIeY44ziVwiMohYIaox0btbc2fLoJMnSt1RI,4972 +coverage/exceptions.py,sha256=VD6utQATQ5GRIAK0SPJyOlL2och54qRfVD9vlt9EElA,1640 +coverage/execfile.py,sha256=IL3TzwhAxiMLs6QwUvF-HK-A2Scjgh8GjkpOKhcWFtY,11993 +coverage/files.py,sha256=WrI3dw_zEMG1YNS6674vFP9TPoUXvXg0itxVSWeOoNc,19357 +coverage/html.py,sha256=hQVh56hKJcF_v0Di02Gd-ov22_MCYF29f3A-0lHwZmg,31524 +coverage/htmlfiles/coverage_html.js,sha256=c3j5ad-4xXqf3u_aBPpiNhoHYn2mTYsKmercXP2XW3M,25450 +coverage/htmlfiles/favicon_32.png,sha256=vIEA-odDwRvSQ-syWfSwEnWGUWEv2b-Tv4tzTRfwJWE,1732 +coverage/htmlfiles/index.html,sha256=8_Resfvt6vgAJs82-dF5gtKCjkPp8by8PMff4eWmMkU,8702 +coverage/htmlfiles/keybd_closed.png,sha256=fZv4rmY3DkNJtPQjrFJ5UBOE5DdNof3mdeCZWC7TOoo,9004 +coverage/htmlfiles/pyfile.html,sha256=iBCg74uV_XEcCTP2F02daNZYlIuzzh0vhsZi35zkpGU,6508 +coverage/htmlfiles/style.css,sha256=vkP6XozR7sM-MQotUpXV03kyobzBvl8rquzommeX58A,15941 +coverage/htmlfiles/style.scss,sha256=fzKmMXr61ZvtnpvtgbDeA9kSnfNmbFFY7jYmmiP9SxI,21291 +coverage/inorout.py,sha256=jFUz-I_g50G_8vGR935hWuwXdrNPep3nfnwVYmAVYEQ,24345 +coverage/jsonreport.py,sha256=mpNhylwzkx3bitcEKOSrvYtP0uAzR4rz1Ofq-JHiwcs,7333 +coverage/lcovreport.py,sha256=Q9-g1QS7dUO-9ckqxecRs1_18yfxQPcHw-Mu8y2XU1U,7874 +coverage/misc.py,sha256=iPEV4g_HsXhQmpuzuN1CPy82uF2YLjKOleMnWgXJyDA,11291 +coverage/multiproc.py,sha256=Y1AeYjch8pD4Zb5HjoW51IVz5yeLDY5-ipIOOk-Adyk,4175 +coverage/numbits.py,sha256=4B171qTbHyZ0WDnYGjGo456_PC2jDZSe22F9KCrmZ4Q,4673 +coverage/parser.py,sha256=Toq-DgeZBl_ZawrKpWPDkfAYbLxMLQPJwi4UlWMPDjY,46313 +coverage/patch.py,sha256=oEcBoH6lcSAB9Y0jxuk3ZbFWqJI05R76fJtMHtsp8PY,5567 +coverage/phystokens.py,sha256=Z7chMvCxuYMu2Wj9Oc1vlO8dV1OOqZXWfzALsWfEC7s,7450 +coverage/plugin.py,sha256=4fF3Bq9JVZIIxPUI-DCUGUqeGNzvYldJnqMvP6JlsKY,21509 +coverage/plugin_support.py,sha256=X0I5D3q7MX2IsZZUBBkJJ6lP3guF-zNo_2oTu1XyePs,10444 +coverage/py.typed,sha256=_B1ZXy5hKJZ2Zo3jWSXjqy1SO3rnLdZsUULnKGTplfc,72 +coverage/python.py,sha256=X6-ypbCYnJoihsSj1M5thX_kFRyHjN37ajOHQWo5F1E,8753 +coverage/pytracer.py,sha256=RBs6GaQQHSEau-eSiuyjIyS64W5Jg61GRVRO2wo1rZA,15322 +coverage/regions.py,sha256=hIf6Hly1Zsfl4hPA2FQgDtDT6Lcv0fLlFCXmc94WOlY,4500 +coverage/report.py,sha256=QrMfQaXfghss5g301vp7BudGLaQXD_eaKrFLFFC4Ixg,10818 +coverage/report_core.py,sha256=xyhYM93YVQUu05fDpPkG1raDtTQyQbzPUjIwrlUf-ns,4044 +coverage/results.py,sha256=I1SBr07T-EtHdgmOBYj7ivydla7QskEVzqBPiHjrojQ,17242 +coverage/sqldata.py,sha256=ba76NYiiMWNwfsFWxTrrgjSVKVvA9vw42x4cZv-c_dU,45528 +coverage/sqlitedb.py,sha256=R6A4B4D0LQNqMUPZTE4W66hC7V5OenHN9hRWb_pXJSg,10024 +coverage/sysmon.py,sha256=ZDJfR27XU6jSOH4LtrKU6bVqgQxtt2CZHnoz7gJBhnA,19729 +coverage/templite.py,sha256=OXquHdxqm3SWGY4GnGb6SjCUNa40_yXmzzY3hnV6zNU,11307 +coverage/tomlconfig.py,sha256=9KXQWUPpnqkmRYFhyJMS8ZOfJ4bG69EsskVXm_NT7j8,7558 +coverage/tracer.cpython-312-x86_64-linux-gnu.so,sha256=c-WzBMl8koEIhlFYJyUBC3b4PnB6Wl_lAgcgbLrBnSg,129792 +coverage/tracer.pyi,sha256=53ZiaWNz6q6qWiEZWFMHeLLkbg6-4oBAzNJ2i1-hA-g,1207 +coverage/types.py,sha256=nOLSWf-CMhaa9h7SMTcueFsklX0vuLdXdmsqzp2Tuqg,5600 +coverage/version.py,sha256=y8jqxYmM1nutdtr6SU9gzKhY_7sW5jywPrrQq_HS86Q,1094 +coverage/xmlreport.py,sha256=ayeJ8DEqIX6f9pdRFJvULIGA4WJ8wDR9BEjoKLdK1PA,9871 diff --git a/Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/WHEEL b/Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/WHEEL new file mode 100644 index 00000000..9921a02f --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/WHEEL @@ -0,0 +1,7 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.9.0) +Root-Is-Purelib: false +Tag: cp312-cp312-manylinux_2_5_x86_64 +Tag: cp312-cp312-manylinux1_x86_64 +Tag: cp312-cp312-manylinux_2_28_x86_64 + diff --git a/Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/entry_points.txt b/Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/entry_points.txt new file mode 100644 index 00000000..242b4774 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/entry_points.txt @@ -0,0 +1,4 @@ +[console_scripts] +coverage = coverage.cmdline:main +coverage-3.12 = coverage.cmdline:main +coverage3 = coverage.cmdline:main diff --git a/Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/licenses/LICENSE.txt b/Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/licenses/LICENSE.txt new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/licenses/LICENSE.txt @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/top_level.txt b/Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/top_level.txt new file mode 100644 index 00000000..4ebc8aea --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/coverage-7.12.0.dist-info/top_level.txt @@ -0,0 +1 @@ +coverage diff --git a/Backend/venv/lib/python3.12/site-packages/cryptography/__pycache__/fernet.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/cryptography/__pycache__/fernet.cpython-312.pyc index 24170122448a2d9c46119ac816a6c80546f1be2a..6ce20e0ea4067e70392cec2a2ff49e7a1d39b0b1 100644 GIT binary patch delta 20 acmZn-Zw==@&CAQh00d7X6gF~KX#)T|aRqSz delta 20 acmZn-Zw==@&CAQh00iGB$Zq7W(gpxMFa@{( diff --git a/Backend/venv/lib/python3.12/site-packages/cryptography/hazmat/primitives/kdf/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/cryptography/hazmat/primitives/kdf/__pycache__/__init__.cpython-312.pyc index cdd515887217e0a5fccc3405065d6e9d33b61e43..bc3daee13635e77b8f34eabf3753af24f4333566 100644 GIT binary patch delta 20 acmaFE`G%AGG%qg~0}wooP}s=*kOcrhY6ar} delta 20 acmaFE`G%AGG%qg~0}y=O_G%qg~0}wooP}s=5f(rmSI0aS! delta 20 acmdlix>=O_G%qg~0}y=3.8 +Requires-Python: >=3.7 License-File: LICENSE.txt -Dynamic: author -Dynamic: author-email -Dynamic: classifier -Dynamic: description -Dynamic: home-page -Dynamic: license -Dynamic: license-file -Dynamic: requires-python -Dynamic: summary +Requires-Dist: typing-extensions ; python_version < "3.8" h11 === @@ -146,7 +137,7 @@ library. It has a test suite with 100.0% coverage for both statements and branches. -Currently it supports Python 3 (testing on 3.8-3.12) and PyPy 3. +Currently it supports Python 3 (testing on 3.7-3.10) and PyPy 3. The last Python 2-compatible version was h11 0.11.x. (Originally it had a Cython wrapper for `http-parser `_ and a beautiful nested state diff --git a/Backend/venv/lib/python3.12/site-packages/h11-0.14.0.dist-info/RECORD b/Backend/venv/lib/python3.12/site-packages/h11-0.14.0.dist-info/RECORD new file mode 100644 index 00000000..a63f6ccf --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/h11-0.14.0.dist-info/RECORD @@ -0,0 +1,52 @@ +h11-0.14.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +h11-0.14.0.dist-info/LICENSE.txt,sha256=N9tbuFkm2yikJ6JYZ_ELEjIAOuob5pzLhRE4rbjm82E,1124 +h11-0.14.0.dist-info/METADATA,sha256=B7pZ0m7WBXNs17vl6hUH9bJTL9s37DaGvY31w7jNxSg,8175 +h11-0.14.0.dist-info/RECORD,, +h11-0.14.0.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 +h11-0.14.0.dist-info/top_level.txt,sha256=F7dC4jl3zeh8TGHEPaWJrMbeuoWbS379Gwdi-Yvdcis,4 +h11/__init__.py,sha256=iO1KzkSO42yZ6ffg-VMgbx_ZVTWGUY00nRYEWn-s3kY,1507 +h11/__pycache__/__init__.cpython-312.pyc,, +h11/__pycache__/_abnf.cpython-312.pyc,, +h11/__pycache__/_connection.cpython-312.pyc,, +h11/__pycache__/_events.cpython-312.pyc,, +h11/__pycache__/_headers.cpython-312.pyc,, +h11/__pycache__/_readers.cpython-312.pyc,, +h11/__pycache__/_receivebuffer.cpython-312.pyc,, +h11/__pycache__/_state.cpython-312.pyc,, +h11/__pycache__/_util.cpython-312.pyc,, +h11/__pycache__/_version.cpython-312.pyc,, +h11/__pycache__/_writers.cpython-312.pyc,, +h11/_abnf.py,sha256=ybixr0xsupnkA6GFAyMubuXF6Tc1lb_hF890NgCsfNc,4815 +h11/_connection.py,sha256=eS2sorMD0zKLCFiB9lW9W9F_Nzny2tjHa4e6s1ujr1c,26539 +h11/_events.py,sha256=LEfuvg1AbhHaVRwxCd0I-pFn9-ezUOaoL8o2Kvy1PBA,11816 +h11/_headers.py,sha256=RqB8cd8CN0blYPzcLe5qeCh-phv6D1U_CHj4hs67lgQ,10230 +h11/_readers.py,sha256=EbSed0jzwVUiD1nOPAeUcVE4Flf3wXkxfb8c06-OTBM,8383 +h11/_receivebuffer.py,sha256=xrspsdsNgWFxRfQcTXxR8RrdjRXXTK0Io5cQYWpJ1Ws,5252 +h11/_state.py,sha256=k1VL6SDbaPkSrZ-49ewCXDpuiUS69_46YhbWjuV1qEY,13300 +h11/_util.py,sha256=LWkkjXyJaFlAy6Lt39w73UStklFT5ovcvo0TkY7RYuk,4888 +h11/_version.py,sha256=LVyTdiZRzIIEv79UyOgbM5iUrJUllEzlCWaJEYBY1zc,686 +h11/_writers.py,sha256=oFKm6PtjeHfbj4RLX7VB7KDc1gIY53gXG3_HR9ltmTA,5081 +h11/py.typed,sha256=sow9soTwP9T_gEAQSVh7Gb8855h04Nwmhs2We-JRgZM,7 +h11/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +h11/tests/__pycache__/__init__.cpython-312.pyc,, +h11/tests/__pycache__/helpers.cpython-312.pyc,, +h11/tests/__pycache__/test_against_stdlib_http.cpython-312.pyc,, +h11/tests/__pycache__/test_connection.cpython-312.pyc,, +h11/tests/__pycache__/test_events.cpython-312.pyc,, +h11/tests/__pycache__/test_headers.cpython-312.pyc,, +h11/tests/__pycache__/test_helpers.cpython-312.pyc,, +h11/tests/__pycache__/test_io.cpython-312.pyc,, +h11/tests/__pycache__/test_receivebuffer.cpython-312.pyc,, +h11/tests/__pycache__/test_state.cpython-312.pyc,, +h11/tests/__pycache__/test_util.cpython-312.pyc,, +h11/tests/data/test-file,sha256=ZJ03Rqs98oJw29OHzJg7LlMzyGQaRAY0r3AqBeM2wVU,65 +h11/tests/helpers.py,sha256=a1EVG_p7xU4wRsa3tMPTRxuaKCmretok9sxXWvqfmQA,3355 +h11/tests/test_against_stdlib_http.py,sha256=cojCHgHXFQ8gWhNlEEwl3trmOpN-5uDukRoHnElqo3A,3995 +h11/tests/test_connection.py,sha256=ZbPLDPclKvjgjAhgk-WlCPBaf17c4XUIV2tpaW08jOI,38720 +h11/tests/test_events.py,sha256=LPVLbcV-NvPNK9fW3rraR6Bdpz1hAlsWubMtNaJ5gHg,4657 +h11/tests/test_headers.py,sha256=qd8T1Zenuz5GbD6wklSJ5G8VS7trrYgMV0jT-SMvqg8,5612 +h11/tests/test_helpers.py,sha256=kAo0CEM4LGqmyyP2ZFmhsyq3UFJqoFfAbzu3hbWreRM,794 +h11/tests/test_io.py,sha256=uCZVnjarkRBkudfC1ij-KSCQ71XWJhnkgkgWWkKgYPQ,16386 +h11/tests/test_receivebuffer.py,sha256=3jGbeJM36Akqg_pAhPb7XzIn2NS6RhPg-Ryg8Eu6ytk,3454 +h11/tests/test_state.py,sha256=rqll9WqFsJPE0zSrtCn9LH659mPKsDeXZ-DwXwleuBQ,8928 +h11/tests/test_util.py,sha256=VO5L4nSFe4pgtSwKuv6u_6l0H7UeizF5WKuHTWreg70,2970 diff --git a/Backend/venv/lib/python3.12/site-packages/h11-0.14.0.dist-info/WHEEL b/Backend/venv/lib/python3.12/site-packages/h11-0.14.0.dist-info/WHEEL new file mode 100644 index 00000000..5bad85fd --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/h11-0.14.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/Backend/venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/top_level.txt b/Backend/venv/lib/python3.12/site-packages/h11-0.14.0.dist-info/top_level.txt similarity index 100% rename from Backend/venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/top_level.txt rename to Backend/venv/lib/python3.12/site-packages/h11-0.14.0.dist-info/top_level.txt diff --git a/Backend/venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/RECORD b/Backend/venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/RECORD deleted file mode 100644 index a8f8e63f..00000000 --- a/Backend/venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/RECORD +++ /dev/null @@ -1,29 +0,0 @@ -h11-0.16.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -h11-0.16.0.dist-info/METADATA,sha256=KPMmCYrAn8unm48YD5YIfIQf4kViFct7hyqcfVzRnWQ,8348 -h11-0.16.0.dist-info/RECORD,, -h11-0.16.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91 -h11-0.16.0.dist-info/licenses/LICENSE.txt,sha256=N9tbuFkm2yikJ6JYZ_ELEjIAOuob5pzLhRE4rbjm82E,1124 -h11-0.16.0.dist-info/top_level.txt,sha256=F7dC4jl3zeh8TGHEPaWJrMbeuoWbS379Gwdi-Yvdcis,4 -h11/__init__.py,sha256=iO1KzkSO42yZ6ffg-VMgbx_ZVTWGUY00nRYEWn-s3kY,1507 -h11/__pycache__/__init__.cpython-312.pyc,, -h11/__pycache__/_abnf.cpython-312.pyc,, -h11/__pycache__/_connection.cpython-312.pyc,, -h11/__pycache__/_events.cpython-312.pyc,, -h11/__pycache__/_headers.cpython-312.pyc,, -h11/__pycache__/_readers.cpython-312.pyc,, -h11/__pycache__/_receivebuffer.cpython-312.pyc,, -h11/__pycache__/_state.cpython-312.pyc,, -h11/__pycache__/_util.cpython-312.pyc,, -h11/__pycache__/_version.cpython-312.pyc,, -h11/__pycache__/_writers.cpython-312.pyc,, -h11/_abnf.py,sha256=ybixr0xsupnkA6GFAyMubuXF6Tc1lb_hF890NgCsfNc,4815 -h11/_connection.py,sha256=k9YRVf6koZqbttBW36xSWaJpWdZwa-xQVU9AHEo9DuI,26863 -h11/_events.py,sha256=I97aXoal1Wu7dkL548BANBUCkOIbe-x5CioYA9IBY14,11792 -h11/_headers.py,sha256=P7D-lBNxHwdLZPLimmYwrPG-9ZkjElvvJZJdZAgSP-4,10412 -h11/_readers.py,sha256=a4RypORUCC3d0q_kxPuBIM7jTD8iLt5X91TH0FsduN4,8590 -h11/_receivebuffer.py,sha256=xrspsdsNgWFxRfQcTXxR8RrdjRXXTK0Io5cQYWpJ1Ws,5252 -h11/_state.py,sha256=_5LG_BGR8FCcFQeBPH-TMHgm_-B-EUcWCnQof_9XjFE,13231 -h11/_util.py,sha256=LWkkjXyJaFlAy6Lt39w73UStklFT5ovcvo0TkY7RYuk,4888 -h11/_version.py,sha256=GVSsbPSPDcOuF6ptfIiXnVJoaEm3ygXbMnqlr_Giahw,686 -h11/_writers.py,sha256=oFKm6PtjeHfbj4RLX7VB7KDc1gIY53gXG3_HR9ltmTA,5081 -h11/py.typed,sha256=sow9soTwP9T_gEAQSVh7Gb8855h04Nwmhs2We-JRgZM,7 diff --git a/Backend/venv/lib/python3.12/site-packages/h11/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/h11/__pycache__/__init__.cpython-312.pyc index 434809b6040dbabb712b796a3e8e87de8793eb70..4a3f03b2ca2b348dd0986dc49a7cc6350f8519f9 100644 GIT binary patch delta 20 acmdnWv6X}SG%qg~0}!YiDsAM}VgUd!oCDba delta 20 acmdnWv6X}SG%qg~0}xbQlibLy#R32}k_5;A diff --git a/Backend/venv/lib/python3.12/site-packages/h11/__pycache__/_abnf.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/h11/__pycache__/_abnf.cpython-312.pyc index ce3acaf04e30f91b9fb3257a799054645a9ca862..307efa92b39780a9d2a59b652211c0f75c206bc5 100644 GIT binary patch delta 20 acmey)`<<8jG%qg~0}!YiDsAL`!v+96q6I4e delta 20 acmey)`<<8jG%qg~0}xbQlibMth7ABhmtbnvXDm1aSKnH&y&bcp6 z%HT@=`JQv`J?GqW&OP^SeUJR+JEZ2<)zvNuMps{Jc5`8)rh%N_=-T5Zl4?<{Ial7D z+DM1?3(}VJ<^3stzAjal52ONuSCOmF2UEd(L#jd0_FO0*PKCwXk!#FHQXAjzSQlhv zt~1}2>dMDc@w}Xp1<#pFZP^wmJqB~eY9`hruxs@Qcv}(b#&l=#11+L8yso?ms1BF z!hdt~HrmyC*nXcd8w9frn-*9CBQ?AhlEXJ~Dm(cPNwRc{+$AKzZ(3hzN&uH3#Nl_R z0^q##ds`*z7kvJz?f!vom`<@CWb`2j(E!d+A|Q#A!5v{Yw^qCrt^?X|EYh?}nPxZ^ zsGin!CR^Ar|5HUUh#CyHa+=a*C7sKzQZ@>l!`x#(P>&{z8b+U9p_;DbslHfLSzl?+ z-f0P;4|i$+GR1;U3wlqE78dkHc7$K54D3g#>@aNl+A?K6AebM(aBEf7IfLh#*i`0Z&mh(gjdzdTv1(9=2<$QEiABE5YO-t=SLOjS7xI&K=$$p*A)`u zw_TjH@C)uYrqvqiQ*G2wn?z=8Y9nN$V~5*m^0#FXkaejsH46W6wTa5GQ-%U)20F1; zA-6KQ^yjK&OJpAyoXS`M%L7w{^qn8`f~VIL)*WSuN|u~_H0I}9jZW_-G1)JWv*fI# zRPdj8dNxEv(l2ElluewqPM;U!zRY&wm-1bUFPkq z{Gij{s!Do8S#Zzd>M~D{q8YcROsf|Dp|`OWUBQ%zp;~~#+=;O!$=0beZ9G!j?0Bw) zefD9wjvuUzE=54fS^*5#@pNv5jx$zdhJC)s@@btN7Xp2eW|_i@Im)8Qa3~q925QtO zP?Jiws1)axXhvsUrt}$>)hS~^lVYNNS&09r_M9ITvL^sE3_&awV*`B3*J}SNvVOr= zeJ%dz*n80#S&vn0B34HRV)<1a<*9L z6)tH%=W1Omto?l5A8UoN)2JD9iKdN#34Zu~OM`{>A!+I0(N;W-U z`B>tMO{c-y^`_@+_}2%}$aZ}A3jCW3=)gn4*hdw>k{iS{@ge7U`s*h;UpUnAh& z$y+JcL)2QbuVRLrgER5L9K?YmvZ3iTvA%tJqe8}Q`p0>9DK4VvBYHZsWf#4m#4 zH{%D$m`LzbxCASYDF{UbagZR+P65bPQ{yT!L@alUZ-1hfJi?a}40LWK-X`QlzL^Yx zv0o;Wb`KhD&71+I-gqwp2a*EdtJqY62p*P8~CRCnA=thVkv>}{Am;-=T zp4PB~v#%fvFSB?!%I>|4j8C{nhYyo4@^24c1vB&es+{|paZv}ttL6Qs!OtNn&W%B4 zUI4=io!#{5B)>8$lXiZ6wAYU&3@d#$bI%p*hB1XforTsrEZnb0QO6X*rx6YyJOVJwCUN=zfaU_gSGLD|W%mOK z-r@UaUR%5!h8_BxRxHT=?en2jED`B=5lOsbPns8@9_b9ilK_Tm_YpU&8r8+MH5_VI z%cPlVSktO%*tH_lsk&>DeG9fi6A>+96LlTrAJ6o}zJMBu!XAQu4PPP*77jCDm-+bP z_mPK6XCEIU&1RmNcqQ4>uvYG7z3|8OAnfH^NB6q%_^cCQhBwTPtYc|0)GHbZ6FGDv ziBA@bBVdXc2APT5xq>q%f{4SHaK;f52;B&xnqR}&>j?NJvr7n95JXS9jI%x|0w`Ll zNWY9s1U$92P^vx_v+&I)Cac5}6imtcxaGvqIy_sV)oJFUpP5WBK*UXX-~mHT(>1e Gq3S=bqqngD delta 3674 zcmbtWdu&tJ8NbKxIF6mzaT42k7?L=-j^pr1NYe6Hpb3wrfu>17R=10jdqNC;r03pH zLQ#z>)Q%OcrH8F(Q?<58Ei6du>aJx{wYJivJvvQPl_utG5<{!DYJV&RO|0G6_I>9j z4ix^Fv@6};{a)w$zI)DhzH@%{2KmEV#QkAKg;Rp(=x+k4zs;O>*O9NEZtE(~I1^4H zsV3E&KAjBQpYbNVg0-Z5nd(G!#-H$KY7#X->JoLCKq4S`TRNBtB|?I>r%(T= zHJ^zjB7$|K+cNEm_Kcj6Gtop;$jZ_knOGv0=}dGI>42m`+pDu=R-2|w zp;@p^We@Pr9rXc`dxbKYQ|Fawmdt>hjl-a?@_5-IS!m<k0>c9n9OXN>QteJDZf?em}~>Qnx_oJgkU=pbV`#`X$+}SS-YWqoTYTiSe?N!vF})r=c=Ccp+mM0K*LE3<6e8gyV~|T zO5Wq`-c_Ea&6kc`N?&ex@MG`rW!Er&)XT~G!lJi=kZ%5OUx@sg2dh^@A-`7>;1kt9 zs}e2`p#xlFaLD8S7V=wuwx*uv{LYf)Zhp;w3Vh^h+R0VWMug^Ijrem4L)jfpw$zT1 zKl3xStzfq34HQ1EebWS^pANQ?zw(R0eUOwjL@+6PeVqJZm56Ah`a@I<|nM0TLZ zodB|f?Lit>i#?1`kAQofh4@0lYX1cjn???|b*TmMYYq3AEQB{U9uj`1%zRZ}b>Up& zq=jtfw&vz?k+^W}rbnh$b|3$6AVS_QTptJ%8MG2k*bTfgyeQ2}>@gsWv-U}# zR_0)KD44$q+fAI)$EB-|a&Z_>;Td=P^FMim0qj+Pqn>T0lQ^!?tol!gpkX^cE-gBw zGS?k3u*{6-{|iDOXLb%e3GZUjL)%?kmB!k1b>(YDk`FMvYBHGzI< zTu#{Qob8H|aN(t0Uei4~;QPbvWPpD<+yr@)J@lF>KZGuDqP7o?!NunxpxADN5roYM z4+4y_VI=Pd(C{q78&Icl1KJ2IWQTVVql;nhDB7UAYq_lKTlzoL3KLO^Cy`YF>_%6p zC7WkDt8%Uzyd3Y;n!mSMd+7F)*QOWXJ)&&e(gRqv*j;<*$N3soJlrM~K z%;V-^=$GXY#7eo4!utrr9l~%S7)~V<4GG&L!?|MOZ=Xl%48qC=75nvNl$9Zf?T@u% zT>3;y7R}is-3P)-)L_*!JQdjw5Ec;DA&4WU8>#3rya8!$b6>Z@cWDq% zi_YcYXIq8amv@{We`WVwt^@qs_%4@75wjbS`}ybNJ#Nwb;no0mjjXQmjT4Q#jjOIh zS-O-P$CfyKYGGi}p%}LY&FJko`Ec1_bc(`dd~$9YZoMpsLYpFPBK*QcXI?bjqE(sG zQ)y$*MZiR?E;uyyl4cwrxR8(#T<+6~a@$J1ivxawuoVGUv}o3#Z;v7?PBt;^cP_;j zj*Xd|+CG=2o7peG5Zacc{RLpr3>So(wsK;+DU~3|=k`vMM*jP~NzXAmabA}Y#lrp< DS3oJX diff --git a/Backend/venv/lib/python3.12/site-packages/h11/__pycache__/_events.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/h11/__pycache__/_events.cpython-312.pyc index b22f438a7227f4cfdcdaacd1541fb35ee541f99e..ed8b0b5f8c689415e2b0b7d47b0f9a4f5bef0db2 100644 GIT binary patch delta 2253 zcmaJ?OK2Nc6rDF3jrFlUmSlbWk!8tFJpM**W0S^poHSJ%(wP1~L+PMMV_QZenKxq^ zZ|W>kD0Jn%MQJvLu4`NEtEn7g+fbUpmziBP!`HkRW>@*4#T5*46o`ne5%jz ztA3+X?c{uSS*`|*pc*tnY6$Ng)v(c}b{P>hf_G0fYQ)r-(XDnHaWzhaWuff-f~W~m z(1q1gVEORJ>A_Vpz4c^xpyz*NLaQK>=oN(*gtGsJQ0~;lTP{bH+Q*(D{iKg2MI{yC zyr|x{&%3o}O--|#q(7QILLXp19!t-#$DDpt&mbEWkE3UqD)wGJDxcdR%D{c_Vo@Qs z>->f16w&sUi&n8zEt;n7UaIKTvLbU>?%t3{fTeu7$pFwt02F{x4v!#9`N@1i5%F$!Xlq4k>Sa3K zSoO`hra*j~&~bQx@0(Al$Z4j;VWnxkjtkDQSl!NUx zbZezv)~HScY$uq=f?maEOQv33ax5M8l#a9e!C83-G~^p6LN(F{aVlY&2GRgP24D~% z%XY$jnGhWP9Bdc&kb2-8$lvp>TF@w1#@5)KO#m3M6m$nYLY(nuSTW1DdwR%c50k6-(2U4mt~x zXal{%aHW3-+BvNAOPsYWx4Vwr)N%xpfW>rzkHa<2 zhSM3xdK|4M8Xu)Uagmor_1>#CyQ{qUDmbNxg@WQcHWgS( zEFhM$RjdW@qb-Ca(2{I(RCCgkWWSHzB&o*c*g8pLF9IyW^1<7InP8ELA700401ofp z)(Bwt;~Ze)9|^Lh3cGqQ$o-aP6O*ghuK%?;z-Kgp0k(B0M&8z@3-IFD4ov|E;4I%S zrpXWgd?@@S1zCKm7QpHQQz7&O+nPE>dK%wNT_IPF+h=Ww?E%FAIwL4mCdv3K*C5_R*lEn6FFmD)0u`Gvx)Qx8CMD>|Fftrf4<_BL=3o_Vwty~uZ) z>0||RZo9R0t5S8gyR*~ToqT%pH0YZFz<(p$q%fcq=L8!XTLSZfF&vKTv1iVxm)EQM zJYC_>cx-~X#PNZP5b`K25Z`Y?bXOSJ6=v=VGxr2#S2(>ZocUQe^HBEq#m36?0+ArE F{{kiOj<^5- delta 2234 zcmaJ@O-vg{6y90?+ZfhfFc`3LaDXfnC`A1Wl$x|5B?6+hp@+R}47>4NjQji4N;w3$&gYPPHGCyZd!ESm4sKVIZ2LrgFT2b2 zy~phPm~*1OpM}K~>0w2pBzw8w!9?7=EK>E5a>?>3;_&t_yck@H8HiIh$uS&Z6i-DL#Jozx5BfYNig3d6e>mW z{NYh5a-HSnP^?)t4T8l8`zQMGPJ}NVe{HR7tlLgR(=5Zz+cwoS+6G2Z01N{npg@3j zaBxDVVHaz4$tM672<5A3#4q3LwpXl=@d8U>e}ycsLX41JMtncdb^d&~aD>HLaMpikjv`H4SUC zZd59o#*^~`-?s>d9L;c~F$A_4``VACh_O`sk>6#^BsuwfgZw<0o(C8K0P_XKgQcLj zhlxi}>YDtnDB;;LFvk61_NeRB4&0zb=rf>(Xz2t1(wc+$jVse(m>mMZl^kk7Kw4vj_R-wIg^-x@kLQzH;a zf;)y8Sj7?8aysLBkE3_FzMsD3i7fHW(nCIn+cPYeQrJ6#a$*?{91r5t!S&OJLD0)! z)Z$47oy>-_2cBfMl(k7`{mbl}PkIYBuK=t9D6B)-&owLgGjP~2ir4LiW(%Bns6NfW zCFAU0xgl!@IdL5I74s6*vaRNH(s*9{2L|oa2)QXR8xsY~H6mBM>vX zK!Pp(ip*P@_UZlF1$P0=5Z;6qU;OpH@YvhV<|eBWRv$JMLI?R5+xH~ccaz;DQU7&v znJ6c{aV_~x@MAQ=u1+N*cpJz{hV4#eE<1jEYok;x9GNws;`p@uTJ_+)h5rutV^}ou zxNXZH%os-oJyhW=-F$ c7;{u-*Qb-A2g0eJgrRSRp*ov>gLr|%e;B!k0RR91 diff --git a/Backend/venv/lib/python3.12/site-packages/h11/__pycache__/_headers.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/h11/__pycache__/_headers.cpython-312.pyc index 4bd147d5a8b8dd4bcc153d9384e82d60b7245483..9bf9871b67a0d0bf7ed03af35d622d46f51d75cc 100644 GIT binary patch delta 1937 zcmaJ??Qc_67(b`?-uAX#U$(aE*4MpMYEh5@0>*7jg<(Jg3M5h)rEpK7vuo#R$AD~h zCX*0Dbj?Y8i+_MfNF;oN_!2V{qi9BmQ5!z+P5rijFVRT!dCo1}MvOP<@7{BMzvuTn z=bq=>&JTV(;Qrd>auB$ZeLeZ#lQnk-eZQ8RTx}tYj6RIS%FEC%P;(|7G&RCY1B!EW zUG0O2x5;%jxgbTR+GIi&vV$0oSz#lpu*Ba5rQsc6cBbCXHfEwrdN~ta z?$DT7rpc+`TqWrk(~K4tF0w5dkkd zi?Enk3`BG_{Cu<8lHWaL^*^QlSuGuxMFm(G!c`W=q)=4*teWm_GQoR{OraYc!1frO zMhM1TIn@Zmxc3z8CCr~7+(C&!3|k>E=1co1IY8#z^#mX-^HV%rpIYsT+$;nF22?G z56A#oR}1-aC1sIXp1=SL20#Sd>eft#y`0IMc0bLs2IJ+m=C~jZ+hgEUweNhPkyt6P zqRh*C(4>(SJ#3i_aA4wDGLaJ{6#4*W$W2e%+{W*Hz1MxcH#~_2J;CFm+i|>g6|gG| z#e0r8<>CrpT+|(V+b}&31Ee%_4Qs_#XN*S0A?E{hLd-fJ0^vu_6OSwjRVl3NJUf{w zX2%#`haT$z$|^t?T=2g3w{+7}VzTAzo*2^I2)zJxYw5@+J6hpM94Pr>g|C)R(!&UG zEM-%(l>do&6Wwe8Gh|UGnhv3Tp3YDRJ_m1;V`SQ5kZHxB)9Mu22JJjDE)35fv?dOE zueD4mGr=kAv@&HG4U1xH!h7!Slrm*~X)_iGt*?p&Z%|xqs@e)PeKj7@cwc!TyN5v(7!Y?-jUz=S1 zTkuf6D1Pxr=^9}Rd?|ho#OQaz)?Q1#NY`jvemzJ@i*N4n8_xKG8vpb5RubGn%T|c` zJQN((t-Mcs*8bc2oiG7Sh-Z)QM%aV!9Kt?;l+CONYpE-xi3(pQehLmn*mW0lO& z(%4uwb6lP@o&ni6;;D`^(7s=F+^*uDq*T)#?dn=3Un!WIpVF7C`C%aWepI+~r3vVh zW}6>GW&{~~kxgR%Ot7*JW9)dm%royT-(L$OL?M3@F&L^sHJ^m8bYpcgCww__=xAjp z$Kg;k`%AnVj%uU8c*NE4V|MKJ6dg__#NF^4)w&IqI-ZB>_nNWAqb2ydJOcy%EJ8nm zJTO~P;50uR`%aCsve~urG|D2NHo`FxA40%Hav6W={bMC#qQD0E1kB<0s0@E(WKp5Q z6J145Tpo<3X>ud&`oYyUxA~o23%=xa*Mo}+&TQB$@3*v;I~t4^i{jJR0lGtU#-r|j PGT-mq?o_^TP{4lyo4Tkd delta 2044 zcmaJ>Yiv_h96#sw-uCvnt?l|)d)sadw=!h{@-VoCxfciwkOER8wd|g;j;<}|UMxcA z5Eq3&$eQy(KJpC_A2FDinE1s&#OQ|thL{E;U;Va#i4ni}Kez23#&DDSJO9V;|G4Lz z|NXA-R=?{fr_+YuIsG7*`K-I_3gD~duA)tqlRg0vG_|(;C_I}C)MQRv`GDO%wSJ|I zYpU%Au2~yvvh5i;9M8-#ix!iF1T>0N`y5go#7rzCOu`dF5GwKahN)IEsSqm>|Ld(= zB1?S35?P+1Zdt~e5QAtIp=6Xus*@G zl4C`x0=cY2A<q3U zM%fm{I{OMR#_rnoyLmnu28c_w^1W=6J%S_bfPFPCvNQG`AYQY-)4N0z4PsTM>0&CE zo+fl1&#?hO69HoIK;yd{?f4AKInM8ma(4%ZPJpU8e{_nB>$ICIrc6$!J%*&|l&Ns?9x)vA_MAx$-+CTNgVTpbak;{l*t7wD@z zdJte~XUAM&?+92vg7=98a27ohEi}g7bFHd1U_cAp#s{1_cWGlGg{Oe&rcOK1qq1?26o-vh#DTVd4bh_v9rBKOKG9QPn z*96cySgPd$Hk>l3XBtedltrXRZw|QoFm7hEo-Lt1s8N-&`68iJNh@SCIxITP6zCdu z&(rZ@aB$C9Vt6c(+BG~jn%JJ&J@7_qXvgS|#Qv%|T_}*8YMgM~RzE6c_4e8M&(yeB zbQ9a>?erQKVR8SjfLHs0UGOTnmtFJT2|jFc&u_eV`j_y=drcc3`noIezWcuZN^?KU zw?^;-cDc3uLJIdvTmF0-BZp^x>wSB)B1ZpyJcyBh6l-Qk_s$NYlL>_HlTf!$E+!rPei%s+#gAb-IpC`?tXqec|^>X|Ud9Kl(p}N!lBD zY}~E5SWCvEtCF74v&6Vlad{~dJqk{Gh@A*}%}2PHVi$v9g>OQ&StELCJU>01PSvhI zJqBik*{$F#lo@LKww;$XlEIH#hkRoNl<6#4#m#s07;&>#!ntc$u zJWCJo5DSzjWT3YM*UTh0o`+G}4GJCOz)!t4ncG0B+P52{n0U89wA!2)V{dfA`=h)X zHO4B&CH^T-d7qWr$taPyjq9AE{1!oGE$-y6kMP;zX>OC*CrhM)#;w!d}U|F z({q$HfxbZ5mdN4f_sB^0JVqF=`CW`x#Q1^<#HU&dWdGVc)Q@iU M+lQT|+YSu)FQnX}!2kdN diff --git a/Backend/venv/lib/python3.12/site-packages/h11/__pycache__/_readers.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/h11/__pycache__/_readers.cpython-312.pyc index f0ab5d3f6502016761af79064fa94c392140462f..749eb35d2f1762450b402d00d6049ff25a1564a2 100644 GIT binary patch delta 729 zcmdnvea@5bG%qg~0}!YiDrN3h*vL0uREe>MA)XN=1_CKeKpqo>&A?E}pvml4#5DPe zXe|o|gOc*(P%&Lr31$X{>5~fu#V5}btKvyv%64I3DDtUc%;KHg$SORUSzL5-fH=qG zDoI{OuE}!ZvXgtoIVZ1^WanUo8atU$T$~fCkFgTO(*yD}7#NC~ycn8*&dCyhIRHUJ zR1`%6?Gc`wC!t&~3X>}lgRyE@QkZL4RwHny`LeJ} zhszCq;f|Uc;?g&G1g`TaUE)z%lDtA?appxH{i{4mK*{U;su%fHS1?}U*95A102dIv z#IJRML+b*+*5nA8ON?rhwPl|$o}Mfq*P+hFs`f!Zht-#HhVKUk5WOOGgUb$`6D$`@ zd@kzweg=zuh}pbfPKuH7^5k3cGZ}AAE>TEeyfpc$!d}L^nf7e=ruX9g-sgGW?|HtT&4Km7 znr~*a3E)WVIHn#ozOPxf`3VJFgC(vMB8d@EsR3930cuz(L8%vQ*^(3kygqb^55b4% z4*!&pQfSN0qqxALCxVU0K-4V=kVIjDZx?~g={gFTIdX7<*cq%=FW2flkxFo4h%%&iYasS4){fk$NeKWW5taVS#;m- zHnT})ju5MpjD8x(pwIRuXhFa1ew=F^A@hG4ixmx%XvPk-?jW*BLs`opyLC<>6-F_v z)MUsCGXetKZ~#hO7$)qqH)bZ{(ri@pj8ER0i+LvEaXBh=coM9qC3^d2bX>-*zc3*6 z;_>otCY)0wVqM6drTd>qSjqKAPK=z1VpW+$bjb7ysuO9b*`V^pgs<)4XVEWROGw2 z+KaXGoG-`uu+Lt();M3yt5o$}LkbFrqK9)iz?ilDTn zquIz>@C&7$zNobg9hAAExT?9G5bmJgC!fPzbg$t${DFQpe1dzcZ#;gIJqt~L08WQJ J1j;uZ{sVGL(((WR diff --git a/Backend/venv/lib/python3.12/site-packages/h11/__pycache__/_receivebuffer.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/h11/__pycache__/_receivebuffer.cpython-312.pyc index cbb4df58d4fd98e16a4c9e96bef13103653a8dde..212f53a1ea2af3e4d8366149e2e7131e645405f7 100644 GIT binary patch delta 20 acmaE)@<@gIG%qg~0}!YiDsAKr7Xkn~q6DS@ delta 20 acmaE)@<@gIG%qg~0}xbQlibK1E(8EUm<5#p diff --git a/Backend/venv/lib/python3.12/site-packages/h11/__pycache__/_state.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/h11/__pycache__/_state.cpython-312.pyc index a9e2c4a247854332a81be99615b34a025e3263ec..6e8e60f3df76446269e1fb913ff45face928362a 100644 GIT binary patch delta 697 zcmbQ{bk~XZG%qg~0}!YiDrJ7z$Xn0P7sSZGFrA@>sg|XNxzTL$EPj1{sS;L@8W2cf z%wnG$C@gHqyc(zu1YmR%V+~Uca~fD*gLn-~2?s=;fuWS4h`)q;@;`ogMqZ#^(P);{ zOb|In2B11VBz0Ofj3ojHele351JGQCEMAxxf-ZqG5hj!fOzsm9XB3{?!y_lI$>djM z9F&@znpu{bqEMV#RF+z#P+dFug@BR-R}nu@dyyrG-~kf1*wXTgauZ7?O9`%G+%oy7 zps}>)b&-IJA^|rfv_CU5iHdz@W)TIE#>reF7L0zA9VMhE z$B9TY>Ta$Q`OC~`x4A+>ijj$dak4j`)Z}%N`ur*-oIurJ01QHIP!LWI;1e~1g&vFs z`xfLPGyo3?zR7x0%0}=oDq({uM1)%=Qw>8rOqijP0TdAGlbfYfS*mL_IVZ1@l4A6k zd_XFUEe#lj#mSRpq~!z!EUybVToiCPAaY##p!8({-^uRMl~N%E_&#%kSSFL(WnN2z%)cek|ySDaarnV%O@S&&-fKe<;{i_v%U4q1O@ R{xU{J<4+7AvPd4N0|3Rhl4Sq@ delta 662 zcmccXG|h?kG%qg~0}xbQlgwPdk++_o&xDbIVLC$%Q!PskbEEp?S^WB@+$F3aH6W0} zxEd%A0!@rHOf}4DV08`RH7q4;5OD^EQidY_63)po0`iR9lM8r6CI<-cr}7}lYSl27 z@FDocOkN<<7_zuwVhFkf&MbkOfY6)9w3-Q`fRO>HUvP2{kDQ7olV6oXP-=2&W?5>A zLUC$QS!$6&d~mpDh_gq0Xn=c=ql@e0p8`q(oJIUV%|(_V0%Z7PW5HF7%O>9wH0BNa z%*-S%^nrm%Q0N2OW*wowj2ySv((;RP6H6xRiC8fDPEHpwW7OU}PvkE%qs`_y5>kw; zFMvi&pR6b)KKYWQKCeUxCy);YDU6f-`NV`~Gt6Z|3Ot4^uE~B<(vzFSgeTWX2^zrz z1QC!3k7hE}FvP>mW2j^R`B8Q9ekoN6O^zZfphOW!dyz1R0DFpOGP86TTM96)isL88 zOUv;JneSlP&%Kj-L+u3t*U5d-l~TbV#h{2S3IGux=N7Sp2;a$yGKDr`tcDNxgeF*Y z=6zrVvOlmfh)T{7o#_369mL~ckW*Wbvch6v-bGo{51b$&E(R{X&)guE!Q^u?ucble z-x78$OU)|@DN4*M&Me8y&kLz6NGFp)H5UL!g$0ZN delta 24 ecmcc4c%6~^G%qg~0}xbQlbpz{$7nXuYAyg#KL&jO diff --git a/Backend/venv/lib/python3.12/site-packages/h11/__pycache__/_writers.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/h11/__pycache__/_writers.cpython-312.pyc index a802c8678208c64a7cdfc66535b3c6ba5c88f40c..f4ca341c8dbd6fe4f45897b39c4e8577811249af 100644 GIT binary patch delta 20 acmbPfIMa~(G%qg~0}!YiDsANMlK=oS6a;Gk delta 20 acmbPfIMa~(G%qg~0}xbQlibMNCjkIC3I$pK diff --git a/Backend/venv/lib/python3.12/site-packages/h11/_connection.py b/Backend/venv/lib/python3.12/site-packages/h11/_connection.py index e37d82a8..d1752707 100644 --- a/Backend/venv/lib/python3.12/site-packages/h11/_connection.py +++ b/Backend/venv/lib/python3.12/site-packages/h11/_connection.py @@ -1,17 +1,6 @@ # This contains the main Connection class. Everything in h11 revolves around # this. -from typing import ( - Any, - Callable, - cast, - Dict, - List, - Optional, - overload, - Tuple, - Type, - Union, -) +from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Type, Union from ._events import ( ConnectionClosed, @@ -68,7 +57,6 @@ class PAUSED(Sentinel, metaclass=Sentinel): # - Apache: <8 KiB per line> DEFAULT_MAX_INCOMPLETE_EVENT_SIZE = 16 * 1024 - # RFC 7230's rules for connection lifecycles: # - If either side says they want to close the connection, then the connection # must close. @@ -172,7 +160,7 @@ class Connection: self._max_incomplete_event_size = max_incomplete_event_size # State and role tracking if our_role not in (CLIENT, SERVER): - raise ValueError(f"expected CLIENT or SERVER, not {our_role!r}") + raise ValueError("expected CLIENT or SERVER, not {!r}".format(our_role)) self.our_role = our_role self.their_role: Type[Sentinel] if our_role is CLIENT: @@ -428,7 +416,7 @@ class Connection: # return that event, and then the state will change and we'll # get called again to generate the actual ConnectionClosed(). if hasattr(self._reader, "read_eof"): - event = self._reader.read_eof() + event = self._reader.read_eof() # type: ignore[attr-defined] else: event = ConnectionClosed() if event is None: @@ -500,20 +488,6 @@ class Connection: else: raise - @overload - def send(self, event: ConnectionClosed) -> None: - ... - - @overload - def send( - self, event: Union[Request, InformationalResponse, Response, Data, EndOfMessage] - ) -> bytes: - ... - - @overload - def send(self, event: Event) -> Optional[bytes]: - ... - def send(self, event: Event) -> Optional[bytes]: """Convert a high-level event into bytes that can be sent to the peer, while updating our internal state machine. diff --git a/Backend/venv/lib/python3.12/site-packages/h11/_events.py b/Backend/venv/lib/python3.12/site-packages/h11/_events.py index ca1c3adb..075bf8a4 100644 --- a/Backend/venv/lib/python3.12/site-packages/h11/_events.py +++ b/Backend/venv/lib/python3.12/site-packages/h11/_events.py @@ -7,8 +7,8 @@ import re from abc import ABC -from dataclasses import dataclass -from typing import List, Tuple, Union +from dataclasses import dataclass, field +from typing import Any, cast, Dict, List, Tuple, Union from ._abnf import method, request_target from ._headers import Headers, normalize_and_validate diff --git a/Backend/venv/lib/python3.12/site-packages/h11/_headers.py b/Backend/venv/lib/python3.12/site-packages/h11/_headers.py index 31da3e2b..b97d020b 100644 --- a/Backend/venv/lib/python3.12/site-packages/h11/_headers.py +++ b/Backend/venv/lib/python3.12/site-packages/h11/_headers.py @@ -12,8 +12,6 @@ try: except ImportError: from typing_extensions import Literal # type: ignore -CONTENT_LENGTH_MAX_DIGITS = 20 # allow up to 1 billion TB - 1 - # Facts # ----- @@ -175,8 +173,6 @@ def normalize_and_validate( raise LocalProtocolError("conflicting Content-Length headers") value = lengths.pop() validate(_content_length_re, value, "bad Content-Length") - if len(value) > CONTENT_LENGTH_MAX_DIGITS: - raise LocalProtocolError("bad Content-Length") if seen_content_length is None: seen_content_length = value new_headers.append((raw_name, name, value)) diff --git a/Backend/venv/lib/python3.12/site-packages/h11/_readers.py b/Backend/venv/lib/python3.12/site-packages/h11/_readers.py index 576804cc..08a9574d 100644 --- a/Backend/venv/lib/python3.12/site-packages/h11/_readers.py +++ b/Backend/venv/lib/python3.12/site-packages/h11/_readers.py @@ -148,9 +148,10 @@ chunk_header_re = re.compile(chunk_header.encode("ascii")) class ChunkedReader: def __init__(self) -> None: self._bytes_in_chunk = 0 - # After reading a chunk, we have to throw away the trailing \r\n. - # This tracks the bytes that we need to match and throw away. - self._bytes_to_discard = b"" + # After reading a chunk, we have to throw away the trailing \r\n; if + # this is >0 then we discard that many bytes before resuming regular + # de-chunkification. + self._bytes_to_discard = 0 self._reading_trailer = False def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]: @@ -159,19 +160,15 @@ class ChunkedReader: if lines is None: return None return EndOfMessage(headers=list(_decode_header_lines(lines))) - if self._bytes_to_discard: - data = buf.maybe_extract_at_most(len(self._bytes_to_discard)) + if self._bytes_to_discard > 0: + data = buf.maybe_extract_at_most(self._bytes_to_discard) if data is None: return None - if data != self._bytes_to_discard[: len(data)]: - raise LocalProtocolError( - f"malformed chunk footer: {data!r} (expected {self._bytes_to_discard!r})" - ) - self._bytes_to_discard = self._bytes_to_discard[len(data) :] - if self._bytes_to_discard: + self._bytes_to_discard -= len(data) + if self._bytes_to_discard > 0: return None # else, fall through and read some more - assert self._bytes_to_discard == b"" + assert self._bytes_to_discard == 0 if self._bytes_in_chunk == 0: # We need to refill our chunk count chunk_header = buf.maybe_extract_next_line() @@ -197,7 +194,7 @@ class ChunkedReader: return None self._bytes_in_chunk -= len(data) if self._bytes_in_chunk == 0: - self._bytes_to_discard = b"\r\n" + self._bytes_to_discard = 2 chunk_end = True else: chunk_end = False diff --git a/Backend/venv/lib/python3.12/site-packages/h11/_state.py b/Backend/venv/lib/python3.12/site-packages/h11/_state.py index 3ad444b0..3593430a 100644 --- a/Backend/venv/lib/python3.12/site-packages/h11/_state.py +++ b/Backend/venv/lib/python3.12/site-packages/h11/_state.py @@ -283,7 +283,9 @@ class ConnectionState: assert role is SERVER if server_switch_event not in self.pending_switch_proposals: raise LocalProtocolError( - "Received server _SWITCH_UPGRADE event without a pending proposal" + "Received server {} event without a pending proposal".format( + server_switch_event + ) ) _event_type = (event_type, server_switch_event) if server_switch_event is None and _event_type is Response: @@ -356,7 +358,7 @@ class ConnectionState: def start_next_cycle(self) -> None: if self.states != {CLIENT: DONE, SERVER: DONE}: raise LocalProtocolError( - f"not in a reusable state. self.states={self.states}" + "not in a reusable state. self.states={}".format(self.states) ) # Can't reach DONE/DONE with any of these active, but still, let's be # sure. diff --git a/Backend/venv/lib/python3.12/site-packages/h11/_version.py b/Backend/venv/lib/python3.12/site-packages/h11/_version.py index 76e7327b..4c891130 100644 --- a/Backend/venv/lib/python3.12/site-packages/h11/_version.py +++ b/Backend/venv/lib/python3.12/site-packages/h11/_version.py @@ -13,4 +13,4 @@ # want. (Contrast with the special suffix 1.0.0.dev, which sorts *before* # 1.0.0.) -__version__ = "0.16.0" +__version__ = "0.14.0" diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/__init__.py b/Backend/venv/lib/python3.12/site-packages/h11/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..004e72f307224a311c1b29d6df3c81f6cb72b652 GIT binary patch literal 192 zcmX@j%ge<81nP!LnIQTxh(HIQS%4zb87dhx8U0o=6fpsLpFwJV#p`F}=cekX=T+#t zq!wqFx8=sM-+XJ_W6>pLYTXQ$?+=$EDDmFeeXCg~ScmSp7T8S5Du=@(~~ zr0Ny`6(pvo7VBpi8tRv%7MB$3$H!;pWtPOp>lIY~;;_lhPbtkwwJTx;+Q$gQ#URE< MW=2NFB4!{90Mhs|TL1t6 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/helpers.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/helpers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..119bd908ec9a3692ed7d141fc99c86a8c32b0aa6 GIT binary patch literal 4393 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_against_stdlib_http.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_against_stdlib_http.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a6ad3b93f901db53515fea421861ba5eea02588a GIT binary patch literal 6958 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 HJ#6|nBiwr+ literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_connection.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_connection.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8df8751f38d47cd9e50c470446d6008e628cd007 GIT binary patch literal 58358 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 E0epWEo&W#< literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_events.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_events.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..31377192c486557ec2a35d402ae853bc57669c98 GIT binary patch literal 5547 zcmb_gTTC3+89sB}%h_dNx!(!cn8h^<2B)!u?F4XaC-!avaoVhEq4my~i}yli20I{- zQPl^pVhN%?gjA8jLm#kgsl1iPNU2)4N$ta$D60{bS|xpOA1WM(ka(#2pV=LT#Y@sU zIf!%4{QtlI?fmDQZ{{<%+linZ>S!G4@FMgTeJ~rFooqy5@&S^N#6(ey2{8mque`WhehQ7+O^wzl*{-!9q_VG~Uos$J zEVjj^Gs&JK;mYM25{@AzN2VZ;jMI|X8<~WM*xcB_le~5$*ATl)&kxV4TXOHU{Zw9m zEGs#yvDb=J(1$LcKxmrz5o*OPd8EQAW)ha+Dhu&_r{&w-{N7q~OGUZAg3ag!|6T|G zSgpuhUCD0N>ohmTjTW19Yn)E>n8zs&)+%QYfLD9=!0mpB<%iiRzWG1c(Mru2 zr4s7Zy;5@@PV-azXqm~iMybTATad_C-DNquh#$?ji^?r6&lIQScSWw>Pv=srG`*K9 zhY-|I)m?g`L7Nt)guF3UTHb*~o*0Kvjzqo)cb^l_vXYk9C40TzTP@F&K}&yiuKqk3 zd=~$$6{`7O{neTEfHS)GUY)tqro}07w8rFEqf~3DsbAT( zCX+U`a~eqXmZb^QggTJQ++Z2>Hr#3qn&WQ2^)B}|y1_ifVQ>F_U(Wmcaz5IZ^Y%U* z=Jr4KvNoKMbWsZ{ zL$apZhUKs%E2{1q*0iy~>o5f@LZ#bPy7xfK2CXSjWQ<{>zhdw}2GsMz!2xX&%kPF` zV^O&+l8AvQg$%<9RZ}rca#%Wl4(JL!(I*AucDk_6y^=`iTv(NStkOW~BFq`~rfL9> zhMWJMEC4qe{0$U&`5}(S5=en*tQs(EFp_`}$_<)Q2ZKVZH0xr=j=kw~~*n~#0nW%(ER9RKrR3!~lq5DXo z2k|Zojw>J?R+R9BE{4a({%=9#;y*TnYdpq)@D+h|uZ0B9AFPht4VTJG%W! zSQ?BBhn38XDm1LgyF%mZpiYB}U{2CX?fv$e_E)pFk>N9z;B3(l_#O4G#^3&o3* zi_!a~n;h$L-r}|d;B2}O_Rg0ss7s-x3-`xYotI{M|Bef0Pb5#wm!)t$;p+*mPiHA5 zDO^kVT7qjS<;CQS^WF1pDcnN%7J^%V_9d=f!PWEEQ}`g^4-$NEQw05KPf6P2O&3*@ z+AdOfEL~c)=@76u)4N`PT%L>`;_D$*y(!#F_+Em0;djo$-BE~wbIph%U0jwfZCtoW zN{*&0>p+na3kUqo!vFs_>t0ma^bN8J1-I;L?&8_Y$;+$NM;^J`o*>atu;#5ym)E4r ztJ0Oti=|7#vVEofCDQUTspv`9HiGTqf?K^?97wKH@pTULY?dLxWky^_8cvYH7a`(r z9fBaP7oyUZZ;)tME2^6B`gQ#+F>NoHbtm0(!>jiCJuYzIf~y!@Sa@qGwlqive*P`T zx(l{AV5}qB*+AE(H&5JlPrA4%U0eydnhQdrz`nx?u4d9yzu384yX;wYoSW%MWB2T- z|VP0h5hUnM=7w7u95|Av42@!#eFk`MIB^a1G&W2(J0sS@vGrOs}~a z8ZCyS@{yx@re_U%=GgaMAmxWs_%PuQui(R5mpIsW#=Q(L_#us&KAeXKk;b+T4}F=3e&eB^*;a4B0r8uni>J9y z1#iH`efokoP|5wFy*$t?{IRAe&?)?>uqcSQKl7fToBPW#Pq2#n?4T!ji2J#*>OMo(;` z<=11PEKBaYHJkF|Oy(Wi`rY^F-+2=s&%T{cARiY{DEw5Kq4SA~abwYp>1^;d;}X+( zZDLH;xskYbhtU~DrD&l+d7jEt7#9O@3fMF7uhQk1LA0`qJ_>z&`NPXg zJ)c}!wZE3B@RP}tAHDkV#Sbs0u#fOQf_=1znGf9WyC31Y-%9sR{O;tPlaHE@ui)c^ zKTh!R9DdV^ar8sK0cKFn($fVqmi}Kb*}e3cPahHwucN{}(fatIQE8(=8uJu|nf5)O zH}XhIcA8SjVU<9Bjgmuh?*90YUutycqfOR57)*#Q;oQ#`u%PaM;sFVz5IVqH6qQ@x zEujnnta4hoXVQ61mdB2Sqa%O}>D#taLOU9iVto5z4ZyXr8|E2GR9Z}toljvK^wCn~ zV-2)rAi*Bsz%v!F>?*yK${V;!=c01lfK<9e3Evn5P!dy>ZV=dAYhI()GH7-%5>^4* zD6fD_lFH3N2UrB|q3rG7LRw!Tg=aIjzeAjP8L)@TbL*YiiR8q4r{>>C;X1fuLhOQhta4rNLdCF^Y^vJ*w}ONm9PZG;pA&0SNp`A~OP zeh?071pNshAo;5U1dWj|8%_cDuU~DGrfu2;!CItIVe1bAO+Nw@sA4J*s-O1WC0C>< z(M~I?OK^7P-ZOX3%+B4pGyA#ISwq2fDA*eR$1aNc8y2jKDQ5CNgv`$9Ut?=N+TbVUAJ5CG=3@*63o;GcJk*nf?-6cQby?-WZC6QeIaPf;p!l6s3S!+^XB25p+x1)G&C-8F30 zysX+i6kXmnDcOZ_HGg-CPYQ6`vu%62w%;u0%|6by#A$Y`-#?))m~+x9Zyb$F)<|!! zr~KpCY;zQa9kgwKy0$kwp_<3pmc-H%y7vEgLXO9=+2#{+?x1bg4%)8WLEG*fv|aaD z+oktveF-NuYJte!FId1*Fyx`cl z!eL4BmckyaF)VsEYp)2Hd>N35BW?vNzS@>5~ z&2Oq|eqL4cW>w7_RW+?uHFOmXv+%I0rW$Kzp&Dyup&Dyu;bhfzd{s4k6%D=c+p3xm zs%mDcYMKffU(1A#G06FJ+92m5ijT?TbcLdMHzg{Dw;=HR5UHcQ!#^&&d#SU$`U*otgcxAxd>2kwAV4p0VqM8@%f31qfn<%w#72> zx%jux8y~L-h~Va~YOsGZI3*HSWmUmg-XN-`7%c3@V6Rpu<4!BAY7uQ-4bR3?8gBdK zE75rBd|JhE@vcP!v+1N9h^4LsUY6C1T6!)pp4Q|<`|))8B5)5Jk3=uZDJgJCPF)Hl z;^zX;>TEjI-4W~xsBukhp94T7CaZzjU@(A$R5LB^B-M_&tHyp5hqvA^Du;jvJ~p+9 zrKs!F1F_}i3wmo;R_xaM$MwDwi{c60c0w0VfJQ&qt+yV@iaq+^NqykdqIgQTozlfq zD;!nhey8c}rraK1*539FMYG)xgl65_se6Jsp=*_Z;46FWlOQX?20P1Or|(wGv~)!G z^yGwIwAA-dYG82jMNduOtH+wO^Nx~)wY+a6RlFlg57(>?t;VE}6mZb9^a1#PC< zTcp~pdyeFU9&E7pp}70~gEwLyB;QMB_xbOMe%-3ZpBG6qpU` zo)>b$i%4sPE)Ld9dh5$sadb&&gTXqvRf99qYuUUcZS~{{)>n{ zb1S+i26bCd7lRvS<1z)kFl#S4+x5U{-EpSWyJdLB4V!J;6N9TE27KJ~{IykEDJy`# zqO@;ql>-OF>|nJlV0#hJYXYn+80$xAf2FhxutnAd^9ZXAn{Z55gxG36s5Z!9lx>mc=?@S6O!bJ9jW_B$u*$S3=plg;FJEzZfqEajb*vb9;VlzAWJimCDo$TQk z``Hk~XIVB>%V+D^&;dT{VM9JX>t{o~e0G4HB7BZxr`&w5ft_mNb6$3;jnB2SQ+<4H zke#;i_jz`@j=$f?PB-)STiEHt{QVAgx}U#4#D1UUqtNB`3zta@e)0!k@hU$jgnbcR z+84ooVLhV11}CT1sU4umhvjF8u;3IfK-nZ#;vkMM)&v{ant5Ho<+kF|)=$o?U88w= zxX~hCZgH!f0WcD(GO|l}PPESF8U<*v9KCsuu+UbF866Wo0#RcP#7HSP!;Pm?1{+PJ zRT+fBy7x>+lgwXyLm52VI3W=w+z=amQE$`I(o@K=8c2}CrmXCE;(drh8jwcPcw z6*uKLaAWM|k08QX>^r+Cp4DwtpZ=u7G}n(AE9$ zqQV#V@S2K2{l|9qafW=tu*b!XE{M<1bioIZDR;q=%SkkTxR7iy<|EdczLt|z9^O~h ziqV_vC>00(m37d`HrIMmOUO^+KN9W~KMhz63$0zHeD(>0jEzpM(E)G{+jv^lGNODX zlAKG(9q_FLgxsk4fh6OAFC^`_>%cE0K4Ng_RB6fJw1@%|L>Yvkf(bYRw*nTt(!^#+ zu%pvwQ;vZ~IgSZJ4acme&4s~1Rq$J=h#x8xqZav0C)@j z>MtO{aJuz@P`lKycd23b(k{Q=F|IeBSm9whu8@tB#YMRf0rcJZn<2gXq~3KZC!B_b z*NtJ56+6M|d+W5^R&@}y5%K3MyX_et<=35FA7eyb+a}zY(l|RRLX>R2edOuxY{T~Im BXT<;j literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_helpers.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_helpers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76f96902b96472a17faf0d8338f4867be60b9a92 GIT binary patch literal 1200 zcmah|&1(}u6rbJfXR;}wAJ9gTQm`7NUACW>;-Md)f+^J7UcxprO_uCOoS78kNyJ+{ zR21s5$4dK$2wrnqa??w1sZk0&IkVj~ixi#1Z{PdP@4dWv@8w%Qp9SnZux8w127vF} z85}V&`fJQM0uVsK1HNDjzG#bn#?JVXE%~x7`--jjs;vqDiZJ78zHaNxR-pV&va@Ze zq@MF!B{eE+gh7C+)D43TFC+*?(xyY5$k+9^FK8{X6Z*KMTK|Ee%Q)Yat1LjZ{Q47?TEpfpnM^EO&+L=qoN>@A#sWn4*& za)dgxMJm-0vf%WHH927?)#QiE)+i?ph1+XK{(}ek`x7mSmh`7M2)9`fi&d z2Ve?~nomNCyt(x-Y`8(qTz9Gs6hM>h&%Wuo zyJoXZ>tV1sZ!MU_rD(3n1Z-1@S+^{cvSlPOR_e%Wvg+rX?WoA<%B>rT@3JcRB9adb z-hggy`iD{Y#7@cG5bOh=pRYDU70AB=Ekwxdn;a!OYDIW=l1JuoUO7l2u~$f{z4Q81z{ z9F$ABNKO7rn72A^aNJO=aF?$T);RD^^AW~PxE=<7DxTuNv*J4puCz5jge?y}#?#E@ gt3mED=w$>!xD+)(yaWs`IQ$+3!lPfxm~dUyA1sRq+W-In literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_io.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_io.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..735cff37f76ee3439bcbf9a0da3477962b9f402e GIT binary patch literal 20029 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 al^tHDXQkE89D|A;=EcW-HV2~}?e literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_util.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/h11/tests/__pycache__/test_util.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ceb6ae5c1a843287f88d1a129b96114e00ba2096 GIT binary patch literal 6182 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/data/test-file b/Backend/venv/lib/python3.12/site-packages/h11/tests/data/test-file new file mode 100644 index 00000000..d0be0a6c --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/h11/tests/data/test-file @@ -0,0 +1 @@ +92b12bc045050b55b848d37167a1a63947c364579889ce1d39788e45e9fac9e5 diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/helpers.py b/Backend/venv/lib/python3.12/site-packages/h11/tests/helpers.py new file mode 100644 index 00000000..571be444 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/h11/tests/helpers.py @@ -0,0 +1,101 @@ +from typing import cast, List, Type, Union, ValuesView + +from .._connection import Connection, NEED_DATA, PAUSED +from .._events import ( + ConnectionClosed, + Data, + EndOfMessage, + Event, + InformationalResponse, + Request, + Response, +) +from .._state import CLIENT, CLOSED, DONE, MUST_CLOSE, SERVER +from .._util import Sentinel + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal # type: ignore + + +def get_all_events(conn: Connection) -> List[Event]: + got_events = [] + while True: + event = conn.next_event() + if event in (NEED_DATA, PAUSED): + break + event = cast(Event, event) + got_events.append(event) + if type(event) is ConnectionClosed: + break + return got_events + + +def receive_and_get(conn: Connection, data: bytes) -> List[Event]: + conn.receive_data(data) + return get_all_events(conn) + + +# Merges adjacent Data events, converts payloads to bytestrings, and removes +# chunk boundaries. +def normalize_data_events(in_events: List[Event]) -> List[Event]: + out_events: List[Event] = [] + for event in in_events: + if type(event) is Data: + event = Data(data=bytes(event.data), chunk_start=False, chunk_end=False) + if out_events and type(out_events[-1]) is type(event) is Data: + out_events[-1] = Data( + data=out_events[-1].data + event.data, + chunk_start=out_events[-1].chunk_start, + chunk_end=out_events[-1].chunk_end, + ) + else: + out_events.append(event) + return out_events + + +# Given that we want to write tests that push some events through a Connection +# and check that its state updates appropriately... we might as make a habit +# of pushing them through two Connections with a fake network link in +# between. +class ConnectionPair: + def __init__(self) -> None: + self.conn = {CLIENT: Connection(CLIENT), SERVER: Connection(SERVER)} + self.other = {CLIENT: SERVER, SERVER: CLIENT} + + @property + def conns(self) -> ValuesView[Connection]: + return self.conn.values() + + # expect="match" if expect=send_events; expect=[...] to say what expected + def send( + self, + role: Type[Sentinel], + send_events: Union[List[Event], Event], + expect: Union[List[Event], Event, Literal["match"]] = "match", + ) -> bytes: + if not isinstance(send_events, list): + send_events = [send_events] + data = b"" + closed = False + for send_event in send_events: + new_data = self.conn[role].send(send_event) + if new_data is None: + closed = True + else: + data += new_data + # send uses b"" to mean b"", and None to mean closed + # receive uses b"" to mean closed, and None to mean "try again" + # so we have to translate between the two conventions + if data: + self.conn[self.other[role]].receive_data(data) + if closed: + self.conn[self.other[role]].receive_data(b"") + got_events = get_all_events(self.conn[self.other[role]]) + if expect == "match": + expect = send_events + if not isinstance(expect, list): + expect = [expect] + assert got_events == expect + return data diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/test_against_stdlib_http.py b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_against_stdlib_http.py new file mode 100644 index 00000000..d2ee1314 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_against_stdlib_http.py @@ -0,0 +1,115 @@ +import json +import os.path +import socket +import socketserver +import threading +from contextlib import closing, contextmanager +from http.server import SimpleHTTPRequestHandler +from typing import Callable, Generator +from urllib.request import urlopen + +import h11 + + +@contextmanager +def socket_server( + handler: Callable[..., socketserver.BaseRequestHandler] +) -> Generator[socketserver.TCPServer, None, None]: + httpd = socketserver.TCPServer(("127.0.0.1", 0), handler) + thread = threading.Thread( + target=httpd.serve_forever, kwargs={"poll_interval": 0.01} + ) + thread.daemon = True + try: + thread.start() + yield httpd + finally: + httpd.shutdown() + + +test_file_path = os.path.join(os.path.dirname(__file__), "data/test-file") +with open(test_file_path, "rb") as f: + test_file_data = f.read() + + +class SingleMindedRequestHandler(SimpleHTTPRequestHandler): + def translate_path(self, path: str) -> str: + return test_file_path + + +def test_h11_as_client() -> None: + with socket_server(SingleMindedRequestHandler) as httpd: + with closing(socket.create_connection(httpd.server_address)) as s: + c = h11.Connection(h11.CLIENT) + + s.sendall( + c.send( # type: ignore[arg-type] + h11.Request( + method="GET", target="/foo", headers=[("Host", "localhost")] + ) + ) + ) + s.sendall(c.send(h11.EndOfMessage())) # type: ignore[arg-type] + + data = bytearray() + while True: + event = c.next_event() + print(event) + if event is h11.NEED_DATA: + # Use a small read buffer to make things more challenging + # and exercise more paths :-) + c.receive_data(s.recv(10)) + continue + if type(event) is h11.Response: + assert event.status_code == 200 + if type(event) is h11.Data: + data += event.data + if type(event) is h11.EndOfMessage: + break + assert bytes(data) == test_file_data + + +class H11RequestHandler(socketserver.BaseRequestHandler): + def handle(self) -> None: + with closing(self.request) as s: + c = h11.Connection(h11.SERVER) + request = None + while True: + event = c.next_event() + if event is h11.NEED_DATA: + # Use a small read buffer to make things more challenging + # and exercise more paths :-) + c.receive_data(s.recv(10)) + continue + if type(event) is h11.Request: + request = event + if type(event) is h11.EndOfMessage: + break + assert request is not None + info = json.dumps( + { + "method": request.method.decode("ascii"), + "target": request.target.decode("ascii"), + "headers": { + name.decode("ascii"): value.decode("ascii") + for (name, value) in request.headers + }, + } + ) + s.sendall(c.send(h11.Response(status_code=200, headers=[]))) # type: ignore[arg-type] + s.sendall(c.send(h11.Data(data=info.encode("ascii")))) + s.sendall(c.send(h11.EndOfMessage())) + + +def test_h11_as_server() -> None: + with socket_server(H11RequestHandler) as httpd: + host, port = httpd.server_address + url = "http://{}:{}/some-path".format(host, port) + with closing(urlopen(url)) as f: + assert f.getcode() == 200 + data = f.read() + info = json.loads(data.decode("ascii")) + print(info) + assert info["method"] == "GET" + assert info["target"] == "/some-path" + assert "urllib" in info["headers"]["user-agent"] diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/test_connection.py b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_connection.py new file mode 100644 index 00000000..73a27b98 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_connection.py @@ -0,0 +1,1122 @@ +from typing import Any, cast, Dict, List, Optional, Tuple, Type + +import pytest + +from .._connection import _body_framing, _keep_alive, Connection, NEED_DATA, PAUSED +from .._events import ( + ConnectionClosed, + Data, + EndOfMessage, + Event, + InformationalResponse, + Request, + Response, +) +from .._state import ( + CLIENT, + CLOSED, + DONE, + ERROR, + IDLE, + MIGHT_SWITCH_PROTOCOL, + MUST_CLOSE, + SEND_BODY, + SEND_RESPONSE, + SERVER, + SWITCHED_PROTOCOL, +) +from .._util import LocalProtocolError, RemoteProtocolError, Sentinel +from .helpers import ConnectionPair, get_all_events, receive_and_get + + +def test__keep_alive() -> None: + assert _keep_alive( + Request(method="GET", target="/", headers=[("Host", "Example.com")]) + ) + assert not _keep_alive( + Request( + method="GET", + target="/", + headers=[("Host", "Example.com"), ("Connection", "close")], + ) + ) + assert not _keep_alive( + Request( + method="GET", + target="/", + headers=[("Host", "Example.com"), ("Connection", "a, b, cLOse, foo")], + ) + ) + assert not _keep_alive( + Request(method="GET", target="/", headers=[], http_version="1.0") # type: ignore[arg-type] + ) + + assert _keep_alive(Response(status_code=200, headers=[])) # type: ignore[arg-type] + assert not _keep_alive(Response(status_code=200, headers=[("Connection", "close")])) + assert not _keep_alive( + Response(status_code=200, headers=[("Connection", "a, b, cLOse, foo")]) + ) + assert not _keep_alive(Response(status_code=200, headers=[], http_version="1.0")) # type: ignore[arg-type] + + +def test__body_framing() -> None: + def headers(cl: Optional[int], te: bool) -> List[Tuple[str, str]]: + headers = [] + if cl is not None: + headers.append(("Content-Length", str(cl))) + if te: + headers.append(("Transfer-Encoding", "chunked")) + return headers + + def resp( + status_code: int = 200, cl: Optional[int] = None, te: bool = False + ) -> Response: + return Response(status_code=status_code, headers=headers(cl, te)) + + def req(cl: Optional[int] = None, te: bool = False) -> Request: + h = headers(cl, te) + h += [("Host", "example.com")] + return Request(method="GET", target="/", headers=h) + + # Special cases where the headers are ignored: + for kwargs in [{}, {"cl": 100}, {"te": True}, {"cl": 100, "te": True}]: + kwargs = cast(Dict[str, Any], kwargs) + for meth, r in [ + (b"HEAD", resp(**kwargs)), + (b"GET", resp(status_code=204, **kwargs)), + (b"GET", resp(status_code=304, **kwargs)), + ]: + assert _body_framing(meth, r) == ("content-length", (0,)) + + # Transfer-encoding + for kwargs in [{"te": True}, {"cl": 100, "te": True}]: + kwargs = cast(Dict[str, Any], kwargs) + for meth, r in [(None, req(**kwargs)), (b"GET", resp(**kwargs))]: # type: ignore + assert _body_framing(meth, r) == ("chunked", ()) + + # Content-Length + for meth, r in [(None, req(cl=100)), (b"GET", resp(cl=100))]: # type: ignore + assert _body_framing(meth, r) == ("content-length", (100,)) + + # No headers + assert _body_framing(None, req()) == ("content-length", (0,)) # type: ignore + assert _body_framing(b"GET", resp()) == ("http/1.0", ()) + + +def test_Connection_basics_and_content_length() -> None: + with pytest.raises(ValueError): + Connection("CLIENT") # type: ignore + + p = ConnectionPair() + assert p.conn[CLIENT].our_role is CLIENT + assert p.conn[CLIENT].their_role is SERVER + assert p.conn[SERVER].our_role is SERVER + assert p.conn[SERVER].their_role is CLIENT + + data = p.send( + CLIENT, + Request( + method="GET", + target="/", + headers=[("Host", "example.com"), ("Content-Length", "10")], + ), + ) + assert data == ( + b"GET / HTTP/1.1\r\n" b"Host: example.com\r\n" b"Content-Length: 10\r\n\r\n" + ) + + for conn in p.conns: + assert conn.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE} + assert p.conn[CLIENT].our_state is SEND_BODY + assert p.conn[CLIENT].their_state is SEND_RESPONSE + assert p.conn[SERVER].our_state is SEND_RESPONSE + assert p.conn[SERVER].their_state is SEND_BODY + + assert p.conn[CLIENT].their_http_version is None + assert p.conn[SERVER].their_http_version == b"1.1" + + data = p.send(SERVER, InformationalResponse(status_code=100, headers=[])) # type: ignore[arg-type] + assert data == b"HTTP/1.1 100 \r\n\r\n" + + data = p.send(SERVER, Response(status_code=200, headers=[("Content-Length", "11")])) + assert data == b"HTTP/1.1 200 \r\nContent-Length: 11\r\n\r\n" + + for conn in p.conns: + assert conn.states == {CLIENT: SEND_BODY, SERVER: SEND_BODY} + + assert p.conn[CLIENT].their_http_version == b"1.1" + assert p.conn[SERVER].their_http_version == b"1.1" + + data = p.send(CLIENT, Data(data=b"12345")) + assert data == b"12345" + data = p.send( + CLIENT, Data(data=b"67890"), expect=[Data(data=b"67890"), EndOfMessage()] + ) + assert data == b"67890" + data = p.send(CLIENT, EndOfMessage(), expect=[]) + assert data == b"" + + for conn in p.conns: + assert conn.states == {CLIENT: DONE, SERVER: SEND_BODY} + + data = p.send(SERVER, Data(data=b"1234567890")) + assert data == b"1234567890" + data = p.send(SERVER, Data(data=b"1"), expect=[Data(data=b"1"), EndOfMessage()]) + assert data == b"1" + data = p.send(SERVER, EndOfMessage(), expect=[]) + assert data == b"" + + for conn in p.conns: + assert conn.states == {CLIENT: DONE, SERVER: DONE} + + +def test_chunked() -> None: + p = ConnectionPair() + + p.send( + CLIENT, + Request( + method="GET", + target="/", + headers=[("Host", "example.com"), ("Transfer-Encoding", "chunked")], + ), + ) + data = p.send(CLIENT, Data(data=b"1234567890", chunk_start=True, chunk_end=True)) + assert data == b"a\r\n1234567890\r\n" + data = p.send(CLIENT, Data(data=b"abcde", chunk_start=True, chunk_end=True)) + assert data == b"5\r\nabcde\r\n" + data = p.send(CLIENT, Data(data=b""), expect=[]) + assert data == b"" + data = p.send(CLIENT, EndOfMessage(headers=[("hello", "there")])) + assert data == b"0\r\nhello: there\r\n\r\n" + + p.send( + SERVER, Response(status_code=200, headers=[("Transfer-Encoding", "chunked")]) + ) + p.send(SERVER, Data(data=b"54321", chunk_start=True, chunk_end=True)) + p.send(SERVER, Data(data=b"12345", chunk_start=True, chunk_end=True)) + p.send(SERVER, EndOfMessage()) + + for conn in p.conns: + assert conn.states == {CLIENT: DONE, SERVER: DONE} + + +def test_chunk_boundaries() -> None: + conn = Connection(our_role=SERVER) + + request = ( + b"POST / HTTP/1.1\r\n" + b"Host: example.com\r\n" + b"Transfer-Encoding: chunked\r\n" + b"\r\n" + ) + conn.receive_data(request) + assert conn.next_event() == Request( + method="POST", + target="/", + headers=[("Host", "example.com"), ("Transfer-Encoding", "chunked")], + ) + assert conn.next_event() is NEED_DATA + + conn.receive_data(b"5\r\nhello\r\n") + assert conn.next_event() == Data(data=b"hello", chunk_start=True, chunk_end=True) + + conn.receive_data(b"5\r\nhel") + assert conn.next_event() == Data(data=b"hel", chunk_start=True, chunk_end=False) + + conn.receive_data(b"l") + assert conn.next_event() == Data(data=b"l", chunk_start=False, chunk_end=False) + + conn.receive_data(b"o\r\n") + assert conn.next_event() == Data(data=b"o", chunk_start=False, chunk_end=True) + + conn.receive_data(b"5\r\nhello") + assert conn.next_event() == Data(data=b"hello", chunk_start=True, chunk_end=True) + + conn.receive_data(b"\r\n") + assert conn.next_event() == NEED_DATA + + conn.receive_data(b"0\r\n\r\n") + assert conn.next_event() == EndOfMessage() + + +def test_client_talking_to_http10_server() -> None: + c = Connection(CLIENT) + c.send(Request(method="GET", target="/", headers=[("Host", "example.com")])) + c.send(EndOfMessage()) + assert c.our_state is DONE + # No content-length, so Http10 framing for body + assert receive_and_get(c, b"HTTP/1.0 200 OK\r\n\r\n") == [ + Response(status_code=200, headers=[], http_version="1.0", reason=b"OK") # type: ignore[arg-type] + ] + assert c.our_state is MUST_CLOSE + assert receive_and_get(c, b"12345") == [Data(data=b"12345")] + assert receive_and_get(c, b"67890") == [Data(data=b"67890")] + assert receive_and_get(c, b"") == [EndOfMessage(), ConnectionClosed()] + assert c.their_state is CLOSED + + +def test_server_talking_to_http10_client() -> None: + c = Connection(SERVER) + # No content-length, so no body + # NB: no host header + assert receive_and_get(c, b"GET / HTTP/1.0\r\n\r\n") == [ + Request(method="GET", target="/", headers=[], http_version="1.0"), # type: ignore[arg-type] + EndOfMessage(), + ] + assert c.their_state is MUST_CLOSE + + # We automatically Connection: close back at them + assert ( + c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type] + == b"HTTP/1.1 200 \r\nConnection: close\r\n\r\n" + ) + + assert c.send(Data(data=b"12345")) == b"12345" + assert c.send(EndOfMessage()) == b"" + assert c.our_state is MUST_CLOSE + + # Check that it works if they do send Content-Length + c = Connection(SERVER) + # NB: no host header + assert receive_and_get(c, b"POST / HTTP/1.0\r\nContent-Length: 10\r\n\r\n1") == [ + Request( + method="POST", + target="/", + headers=[("Content-Length", "10")], + http_version="1.0", + ), + Data(data=b"1"), + ] + assert receive_and_get(c, b"234567890") == [Data(data=b"234567890"), EndOfMessage()] + assert c.their_state is MUST_CLOSE + assert receive_and_get(c, b"") == [ConnectionClosed()] + + +def test_automatic_transfer_encoding_in_response() -> None: + # Check that in responses, the user can specify either Transfer-Encoding: + # chunked or no framing at all, and in both cases we automatically select + # the right option depending on whether the peer speaks HTTP/1.0 or + # HTTP/1.1 + for user_headers in [ + [("Transfer-Encoding", "chunked")], + [], + # In fact, this even works if Content-Length is set, + # because if both are set then Transfer-Encoding wins + [("Transfer-Encoding", "chunked"), ("Content-Length", "100")], + ]: + user_headers = cast(List[Tuple[str, str]], user_headers) + p = ConnectionPair() + p.send( + CLIENT, + [ + Request(method="GET", target="/", headers=[("Host", "example.com")]), + EndOfMessage(), + ], + ) + # When speaking to HTTP/1.1 client, all of the above cases get + # normalized to Transfer-Encoding: chunked + p.send( + SERVER, + Response(status_code=200, headers=user_headers), + expect=Response( + status_code=200, headers=[("Transfer-Encoding", "chunked")] + ), + ) + + # When speaking to HTTP/1.0 client, all of the above cases get + # normalized to no-framing-headers + c = Connection(SERVER) + receive_and_get(c, b"GET / HTTP/1.0\r\n\r\n") + assert ( + c.send(Response(status_code=200, headers=user_headers)) + == b"HTTP/1.1 200 \r\nConnection: close\r\n\r\n" + ) + assert c.send(Data(data=b"12345")) == b"12345" + + +def test_automagic_connection_close_handling() -> None: + p = ConnectionPair() + # If the user explicitly sets Connection: close, then we notice and + # respect it + p.send( + CLIENT, + [ + Request( + method="GET", + target="/", + headers=[("Host", "example.com"), ("Connection", "close")], + ), + EndOfMessage(), + ], + ) + for conn in p.conns: + assert conn.states[CLIENT] is MUST_CLOSE + # And if the client sets it, the server automatically echoes it back + p.send( + SERVER, + # no header here... + [Response(status_code=204, headers=[]), EndOfMessage()], # type: ignore[arg-type] + # ...but oh look, it arrived anyway + expect=[ + Response(status_code=204, headers=[("connection", "close")]), + EndOfMessage(), + ], + ) + for conn in p.conns: + assert conn.states == {CLIENT: MUST_CLOSE, SERVER: MUST_CLOSE} + + +def test_100_continue() -> None: + def setup() -> ConnectionPair: + p = ConnectionPair() + p.send( + CLIENT, + Request( + method="GET", + target="/", + headers=[ + ("Host", "example.com"), + ("Content-Length", "100"), + ("Expect", "100-continue"), + ], + ), + ) + for conn in p.conns: + assert conn.client_is_waiting_for_100_continue + assert not p.conn[CLIENT].they_are_waiting_for_100_continue + assert p.conn[SERVER].they_are_waiting_for_100_continue + return p + + # Disabled by 100 Continue + p = setup() + p.send(SERVER, InformationalResponse(status_code=100, headers=[])) # type: ignore[arg-type] + for conn in p.conns: + assert not conn.client_is_waiting_for_100_continue + assert not conn.they_are_waiting_for_100_continue + + # Disabled by a real response + p = setup() + p.send( + SERVER, Response(status_code=200, headers=[("Transfer-Encoding", "chunked")]) + ) + for conn in p.conns: + assert not conn.client_is_waiting_for_100_continue + assert not conn.they_are_waiting_for_100_continue + + # Disabled by the client going ahead and sending stuff anyway + p = setup() + p.send(CLIENT, Data(data=b"12345")) + for conn in p.conns: + assert not conn.client_is_waiting_for_100_continue + assert not conn.they_are_waiting_for_100_continue + + +def test_max_incomplete_event_size_countermeasure() -> None: + # Infinitely long headers are definitely not okay + c = Connection(SERVER) + c.receive_data(b"GET / HTTP/1.0\r\nEndless: ") + assert c.next_event() is NEED_DATA + with pytest.raises(RemoteProtocolError): + while True: + c.receive_data(b"a" * 1024) + c.next_event() + + # Checking that the same header is accepted / rejected depending on the + # max_incomplete_event_size setting: + c = Connection(SERVER, max_incomplete_event_size=5000) + c.receive_data(b"GET / HTTP/1.0\r\nBig: ") + c.receive_data(b"a" * 4000) + c.receive_data(b"\r\n\r\n") + assert get_all_events(c) == [ + Request( + method="GET", target="/", http_version="1.0", headers=[("big", "a" * 4000)] + ), + EndOfMessage(), + ] + + c = Connection(SERVER, max_incomplete_event_size=4000) + c.receive_data(b"GET / HTTP/1.0\r\nBig: ") + c.receive_data(b"a" * 4000) + with pytest.raises(RemoteProtocolError): + c.next_event() + + # Temporarily exceeding the size limit is fine, as long as its done with + # complete events: + c = Connection(SERVER, max_incomplete_event_size=5000) + c.receive_data(b"GET / HTTP/1.0\r\nContent-Length: 10000") + c.receive_data(b"\r\n\r\n" + b"a" * 10000) + assert get_all_events(c) == [ + Request( + method="GET", + target="/", + http_version="1.0", + headers=[("Content-Length", "10000")], + ), + Data(data=b"a" * 10000), + EndOfMessage(), + ] + + c = Connection(SERVER, max_incomplete_event_size=100) + # Two pipelined requests to create a way-too-big receive buffer... but + # it's fine because we're not checking + c.receive_data( + b"GET /1 HTTP/1.1\r\nHost: a\r\n\r\n" + b"GET /2 HTTP/1.1\r\nHost: b\r\n\r\n" + b"X" * 1000 + ) + assert get_all_events(c) == [ + Request(method="GET", target="/1", headers=[("host", "a")]), + EndOfMessage(), + ] + # Even more data comes in, still no problem + c.receive_data(b"X" * 1000) + # We can respond and reuse to get the second pipelined request + c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type] + c.send(EndOfMessage()) + c.start_next_cycle() + assert get_all_events(c) == [ + Request(method="GET", target="/2", headers=[("host", "b")]), + EndOfMessage(), + ] + # But once we unpause and try to read the next message, and find that it's + # incomplete and the buffer is *still* way too large, then *that's* a + # problem: + c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type] + c.send(EndOfMessage()) + c.start_next_cycle() + with pytest.raises(RemoteProtocolError): + c.next_event() + + +def test_reuse_simple() -> None: + p = ConnectionPair() + p.send( + CLIENT, + [Request(method="GET", target="/", headers=[("Host", "a")]), EndOfMessage()], + ) + p.send( + SERVER, + [ + Response(status_code=200, headers=[(b"transfer-encoding", b"chunked")]), + EndOfMessage(), + ], + ) + for conn in p.conns: + assert conn.states == {CLIENT: DONE, SERVER: DONE} + conn.start_next_cycle() + + p.send( + CLIENT, + [ + Request(method="DELETE", target="/foo", headers=[("Host", "a")]), + EndOfMessage(), + ], + ) + p.send( + SERVER, + [ + Response(status_code=404, headers=[(b"transfer-encoding", b"chunked")]), + EndOfMessage(), + ], + ) + + +def test_pipelining() -> None: + # Client doesn't support pipelining, so we have to do this by hand + c = Connection(SERVER) + assert c.next_event() is NEED_DATA + # 3 requests all bunched up + c.receive_data( + b"GET /1 HTTP/1.1\r\nHost: a.com\r\nContent-Length: 5\r\n\r\n" + b"12345" + b"GET /2 HTTP/1.1\r\nHost: a.com\r\nContent-Length: 5\r\n\r\n" + b"67890" + b"GET /3 HTTP/1.1\r\nHost: a.com\r\n\r\n" + ) + assert get_all_events(c) == [ + Request( + method="GET", + target="/1", + headers=[("Host", "a.com"), ("Content-Length", "5")], + ), + Data(data=b"12345"), + EndOfMessage(), + ] + assert c.their_state is DONE + assert c.our_state is SEND_RESPONSE + + assert c.next_event() is PAUSED + + c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type] + c.send(EndOfMessage()) + assert c.their_state is DONE + assert c.our_state is DONE + + c.start_next_cycle() + + assert get_all_events(c) == [ + Request( + method="GET", + target="/2", + headers=[("Host", "a.com"), ("Content-Length", "5")], + ), + Data(data=b"67890"), + EndOfMessage(), + ] + assert c.next_event() is PAUSED + c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type] + c.send(EndOfMessage()) + c.start_next_cycle() + + assert get_all_events(c) == [ + Request(method="GET", target="/3", headers=[("Host", "a.com")]), + EndOfMessage(), + ] + # Doesn't pause this time, no trailing data + assert c.next_event() is NEED_DATA + c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type] + c.send(EndOfMessage()) + + # Arrival of more data triggers pause + assert c.next_event() is NEED_DATA + c.receive_data(b"SADF") + assert c.next_event() is PAUSED + assert c.trailing_data == (b"SADF", False) + # If EOF arrives while paused, we don't see that either: + c.receive_data(b"") + assert c.trailing_data == (b"SADF", True) + assert c.next_event() is PAUSED + c.receive_data(b"") + assert c.next_event() is PAUSED + # Can't call receive_data with non-empty buf after closing it + with pytest.raises(RuntimeError): + c.receive_data(b"FDSA") + + +def test_protocol_switch() -> None: + for (req, deny, accept) in [ + ( + Request( + method="CONNECT", + target="example.com:443", + headers=[("Host", "foo"), ("Content-Length", "1")], + ), + Response(status_code=404, headers=[(b"transfer-encoding", b"chunked")]), + Response(status_code=200, headers=[(b"transfer-encoding", b"chunked")]), + ), + ( + Request( + method="GET", + target="/", + headers=[("Host", "foo"), ("Content-Length", "1"), ("Upgrade", "a, b")], + ), + Response(status_code=200, headers=[(b"transfer-encoding", b"chunked")]), + InformationalResponse(status_code=101, headers=[("Upgrade", "a")]), + ), + ( + Request( + method="CONNECT", + target="example.com:443", + headers=[("Host", "foo"), ("Content-Length", "1"), ("Upgrade", "a, b")], + ), + Response(status_code=404, headers=[(b"transfer-encoding", b"chunked")]), + # Accept CONNECT, not upgrade + Response(status_code=200, headers=[(b"transfer-encoding", b"chunked")]), + ), + ( + Request( + method="CONNECT", + target="example.com:443", + headers=[("Host", "foo"), ("Content-Length", "1"), ("Upgrade", "a, b")], + ), + Response(status_code=404, headers=[(b"transfer-encoding", b"chunked")]), + # Accept Upgrade, not CONNECT + InformationalResponse(status_code=101, headers=[("Upgrade", "b")]), + ), + ]: + + def setup() -> ConnectionPair: + p = ConnectionPair() + p.send(CLIENT, req) + # No switch-related state change stuff yet; the client has to + # finish the request before that kicks in + for conn in p.conns: + assert conn.states[CLIENT] is SEND_BODY + p.send(CLIENT, [Data(data=b"1"), EndOfMessage()]) + for conn in p.conns: + assert conn.states[CLIENT] is MIGHT_SWITCH_PROTOCOL + assert p.conn[SERVER].next_event() is PAUSED + return p + + # Test deny case + p = setup() + p.send(SERVER, deny) + for conn in p.conns: + assert conn.states == {CLIENT: DONE, SERVER: SEND_BODY} + p.send(SERVER, EndOfMessage()) + # Check that re-use is still allowed after a denial + for conn in p.conns: + conn.start_next_cycle() + + # Test accept case + p = setup() + p.send(SERVER, accept) + for conn in p.conns: + assert conn.states == {CLIENT: SWITCHED_PROTOCOL, SERVER: SWITCHED_PROTOCOL} + conn.receive_data(b"123") + assert conn.next_event() is PAUSED + conn.receive_data(b"456") + assert conn.next_event() is PAUSED + assert conn.trailing_data == (b"123456", False) + + # Pausing in might-switch, then recovery + # (weird artificial case where the trailing data actually is valid + # HTTP for some reason, because this makes it easier to test the state + # logic) + p = setup() + sc = p.conn[SERVER] + sc.receive_data(b"GET / HTTP/1.0\r\n\r\n") + assert sc.next_event() is PAUSED + assert sc.trailing_data == (b"GET / HTTP/1.0\r\n\r\n", False) + sc.send(deny) + assert sc.next_event() is PAUSED + sc.send(EndOfMessage()) + sc.start_next_cycle() + assert get_all_events(sc) == [ + Request(method="GET", target="/", headers=[], http_version="1.0"), # type: ignore[arg-type] + EndOfMessage(), + ] + + # When we're DONE, have no trailing data, and the connection gets + # closed, we report ConnectionClosed(). When we're in might-switch or + # switched, we don't. + p = setup() + sc = p.conn[SERVER] + sc.receive_data(b"") + assert sc.next_event() is PAUSED + assert sc.trailing_data == (b"", True) + p.send(SERVER, accept) + assert sc.next_event() is PAUSED + + p = setup() + sc = p.conn[SERVER] + sc.receive_data(b"") + assert sc.next_event() is PAUSED + sc.send(deny) + assert sc.next_event() == ConnectionClosed() + + # You can't send after switching protocols, or while waiting for a + # protocol switch + p = setup() + with pytest.raises(LocalProtocolError): + p.conn[CLIENT].send( + Request(method="GET", target="/", headers=[("Host", "a")]) + ) + p = setup() + p.send(SERVER, accept) + with pytest.raises(LocalProtocolError): + p.conn[SERVER].send(Data(data=b"123")) + + +def test_close_simple() -> None: + # Just immediately closing a new connection without anything having + # happened yet. + for (who_shot_first, who_shot_second) in [(CLIENT, SERVER), (SERVER, CLIENT)]: + + def setup() -> ConnectionPair: + p = ConnectionPair() + p.send(who_shot_first, ConnectionClosed()) + for conn in p.conns: + assert conn.states == { + who_shot_first: CLOSED, + who_shot_second: MUST_CLOSE, + } + return p + + # You can keep putting b"" into a closed connection, and you keep + # getting ConnectionClosed() out: + p = setup() + assert p.conn[who_shot_second].next_event() == ConnectionClosed() + assert p.conn[who_shot_second].next_event() == ConnectionClosed() + p.conn[who_shot_second].receive_data(b"") + assert p.conn[who_shot_second].next_event() == ConnectionClosed() + # Second party can close... + p = setup() + p.send(who_shot_second, ConnectionClosed()) + for conn in p.conns: + assert conn.our_state is CLOSED + assert conn.their_state is CLOSED + # But trying to receive new data on a closed connection is a + # RuntimeError (not ProtocolError, because the problem here isn't + # violation of HTTP, it's violation of physics) + p = setup() + with pytest.raises(RuntimeError): + p.conn[who_shot_second].receive_data(b"123") + # And receiving new data on a MUST_CLOSE connection is a ProtocolError + p = setup() + p.conn[who_shot_first].receive_data(b"GET") + with pytest.raises(RemoteProtocolError): + p.conn[who_shot_first].next_event() + + +def test_close_different_states() -> None: + req = [ + Request(method="GET", target="/foo", headers=[("Host", "a")]), + EndOfMessage(), + ] + resp = [ + Response(status_code=200, headers=[(b"transfer-encoding", b"chunked")]), + EndOfMessage(), + ] + + # Client before request + p = ConnectionPair() + p.send(CLIENT, ConnectionClosed()) + for conn in p.conns: + assert conn.states == {CLIENT: CLOSED, SERVER: MUST_CLOSE} + + # Client after request + p = ConnectionPair() + p.send(CLIENT, req) + p.send(CLIENT, ConnectionClosed()) + for conn in p.conns: + assert conn.states == {CLIENT: CLOSED, SERVER: SEND_RESPONSE} + + # Server after request -> not allowed + p = ConnectionPair() + p.send(CLIENT, req) + with pytest.raises(LocalProtocolError): + p.conn[SERVER].send(ConnectionClosed()) + p.conn[CLIENT].receive_data(b"") + with pytest.raises(RemoteProtocolError): + p.conn[CLIENT].next_event() + + # Server after response + p = ConnectionPair() + p.send(CLIENT, req) + p.send(SERVER, resp) + p.send(SERVER, ConnectionClosed()) + for conn in p.conns: + assert conn.states == {CLIENT: MUST_CLOSE, SERVER: CLOSED} + + # Both after closing (ConnectionClosed() is idempotent) + p = ConnectionPair() + p.send(CLIENT, req) + p.send(SERVER, resp) + p.send(CLIENT, ConnectionClosed()) + p.send(SERVER, ConnectionClosed()) + p.send(CLIENT, ConnectionClosed()) + p.send(SERVER, ConnectionClosed()) + + # In the middle of sending -> not allowed + p = ConnectionPair() + p.send( + CLIENT, + Request( + method="GET", target="/", headers=[("Host", "a"), ("Content-Length", "10")] + ), + ) + with pytest.raises(LocalProtocolError): + p.conn[CLIENT].send(ConnectionClosed()) + p.conn[SERVER].receive_data(b"") + with pytest.raises(RemoteProtocolError): + p.conn[SERVER].next_event() + + +# Receive several requests and then client shuts down their side of the +# connection; we can respond to each +def test_pipelined_close() -> None: + c = Connection(SERVER) + # 2 requests then a close + c.receive_data( + b"GET /1 HTTP/1.1\r\nHost: a.com\r\nContent-Length: 5\r\n\r\n" + b"12345" + b"GET /2 HTTP/1.1\r\nHost: a.com\r\nContent-Length: 5\r\n\r\n" + b"67890" + ) + c.receive_data(b"") + assert get_all_events(c) == [ + Request( + method="GET", + target="/1", + headers=[("host", "a.com"), ("content-length", "5")], + ), + Data(data=b"12345"), + EndOfMessage(), + ] + assert c.states[CLIENT] is DONE + c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type] + c.send(EndOfMessage()) + assert c.states[SERVER] is DONE + c.start_next_cycle() + assert get_all_events(c) == [ + Request( + method="GET", + target="/2", + headers=[("host", "a.com"), ("content-length", "5")], + ), + Data(data=b"67890"), + EndOfMessage(), + ConnectionClosed(), + ] + assert c.states == {CLIENT: CLOSED, SERVER: SEND_RESPONSE} + c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type] + c.send(EndOfMessage()) + assert c.states == {CLIENT: CLOSED, SERVER: MUST_CLOSE} + c.send(ConnectionClosed()) + assert c.states == {CLIENT: CLOSED, SERVER: CLOSED} + + +def test_sendfile() -> None: + class SendfilePlaceholder: + def __len__(self) -> int: + return 10 + + placeholder = SendfilePlaceholder() + + def setup( + header: Tuple[str, str], http_version: str + ) -> Tuple[Connection, Optional[List[bytes]]]: + c = Connection(SERVER) + receive_and_get( + c, "GET / HTTP/{}\r\nHost: a\r\n\r\n".format(http_version).encode("ascii") + ) + headers = [] + if header: + headers.append(header) + c.send(Response(status_code=200, headers=headers)) + return c, c.send_with_data_passthrough(Data(data=placeholder)) # type: ignore + + c, data = setup(("Content-Length", "10"), "1.1") + assert data == [placeholder] # type: ignore + # Raises an error if the connection object doesn't think we've sent + # exactly 10 bytes + c.send(EndOfMessage()) + + _, data = setup(("Transfer-Encoding", "chunked"), "1.1") + assert placeholder in data # type: ignore + data[data.index(placeholder)] = b"x" * 10 # type: ignore + assert b"".join(data) == b"a\r\nxxxxxxxxxx\r\n" # type: ignore + + c, data = setup(None, "1.0") # type: ignore + assert data == [placeholder] # type: ignore + assert c.our_state is SEND_BODY + + +def test_errors() -> None: + # After a receive error, you can't receive + for role in [CLIENT, SERVER]: + c = Connection(our_role=role) + c.receive_data(b"gibberish\r\n\r\n") + with pytest.raises(RemoteProtocolError): + c.next_event() + # Now any attempt to receive continues to raise + assert c.their_state is ERROR + assert c.our_state is not ERROR + print(c._cstate.states) + with pytest.raises(RemoteProtocolError): + c.next_event() + # But we can still yell at the client for sending us gibberish + if role is SERVER: + assert ( + c.send(Response(status_code=400, headers=[])) # type: ignore[arg-type] + == b"HTTP/1.1 400 \r\nConnection: close\r\n\r\n" + ) + + # After an error sending, you can no longer send + # (This is especially important for things like content-length errors, + # where there's complex internal state being modified) + def conn(role: Type[Sentinel]) -> Connection: + c = Connection(our_role=role) + if role is SERVER: + # Put it into the state where it *could* send a response... + receive_and_get(c, b"GET / HTTP/1.0\r\n\r\n") + assert c.our_state is SEND_RESPONSE + return c + + for role in [CLIENT, SERVER]: + if role is CLIENT: + # This HTTP/1.0 request won't be detected as bad until after we go + # through the state machine and hit the writing code + good = Request(method="GET", target="/", headers=[("Host", "example.com")]) + bad = Request( + method="GET", + target="/", + headers=[("Host", "example.com")], + http_version="1.0", + ) + elif role is SERVER: + good = Response(status_code=200, headers=[]) # type: ignore[arg-type,assignment] + bad = Response(status_code=200, headers=[], http_version="1.0") # type: ignore[arg-type,assignment] + # Make sure 'good' actually is good + c = conn(role) + c.send(good) + assert c.our_state is not ERROR + # Do that again, but this time sending 'bad' first + c = conn(role) + with pytest.raises(LocalProtocolError): + c.send(bad) + assert c.our_state is ERROR + assert c.their_state is not ERROR + # Now 'good' is not so good + with pytest.raises(LocalProtocolError): + c.send(good) + + # And check send_failed() too + c = conn(role) + c.send_failed() + assert c.our_state is ERROR + assert c.their_state is not ERROR + # This is idempotent + c.send_failed() + assert c.our_state is ERROR + assert c.their_state is not ERROR + + +def test_idle_receive_nothing() -> None: + # At one point this incorrectly raised an error + for role in [CLIENT, SERVER]: + c = Connection(role) + assert c.next_event() is NEED_DATA + + +def test_connection_drop() -> None: + c = Connection(SERVER) + c.receive_data(b"GET /") + assert c.next_event() is NEED_DATA + c.receive_data(b"") + with pytest.raises(RemoteProtocolError): + c.next_event() + + +def test_408_request_timeout() -> None: + # Should be able to send this spontaneously as a server without seeing + # anything from client + p = ConnectionPair() + p.send(SERVER, Response(status_code=408, headers=[(b"connection", b"close")])) + + +# This used to raise IndexError +def test_empty_request() -> None: + c = Connection(SERVER) + c.receive_data(b"\r\n") + with pytest.raises(RemoteProtocolError): + c.next_event() + + +# This used to raise IndexError +def test_empty_response() -> None: + c = Connection(CLIENT) + c.send(Request(method="GET", target="/", headers=[("Host", "a")])) + c.receive_data(b"\r\n") + with pytest.raises(RemoteProtocolError): + c.next_event() + + +@pytest.mark.parametrize( + "data", + [ + b"\x00", + b"\x20", + b"\x16\x03\x01\x00\xa5", # Typical start of a TLS Client Hello + ], +) +def test_early_detection_of_invalid_request(data: bytes) -> None: + c = Connection(SERVER) + # Early detection should occur before even receiving a `\r\n` + c.receive_data(data) + with pytest.raises(RemoteProtocolError): + c.next_event() + + +@pytest.mark.parametrize( + "data", + [ + b"\x00", + b"\x20", + b"\x16\x03\x03\x00\x31", # Typical start of a TLS Server Hello + ], +) +def test_early_detection_of_invalid_response(data: bytes) -> None: + c = Connection(CLIENT) + # Early detection should occur before even receiving a `\r\n` + c.receive_data(data) + with pytest.raises(RemoteProtocolError): + c.next_event() + + +# This used to give different headers for HEAD and GET. +# The correct way to handle HEAD is to put whatever headers we *would* have +# put if it were a GET -- even though we know that for HEAD, those headers +# will be ignored. +def test_HEAD_framing_headers() -> None: + def setup(method: bytes, http_version: bytes) -> Connection: + c = Connection(SERVER) + c.receive_data( + method + b" / HTTP/" + http_version + b"\r\n" + b"Host: example.com\r\n\r\n" + ) + assert type(c.next_event()) is Request + assert type(c.next_event()) is EndOfMessage + return c + + for method in [b"GET", b"HEAD"]: + # No Content-Length, HTTP/1.1 peer, should use chunked + c = setup(method, b"1.1") + assert ( + c.send(Response(status_code=200, headers=[])) == b"HTTP/1.1 200 \r\n" # type: ignore[arg-type] + b"Transfer-Encoding: chunked\r\n\r\n" + ) + + # No Content-Length, HTTP/1.0 peer, frame with connection: close + c = setup(method, b"1.0") + assert ( + c.send(Response(status_code=200, headers=[])) == b"HTTP/1.1 200 \r\n" # type: ignore[arg-type] + b"Connection: close\r\n\r\n" + ) + + # Content-Length + Transfer-Encoding, TE wins + c = setup(method, b"1.1") + assert ( + c.send( + Response( + status_code=200, + headers=[ + ("Content-Length", "100"), + ("Transfer-Encoding", "chunked"), + ], + ) + ) + == b"HTTP/1.1 200 \r\n" + b"Transfer-Encoding: chunked\r\n\r\n" + ) + + +def test_special_exceptions_for_lost_connection_in_message_body() -> None: + c = Connection(SERVER) + c.receive_data( + b"POST / HTTP/1.1\r\n" b"Host: example.com\r\n" b"Content-Length: 100\r\n\r\n" + ) + assert type(c.next_event()) is Request + assert c.next_event() is NEED_DATA + c.receive_data(b"12345") + assert c.next_event() == Data(data=b"12345") + c.receive_data(b"") + with pytest.raises(RemoteProtocolError) as excinfo: + c.next_event() + assert "received 5 bytes" in str(excinfo.value) + assert "expected 100" in str(excinfo.value) + + c = Connection(SERVER) + c.receive_data( + b"POST / HTTP/1.1\r\n" + b"Host: example.com\r\n" + b"Transfer-Encoding: chunked\r\n\r\n" + ) + assert type(c.next_event()) is Request + assert c.next_event() is NEED_DATA + c.receive_data(b"8\r\n012345") + assert c.next_event().data == b"012345" # type: ignore + c.receive_data(b"") + with pytest.raises(RemoteProtocolError) as excinfo: + c.next_event() + assert "incomplete chunked read" in str(excinfo.value) diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/test_events.py b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_events.py new file mode 100644 index 00000000..bc6c3137 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_events.py @@ -0,0 +1,150 @@ +from http import HTTPStatus + +import pytest + +from .. import _events +from .._events import ( + ConnectionClosed, + Data, + EndOfMessage, + Event, + InformationalResponse, + Request, + Response, +) +from .._util import LocalProtocolError + + +def test_events() -> None: + with pytest.raises(LocalProtocolError): + # Missing Host: + req = Request( + method="GET", target="/", headers=[("a", "b")], http_version="1.1" + ) + # But this is okay (HTTP/1.0) + req = Request(method="GET", target="/", headers=[("a", "b")], http_version="1.0") + # fields are normalized + assert req.method == b"GET" + assert req.target == b"/" + assert req.headers == [(b"a", b"b")] + assert req.http_version == b"1.0" + + # This is also okay -- has a Host (with weird capitalization, which is ok) + req = Request( + method="GET", + target="/", + headers=[("a", "b"), ("hOSt", "example.com")], + http_version="1.1", + ) + # we normalize header capitalization + assert req.headers == [(b"a", b"b"), (b"host", b"example.com")] + + # Multiple host is bad too + with pytest.raises(LocalProtocolError): + req = Request( + method="GET", + target="/", + headers=[("Host", "a"), ("Host", "a")], + http_version="1.1", + ) + # Even for HTTP/1.0 + with pytest.raises(LocalProtocolError): + req = Request( + method="GET", + target="/", + headers=[("Host", "a"), ("Host", "a")], + http_version="1.0", + ) + + # Header values are validated + for bad_char in "\x00\r\n\f\v": + with pytest.raises(LocalProtocolError): + req = Request( + method="GET", + target="/", + headers=[("Host", "a"), ("Foo", "asd" + bad_char)], + http_version="1.0", + ) + + # But for compatibility we allow non-whitespace control characters, even + # though they're forbidden by the spec. + Request( + method="GET", + target="/", + headers=[("Host", "a"), ("Foo", "asd\x01\x02\x7f")], + http_version="1.0", + ) + + # Request target is validated + for bad_byte in b"\x00\x20\x7f\xee": + target = bytearray(b"/") + target.append(bad_byte) + with pytest.raises(LocalProtocolError): + Request( + method="GET", target=target, headers=[("Host", "a")], http_version="1.1" + ) + + # Request method is validated + with pytest.raises(LocalProtocolError): + Request( + method="GET / HTTP/1.1", + target=target, + headers=[("Host", "a")], + http_version="1.1", + ) + + ir = InformationalResponse(status_code=100, headers=[("Host", "a")]) + assert ir.status_code == 100 + assert ir.headers == [(b"host", b"a")] + assert ir.http_version == b"1.1" + + with pytest.raises(LocalProtocolError): + InformationalResponse(status_code=200, headers=[("Host", "a")]) + + resp = Response(status_code=204, headers=[], http_version="1.0") # type: ignore[arg-type] + assert resp.status_code == 204 + assert resp.headers == [] + assert resp.http_version == b"1.0" + + with pytest.raises(LocalProtocolError): + resp = Response(status_code=100, headers=[], http_version="1.0") # type: ignore[arg-type] + + with pytest.raises(LocalProtocolError): + Response(status_code="100", headers=[], http_version="1.0") # type: ignore[arg-type] + + with pytest.raises(LocalProtocolError): + InformationalResponse(status_code=b"100", headers=[], http_version="1.0") # type: ignore[arg-type] + + d = Data(data=b"asdf") + assert d.data == b"asdf" + + eom = EndOfMessage() + assert eom.headers == [] + + cc = ConnectionClosed() + assert repr(cc) == "ConnectionClosed()" + + +def test_intenum_status_code() -> None: + # https://github.com/python-hyper/h11/issues/72 + + r = Response(status_code=HTTPStatus.OK, headers=[], http_version="1.0") # type: ignore[arg-type] + assert r.status_code == HTTPStatus.OK + assert type(r.status_code) is not type(HTTPStatus.OK) + assert type(r.status_code) is int + + +def test_header_casing() -> None: + r = Request( + method="GET", + target="/", + headers=[("Host", "example.org"), ("Connection", "keep-alive")], + http_version="1.1", + ) + assert len(r.headers) == 2 + assert r.headers[0] == (b"host", b"example.org") + assert r.headers == [(b"host", b"example.org"), (b"connection", b"keep-alive")] + assert r.headers.raw_items() == [ + (b"Host", b"example.org"), + (b"Connection", b"keep-alive"), + ] diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/test_headers.py b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_headers.py new file mode 100644 index 00000000..ba53d088 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_headers.py @@ -0,0 +1,157 @@ +import pytest + +from .._events import Request +from .._headers import ( + get_comma_header, + has_expect_100_continue, + Headers, + normalize_and_validate, + set_comma_header, +) +from .._util import LocalProtocolError + + +def test_normalize_and_validate() -> None: + assert normalize_and_validate([("foo", "bar")]) == [(b"foo", b"bar")] + assert normalize_and_validate([(b"foo", b"bar")]) == [(b"foo", b"bar")] + + # no leading/trailing whitespace in names + with pytest.raises(LocalProtocolError): + normalize_and_validate([(b"foo ", "bar")]) + with pytest.raises(LocalProtocolError): + normalize_and_validate([(b" foo", "bar")]) + + # no weird characters in names + with pytest.raises(LocalProtocolError) as excinfo: + normalize_and_validate([(b"foo bar", b"baz")]) + assert "foo bar" in str(excinfo.value) + with pytest.raises(LocalProtocolError): + normalize_and_validate([(b"foo\x00bar", b"baz")]) + # Not even 8-bit characters: + with pytest.raises(LocalProtocolError): + normalize_and_validate([(b"foo\xffbar", b"baz")]) + # And not even the control characters we allow in values: + with pytest.raises(LocalProtocolError): + normalize_and_validate([(b"foo\x01bar", b"baz")]) + + # no return or NUL characters in values + with pytest.raises(LocalProtocolError) as excinfo: + normalize_and_validate([("foo", "bar\rbaz")]) + assert "bar\\rbaz" in str(excinfo.value) + with pytest.raises(LocalProtocolError): + normalize_and_validate([("foo", "bar\nbaz")]) + with pytest.raises(LocalProtocolError): + normalize_and_validate([("foo", "bar\x00baz")]) + # no leading/trailing whitespace + with pytest.raises(LocalProtocolError): + normalize_and_validate([("foo", "barbaz ")]) + with pytest.raises(LocalProtocolError): + normalize_and_validate([("foo", " barbaz")]) + with pytest.raises(LocalProtocolError): + normalize_and_validate([("foo", "barbaz\t")]) + with pytest.raises(LocalProtocolError): + normalize_and_validate([("foo", "\tbarbaz")]) + + # content-length + assert normalize_and_validate([("Content-Length", "1")]) == [ + (b"content-length", b"1") + ] + with pytest.raises(LocalProtocolError): + normalize_and_validate([("Content-Length", "asdf")]) + with pytest.raises(LocalProtocolError): + normalize_and_validate([("Content-Length", "1x")]) + with pytest.raises(LocalProtocolError): + normalize_and_validate([("Content-Length", "1"), ("Content-Length", "2")]) + assert normalize_and_validate( + [("Content-Length", "0"), ("Content-Length", "0")] + ) == [(b"content-length", b"0")] + assert normalize_and_validate([("Content-Length", "0 , 0")]) == [ + (b"content-length", b"0") + ] + with pytest.raises(LocalProtocolError): + normalize_and_validate( + [("Content-Length", "1"), ("Content-Length", "1"), ("Content-Length", "2")] + ) + with pytest.raises(LocalProtocolError): + normalize_and_validate([("Content-Length", "1 , 1,2")]) + + # transfer-encoding + assert normalize_and_validate([("Transfer-Encoding", "chunked")]) == [ + (b"transfer-encoding", b"chunked") + ] + assert normalize_and_validate([("Transfer-Encoding", "cHuNkEd")]) == [ + (b"transfer-encoding", b"chunked") + ] + with pytest.raises(LocalProtocolError) as excinfo: + normalize_and_validate([("Transfer-Encoding", "gzip")]) + assert excinfo.value.error_status_hint == 501 # Not Implemented + with pytest.raises(LocalProtocolError) as excinfo: + normalize_and_validate( + [("Transfer-Encoding", "chunked"), ("Transfer-Encoding", "gzip")] + ) + assert excinfo.value.error_status_hint == 501 # Not Implemented + + +def test_get_set_comma_header() -> None: + headers = normalize_and_validate( + [ + ("Connection", "close"), + ("whatever", "something"), + ("connectiON", "fOo,, , BAR"), + ] + ) + + assert get_comma_header(headers, b"connection") == [b"close", b"foo", b"bar"] + + headers = set_comma_header(headers, b"newthing", ["a", "b"]) # type: ignore + + with pytest.raises(LocalProtocolError): + set_comma_header(headers, b"newthing", [" a", "b"]) # type: ignore + + assert headers == [ + (b"connection", b"close"), + (b"whatever", b"something"), + (b"connection", b"fOo,, , BAR"), + (b"newthing", b"a"), + (b"newthing", b"b"), + ] + + headers = set_comma_header(headers, b"whatever", ["different thing"]) # type: ignore + + assert headers == [ + (b"connection", b"close"), + (b"connection", b"fOo,, , BAR"), + (b"newthing", b"a"), + (b"newthing", b"b"), + (b"whatever", b"different thing"), + ] + + +def test_has_100_continue() -> None: + assert has_expect_100_continue( + Request( + method="GET", + target="/", + headers=[("Host", "example.com"), ("Expect", "100-continue")], + ) + ) + assert not has_expect_100_continue( + Request(method="GET", target="/", headers=[("Host", "example.com")]) + ) + # Case insensitive + assert has_expect_100_continue( + Request( + method="GET", + target="/", + headers=[("Host", "example.com"), ("Expect", "100-Continue")], + ) + ) + # Doesn't work in HTTP/1.0 + assert not has_expect_100_continue( + Request( + method="GET", + target="/", + headers=[("Host", "example.com"), ("Expect", "100-continue")], + http_version="1.0", + ) + ) diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/test_helpers.py b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_helpers.py new file mode 100644 index 00000000..c329c767 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_helpers.py @@ -0,0 +1,32 @@ +from .._events import ( + ConnectionClosed, + Data, + EndOfMessage, + Event, + InformationalResponse, + Request, + Response, +) +from .helpers import normalize_data_events + + +def test_normalize_data_events() -> None: + assert normalize_data_events( + [ + Data(data=bytearray(b"1")), + Data(data=b"2"), + Response(status_code=200, headers=[]), # type: ignore[arg-type] + Data(data=b"3"), + Data(data=b"4"), + EndOfMessage(), + Data(data=b"5"), + Data(data=b"6"), + Data(data=b"7"), + ] + ) == [ + Data(data=b"12"), + Response(status_code=200, headers=[]), # type: ignore[arg-type] + Data(data=b"34"), + EndOfMessage(), + Data(data=b"567"), + ] diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/test_io.py b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_io.py new file mode 100644 index 00000000..2b47c0ea --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_io.py @@ -0,0 +1,572 @@ +from typing import Any, Callable, Generator, List + +import pytest + +from .._events import ( + ConnectionClosed, + Data, + EndOfMessage, + Event, + InformationalResponse, + Request, + Response, +) +from .._headers import Headers, normalize_and_validate +from .._readers import ( + _obsolete_line_fold, + ChunkedReader, + ContentLengthReader, + Http10Reader, + READERS, +) +from .._receivebuffer import ReceiveBuffer +from .._state import ( + CLIENT, + CLOSED, + DONE, + IDLE, + MIGHT_SWITCH_PROTOCOL, + MUST_CLOSE, + SEND_BODY, + SEND_RESPONSE, + SERVER, + SWITCHED_PROTOCOL, +) +from .._util import LocalProtocolError +from .._writers import ( + ChunkedWriter, + ContentLengthWriter, + Http10Writer, + write_any_response, + write_headers, + write_request, + WRITERS, +) +from .helpers import normalize_data_events + +SIMPLE_CASES = [ + ( + (CLIENT, IDLE), + Request( + method="GET", + target="/a", + headers=[("Host", "foo"), ("Connection", "close")], + ), + b"GET /a HTTP/1.1\r\nHost: foo\r\nConnection: close\r\n\r\n", + ), + ( + (SERVER, SEND_RESPONSE), + Response(status_code=200, headers=[("Connection", "close")], reason=b"OK"), + b"HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + ), + ( + (SERVER, SEND_RESPONSE), + Response(status_code=200, headers=[], reason=b"OK"), # type: ignore[arg-type] + b"HTTP/1.1 200 OK\r\n\r\n", + ), + ( + (SERVER, SEND_RESPONSE), + InformationalResponse( + status_code=101, headers=[("Upgrade", "websocket")], reason=b"Upgrade" + ), + b"HTTP/1.1 101 Upgrade\r\nUpgrade: websocket\r\n\r\n", + ), + ( + (SERVER, SEND_RESPONSE), + InformationalResponse(status_code=101, headers=[], reason=b"Upgrade"), # type: ignore[arg-type] + b"HTTP/1.1 101 Upgrade\r\n\r\n", + ), +] + + +def dowrite(writer: Callable[..., None], obj: Any) -> bytes: + got_list: List[bytes] = [] + writer(obj, got_list.append) + return b"".join(got_list) + + +def tw(writer: Any, obj: Any, expected: Any) -> None: + got = dowrite(writer, obj) + assert got == expected + + +def makebuf(data: bytes) -> ReceiveBuffer: + buf = ReceiveBuffer() + buf += data + return buf + + +def tr(reader: Any, data: bytes, expected: Any) -> None: + def check(got: Any) -> None: + assert got == expected + # Headers should always be returned as bytes, not e.g. bytearray + # https://github.com/python-hyper/wsproto/pull/54#issuecomment-377709478 + for name, value in getattr(got, "headers", []): + assert type(name) is bytes + assert type(value) is bytes + + # Simple: consume whole thing + buf = makebuf(data) + check(reader(buf)) + assert not buf + + # Incrementally growing buffer + buf = ReceiveBuffer() + for i in range(len(data)): + assert reader(buf) is None + buf += data[i : i + 1] + check(reader(buf)) + + # Trailing data + buf = makebuf(data) + buf += b"trailing" + check(reader(buf)) + assert bytes(buf) == b"trailing" + + +def test_writers_simple() -> None: + for ((role, state), event, binary) in SIMPLE_CASES: + tw(WRITERS[role, state], event, binary) + + +def test_readers_simple() -> None: + for ((role, state), event, binary) in SIMPLE_CASES: + tr(READERS[role, state], binary, event) + + +def test_writers_unusual() -> None: + # Simple test of the write_headers utility routine + tw( + write_headers, + normalize_and_validate([("foo", "bar"), ("baz", "quux")]), + b"foo: bar\r\nbaz: quux\r\n\r\n", + ) + tw(write_headers, Headers([]), b"\r\n") + + # We understand HTTP/1.0, but we don't speak it + with pytest.raises(LocalProtocolError): + tw( + write_request, + Request( + method="GET", + target="/", + headers=[("Host", "foo"), ("Connection", "close")], + http_version="1.0", + ), + None, + ) + with pytest.raises(LocalProtocolError): + tw( + write_any_response, + Response( + status_code=200, headers=[("Connection", "close")], http_version="1.0" + ), + None, + ) + + +def test_readers_unusual() -> None: + # Reading HTTP/1.0 + tr( + READERS[CLIENT, IDLE], + b"HEAD /foo HTTP/1.0\r\nSome: header\r\n\r\n", + Request( + method="HEAD", + target="/foo", + headers=[("Some", "header")], + http_version="1.0", + ), + ) + + # check no-headers, since it's only legal with HTTP/1.0 + tr( + READERS[CLIENT, IDLE], + b"HEAD /foo HTTP/1.0\r\n\r\n", + Request(method="HEAD", target="/foo", headers=[], http_version="1.0"), # type: ignore[arg-type] + ) + + tr( + READERS[SERVER, SEND_RESPONSE], + b"HTTP/1.0 200 OK\r\nSome: header\r\n\r\n", + Response( + status_code=200, + headers=[("Some", "header")], + http_version="1.0", + reason=b"OK", + ), + ) + + # single-character header values (actually disallowed by the ABNF in RFC + # 7230 -- this is a bug in the standard that we originally copied...) + tr( + READERS[SERVER, SEND_RESPONSE], + b"HTTP/1.0 200 OK\r\n" b"Foo: a a a a a \r\n\r\n", + Response( + status_code=200, + headers=[("Foo", "a a a a a")], + http_version="1.0", + reason=b"OK", + ), + ) + + # Empty headers -- also legal + tr( + READERS[SERVER, SEND_RESPONSE], + b"HTTP/1.0 200 OK\r\n" b"Foo:\r\n\r\n", + Response( + status_code=200, headers=[("Foo", "")], http_version="1.0", reason=b"OK" + ), + ) + + tr( + READERS[SERVER, SEND_RESPONSE], + b"HTTP/1.0 200 OK\r\n" b"Foo: \t \t \r\n\r\n", + Response( + status_code=200, headers=[("Foo", "")], http_version="1.0", reason=b"OK" + ), + ) + + # Tolerate broken servers that leave off the response code + tr( + READERS[SERVER, SEND_RESPONSE], + b"HTTP/1.0 200\r\n" b"Foo: bar\r\n\r\n", + Response( + status_code=200, headers=[("Foo", "bar")], http_version="1.0", reason=b"" + ), + ) + + # Tolerate headers line endings (\r\n and \n) + # \n\r\b between headers and body + tr( + READERS[SERVER, SEND_RESPONSE], + b"HTTP/1.1 200 OK\r\nSomeHeader: val\n\r\n", + Response( + status_code=200, + headers=[("SomeHeader", "val")], + http_version="1.1", + reason="OK", + ), + ) + + # delimited only with \n + tr( + READERS[SERVER, SEND_RESPONSE], + b"HTTP/1.1 200 OK\nSomeHeader1: val1\nSomeHeader2: val2\n\n", + Response( + status_code=200, + headers=[("SomeHeader1", "val1"), ("SomeHeader2", "val2")], + http_version="1.1", + reason="OK", + ), + ) + + # mixed \r\n and \n + tr( + READERS[SERVER, SEND_RESPONSE], + b"HTTP/1.1 200 OK\r\nSomeHeader1: val1\nSomeHeader2: val2\n\r\n", + Response( + status_code=200, + headers=[("SomeHeader1", "val1"), ("SomeHeader2", "val2")], + http_version="1.1", + reason="OK", + ), + ) + + # obsolete line folding + tr( + READERS[CLIENT, IDLE], + b"HEAD /foo HTTP/1.1\r\n" + b"Host: example.com\r\n" + b"Some: multi-line\r\n" + b" header\r\n" + b"\tnonsense\r\n" + b" \t \t\tI guess\r\n" + b"Connection: close\r\n" + b"More-nonsense: in the\r\n" + b" last header \r\n\r\n", + Request( + method="HEAD", + target="/foo", + headers=[ + ("Host", "example.com"), + ("Some", "multi-line header nonsense I guess"), + ("Connection", "close"), + ("More-nonsense", "in the last header"), + ], + ), + ) + + with pytest.raises(LocalProtocolError): + tr( + READERS[CLIENT, IDLE], + b"HEAD /foo HTTP/1.1\r\n" b" folded: line\r\n\r\n", + None, + ) + + with pytest.raises(LocalProtocolError): + tr( + READERS[CLIENT, IDLE], + b"HEAD /foo HTTP/1.1\r\n" b"foo : line\r\n\r\n", + None, + ) + with pytest.raises(LocalProtocolError): + tr( + READERS[CLIENT, IDLE], + b"HEAD /foo HTTP/1.1\r\n" b"foo\t: line\r\n\r\n", + None, + ) + with pytest.raises(LocalProtocolError): + tr( + READERS[CLIENT, IDLE], + b"HEAD /foo HTTP/1.1\r\n" b"foo\t: line\r\n\r\n", + None, + ) + with pytest.raises(LocalProtocolError): + tr(READERS[CLIENT, IDLE], b"HEAD /foo HTTP/1.1\r\n" b": line\r\n\r\n", None) + + +def test__obsolete_line_fold_bytes() -> None: + # _obsolete_line_fold has a defensive cast to bytearray, which is + # necessary to protect against O(n^2) behavior in case anyone ever passes + # in regular bytestrings... but right now we never pass in regular + # bytestrings. so this test just exists to get some coverage on that + # defensive cast. + assert list(_obsolete_line_fold([b"aaa", b"bbb", b" ccc", b"ddd"])) == [ + b"aaa", + bytearray(b"bbb ccc"), + b"ddd", + ] + + +def _run_reader_iter( + reader: Any, buf: bytes, do_eof: bool +) -> Generator[Any, None, None]: + while True: + event = reader(buf) + if event is None: + break + yield event + # body readers have undefined behavior after returning EndOfMessage, + # because this changes the state so they don't get called again + if type(event) is EndOfMessage: + break + if do_eof: + assert not buf + yield reader.read_eof() + + +def _run_reader(*args: Any) -> List[Event]: + events = list(_run_reader_iter(*args)) + return normalize_data_events(events) + + +def t_body_reader(thunk: Any, data: bytes, expected: Any, do_eof: bool = False) -> None: + # Simple: consume whole thing + print("Test 1") + buf = makebuf(data) + assert _run_reader(thunk(), buf, do_eof) == expected + + # Incrementally growing buffer + print("Test 2") + reader = thunk() + buf = ReceiveBuffer() + events = [] + for i in range(len(data)): + events += _run_reader(reader, buf, False) + buf += data[i : i + 1] + events += _run_reader(reader, buf, do_eof) + assert normalize_data_events(events) == expected + + is_complete = any(type(event) is EndOfMessage for event in expected) + if is_complete and not do_eof: + buf = makebuf(data + b"trailing") + assert _run_reader(thunk(), buf, False) == expected + + +def test_ContentLengthReader() -> None: + t_body_reader(lambda: ContentLengthReader(0), b"", [EndOfMessage()]) + + t_body_reader( + lambda: ContentLengthReader(10), + b"0123456789", + [Data(data=b"0123456789"), EndOfMessage()], + ) + + +def test_Http10Reader() -> None: + t_body_reader(Http10Reader, b"", [EndOfMessage()], do_eof=True) + t_body_reader(Http10Reader, b"asdf", [Data(data=b"asdf")], do_eof=False) + t_body_reader( + Http10Reader, b"asdf", [Data(data=b"asdf"), EndOfMessage()], do_eof=True + ) + + +def test_ChunkedReader() -> None: + t_body_reader(ChunkedReader, b"0\r\n\r\n", [EndOfMessage()]) + + t_body_reader( + ChunkedReader, + b"0\r\nSome: header\r\n\r\n", + [EndOfMessage(headers=[("Some", "header")])], + ) + + t_body_reader( + ChunkedReader, + b"5\r\n01234\r\n" + + b"10\r\n0123456789abcdef\r\n" + + b"0\r\n" + + b"Some: header\r\n\r\n", + [ + Data(data=b"012340123456789abcdef"), + EndOfMessage(headers=[("Some", "header")]), + ], + ) + + t_body_reader( + ChunkedReader, + b"5\r\n01234\r\n" + b"10\r\n0123456789abcdef\r\n" + b"0\r\n\r\n", + [Data(data=b"012340123456789abcdef"), EndOfMessage()], + ) + + # handles upper and lowercase hex + t_body_reader( + ChunkedReader, + b"aA\r\n" + b"x" * 0xAA + b"\r\n" + b"0\r\n\r\n", + [Data(data=b"x" * 0xAA), EndOfMessage()], + ) + + # refuses arbitrarily long chunk integers + with pytest.raises(LocalProtocolError): + # Technically this is legal HTTP/1.1, but we refuse to process chunk + # sizes that don't fit into 20 characters of hex + t_body_reader(ChunkedReader, b"9" * 100 + b"\r\nxxx", [Data(data=b"xxx")]) + + # refuses garbage in the chunk count + with pytest.raises(LocalProtocolError): + t_body_reader(ChunkedReader, b"10\x00\r\nxxx", None) + + # handles (and discards) "chunk extensions" omg wtf + t_body_reader( + ChunkedReader, + b"5; hello=there\r\n" + + b"xxxxx" + + b"\r\n" + + b'0; random="junk"; some=more; canbe=lonnnnngg\r\n\r\n', + [Data(data=b"xxxxx"), EndOfMessage()], + ) + + t_body_reader( + ChunkedReader, + b"5 \r\n01234\r\n" + b"0\r\n\r\n", + [Data(data=b"01234"), EndOfMessage()], + ) + + +def test_ContentLengthWriter() -> None: + w = ContentLengthWriter(5) + assert dowrite(w, Data(data=b"123")) == b"123" + assert dowrite(w, Data(data=b"45")) == b"45" + assert dowrite(w, EndOfMessage()) == b"" + + w = ContentLengthWriter(5) + with pytest.raises(LocalProtocolError): + dowrite(w, Data(data=b"123456")) + + w = ContentLengthWriter(5) + dowrite(w, Data(data=b"123")) + with pytest.raises(LocalProtocolError): + dowrite(w, Data(data=b"456")) + + w = ContentLengthWriter(5) + dowrite(w, Data(data=b"123")) + with pytest.raises(LocalProtocolError): + dowrite(w, EndOfMessage()) + + w = ContentLengthWriter(5) + dowrite(w, Data(data=b"123")) == b"123" + dowrite(w, Data(data=b"45")) == b"45" + with pytest.raises(LocalProtocolError): + dowrite(w, EndOfMessage(headers=[("Etag", "asdf")])) + + +def test_ChunkedWriter() -> None: + w = ChunkedWriter() + assert dowrite(w, Data(data=b"aaa")) == b"3\r\naaa\r\n" + assert dowrite(w, Data(data=b"a" * 20)) == b"14\r\n" + b"a" * 20 + b"\r\n" + + assert dowrite(w, Data(data=b"")) == b"" + + assert dowrite(w, EndOfMessage()) == b"0\r\n\r\n" + + assert ( + dowrite(w, EndOfMessage(headers=[("Etag", "asdf"), ("a", "b")])) + == b"0\r\nEtag: asdf\r\na: b\r\n\r\n" + ) + + +def test_Http10Writer() -> None: + w = Http10Writer() + assert dowrite(w, Data(data=b"1234")) == b"1234" + assert dowrite(w, EndOfMessage()) == b"" + + with pytest.raises(LocalProtocolError): + dowrite(w, EndOfMessage(headers=[("Etag", "asdf")])) + + +def test_reject_garbage_after_request_line() -> None: + with pytest.raises(LocalProtocolError): + tr(READERS[SERVER, SEND_RESPONSE], b"HTTP/1.0 200 OK\x00xxxx\r\n\r\n", None) + + +def test_reject_garbage_after_response_line() -> None: + with pytest.raises(LocalProtocolError): + tr( + READERS[CLIENT, IDLE], + b"HEAD /foo HTTP/1.1 xxxxxx\r\n" b"Host: a\r\n\r\n", + None, + ) + + +def test_reject_garbage_in_header_line() -> None: + with pytest.raises(LocalProtocolError): + tr( + READERS[CLIENT, IDLE], + b"HEAD /foo HTTP/1.1\r\n" b"Host: foo\x00bar\r\n\r\n", + None, + ) + + +def test_reject_non_vchar_in_path() -> None: + for bad_char in b"\x00\x20\x7f\xee": + message = bytearray(b"HEAD /") + message.append(bad_char) + message.extend(b" HTTP/1.1\r\nHost: foobar\r\n\r\n") + with pytest.raises(LocalProtocolError): + tr(READERS[CLIENT, IDLE], message, None) + + +# https://github.com/python-hyper/h11/issues/57 +def test_allow_some_garbage_in_cookies() -> None: + tr( + READERS[CLIENT, IDLE], + b"HEAD /foo HTTP/1.1\r\n" + b"Host: foo\r\n" + b"Set-Cookie: ___utmvafIumyLc=kUd\x01UpAt; path=/; Max-Age=900\r\n" + b"\r\n", + Request( + method="HEAD", + target="/foo", + headers=[ + ("Host", "foo"), + ("Set-Cookie", "___utmvafIumyLc=kUd\x01UpAt; path=/; Max-Age=900"), + ], + ), + ) + + +def test_host_comes_first() -> None: + tw( + write_headers, + normalize_and_validate([("foo", "bar"), ("Host", "example.com")]), + b"Host: example.com\r\nfoo: bar\r\n\r\n", + ) diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/test_receivebuffer.py b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_receivebuffer.py new file mode 100644 index 00000000..21a3870b --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_receivebuffer.py @@ -0,0 +1,135 @@ +import re +from typing import Tuple + +import pytest + +from .._receivebuffer import ReceiveBuffer + + +def test_receivebuffer() -> None: + b = ReceiveBuffer() + assert not b + assert len(b) == 0 + assert bytes(b) == b"" + + b += b"123" + assert b + assert len(b) == 3 + assert bytes(b) == b"123" + + assert bytes(b) == b"123" + + assert b.maybe_extract_at_most(2) == b"12" + assert b + assert len(b) == 1 + assert bytes(b) == b"3" + + assert bytes(b) == b"3" + + assert b.maybe_extract_at_most(10) == b"3" + assert bytes(b) == b"" + + assert b.maybe_extract_at_most(10) is None + assert not b + + ################################################################ + # maybe_extract_until_next + ################################################################ + + b += b"123\n456\r\n789\r\n" + + assert b.maybe_extract_next_line() == b"123\n456\r\n" + assert bytes(b) == b"789\r\n" + + assert b.maybe_extract_next_line() == b"789\r\n" + assert bytes(b) == b"" + + b += b"12\r" + assert b.maybe_extract_next_line() is None + assert bytes(b) == b"12\r" + + b += b"345\n\r" + assert b.maybe_extract_next_line() is None + assert bytes(b) == b"12\r345\n\r" + + # here we stopped at the middle of b"\r\n" delimiter + + b += b"\n6789aaa123\r\n" + assert b.maybe_extract_next_line() == b"12\r345\n\r\n" + assert b.maybe_extract_next_line() == b"6789aaa123\r\n" + assert b.maybe_extract_next_line() is None + assert bytes(b) == b"" + + ################################################################ + # maybe_extract_lines + ################################################################ + + b += b"123\r\na: b\r\nfoo:bar\r\n\r\ntrailing" + lines = b.maybe_extract_lines() + assert lines == [b"123", b"a: b", b"foo:bar"] + assert bytes(b) == b"trailing" + + assert b.maybe_extract_lines() is None + + b += b"\r\n\r" + assert b.maybe_extract_lines() is None + + assert b.maybe_extract_at_most(100) == b"trailing\r\n\r" + assert not b + + # Empty body case (as happens at the end of chunked encoding if there are + # no trailing headers, e.g.) + b += b"\r\ntrailing" + assert b.maybe_extract_lines() == [] + assert bytes(b) == b"trailing" + + +@pytest.mark.parametrize( + "data", + [ + pytest.param( + ( + b"HTTP/1.1 200 OK\r\n", + b"Content-type: text/plain\r\n", + b"Connection: close\r\n", + b"\r\n", + b"Some body", + ), + id="with_crlf_delimiter", + ), + pytest.param( + ( + b"HTTP/1.1 200 OK\n", + b"Content-type: text/plain\n", + b"Connection: close\n", + b"\n", + b"Some body", + ), + id="with_lf_only_delimiter", + ), + pytest.param( + ( + b"HTTP/1.1 200 OK\n", + b"Content-type: text/plain\r\n", + b"Connection: close\n", + b"\n", + b"Some body", + ), + id="with_mixed_crlf_and_lf", + ), + ], +) +def test_receivebuffer_for_invalid_delimiter(data: Tuple[bytes]) -> None: + b = ReceiveBuffer() + + for line in data: + b += line + + lines = b.maybe_extract_lines() + + assert lines == [ + b"HTTP/1.1 200 OK", + b"Content-type: text/plain", + b"Connection: close", + ] + assert bytes(b) == b"Some body" diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/test_state.py b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_state.py new file mode 100644 index 00000000..bc974e63 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_state.py @@ -0,0 +1,271 @@ +import pytest + +from .._events import ( + ConnectionClosed, + Data, + EndOfMessage, + Event, + InformationalResponse, + Request, + Response, +) +from .._state import ( + _SWITCH_CONNECT, + _SWITCH_UPGRADE, + CLIENT, + CLOSED, + ConnectionState, + DONE, + IDLE, + MIGHT_SWITCH_PROTOCOL, + MUST_CLOSE, + SEND_BODY, + SEND_RESPONSE, + SERVER, + SWITCHED_PROTOCOL, +) +from .._util import LocalProtocolError + + +def test_ConnectionState() -> None: + cs = ConnectionState() + + # Basic event-triggered transitions + + assert cs.states == {CLIENT: IDLE, SERVER: IDLE} + + cs.process_event(CLIENT, Request) + # The SERVER-Request special case: + assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE} + + # Illegal transitions raise an error and nothing happens + with pytest.raises(LocalProtocolError): + cs.process_event(CLIENT, Request) + assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE} + + cs.process_event(SERVER, InformationalResponse) + assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE} + + cs.process_event(SERVER, Response) + assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_BODY} + + cs.process_event(CLIENT, EndOfMessage) + cs.process_event(SERVER, EndOfMessage) + assert cs.states == {CLIENT: DONE, SERVER: DONE} + + # State-triggered transition + + cs.process_event(SERVER, ConnectionClosed) + assert cs.states == {CLIENT: MUST_CLOSE, SERVER: CLOSED} + + +def test_ConnectionState_keep_alive() -> None: + # keep_alive = False + cs = ConnectionState() + cs.process_event(CLIENT, Request) + cs.process_keep_alive_disabled() + cs.process_event(CLIENT, EndOfMessage) + assert cs.states == {CLIENT: MUST_CLOSE, SERVER: SEND_RESPONSE} + + cs.process_event(SERVER, Response) + cs.process_event(SERVER, EndOfMessage) + assert cs.states == {CLIENT: MUST_CLOSE, SERVER: MUST_CLOSE} + + +def test_ConnectionState_keep_alive_in_DONE() -> None: + # Check that if keep_alive is disabled when the CLIENT is already in DONE, + # then this is sufficient to immediately trigger the DONE -> MUST_CLOSE + # transition + cs = ConnectionState() + cs.process_event(CLIENT, Request) + cs.process_event(CLIENT, EndOfMessage) + assert cs.states[CLIENT] is DONE + cs.process_keep_alive_disabled() + assert cs.states[CLIENT] is MUST_CLOSE + + +def test_ConnectionState_switch_denied() -> None: + for switch_type in (_SWITCH_CONNECT, _SWITCH_UPGRADE): + for deny_early in (True, False): + cs = ConnectionState() + cs.process_client_switch_proposal(switch_type) + cs.process_event(CLIENT, Request) + cs.process_event(CLIENT, Data) + assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE} + + assert switch_type in cs.pending_switch_proposals + + if deny_early: + # before client reaches DONE + cs.process_event(SERVER, Response) + assert not cs.pending_switch_proposals + + cs.process_event(CLIENT, EndOfMessage) + + if deny_early: + assert cs.states == {CLIENT: DONE, SERVER: SEND_BODY} + else: + assert cs.states == { + CLIENT: MIGHT_SWITCH_PROTOCOL, + SERVER: SEND_RESPONSE, + } + + cs.process_event(SERVER, InformationalResponse) + assert cs.states == { + CLIENT: MIGHT_SWITCH_PROTOCOL, + SERVER: SEND_RESPONSE, + } + + cs.process_event(SERVER, Response) + assert cs.states == {CLIENT: DONE, SERVER: SEND_BODY} + assert not cs.pending_switch_proposals + + +_response_type_for_switch = { + _SWITCH_UPGRADE: InformationalResponse, + _SWITCH_CONNECT: Response, + None: Response, +} + + +def test_ConnectionState_protocol_switch_accepted() -> None: + for switch_event in [_SWITCH_UPGRADE, _SWITCH_CONNECT]: + cs = ConnectionState() + cs.process_client_switch_proposal(switch_event) + cs.process_event(CLIENT, Request) + cs.process_event(CLIENT, Data) + assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE} + + cs.process_event(CLIENT, EndOfMessage) + assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE} + + cs.process_event(SERVER, InformationalResponse) + assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE} + + cs.process_event(SERVER, _response_type_for_switch[switch_event], switch_event) + assert cs.states == {CLIENT: SWITCHED_PROTOCOL, SERVER: SWITCHED_PROTOCOL} + + +def test_ConnectionState_double_protocol_switch() -> None: + # CONNECT + Upgrade is legal! Very silly, but legal. So we support + # it. Because sometimes doing the silly thing is easier than not. + for server_switch in [None, _SWITCH_UPGRADE, _SWITCH_CONNECT]: + cs = ConnectionState() + cs.process_client_switch_proposal(_SWITCH_UPGRADE) + cs.process_client_switch_proposal(_SWITCH_CONNECT) + cs.process_event(CLIENT, Request) + cs.process_event(CLIENT, EndOfMessage) + assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE} + cs.process_event( + SERVER, _response_type_for_switch[server_switch], server_switch + ) + if server_switch is None: + assert cs.states == {CLIENT: DONE, SERVER: SEND_BODY} + else: + assert cs.states == {CLIENT: SWITCHED_PROTOCOL, SERVER: SWITCHED_PROTOCOL} + + +def test_ConnectionState_inconsistent_protocol_switch() -> None: + for client_switches, server_switch in [ + ([], _SWITCH_CONNECT), + ([], _SWITCH_UPGRADE), + ([_SWITCH_UPGRADE], _SWITCH_CONNECT), + ([_SWITCH_CONNECT], _SWITCH_UPGRADE), + ]: + cs = ConnectionState() + for client_switch in client_switches: # type: ignore[attr-defined] + cs.process_client_switch_proposal(client_switch) + cs.process_event(CLIENT, Request) + with pytest.raises(LocalProtocolError): + cs.process_event(SERVER, Response, server_switch) + + +def test_ConnectionState_keepalive_protocol_switch_interaction() -> None: + # keep_alive=False + pending_switch_proposals + cs = ConnectionState() + cs.process_client_switch_proposal(_SWITCH_UPGRADE) + cs.process_event(CLIENT, Request) + cs.process_keep_alive_disabled() + cs.process_event(CLIENT, Data) + assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE} + + # the protocol switch "wins" + cs.process_event(CLIENT, EndOfMessage) + assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE} + + # but when the server denies the request, keep_alive comes back into play + cs.process_event(SERVER, Response) + assert cs.states == {CLIENT: MUST_CLOSE, SERVER: SEND_BODY} + + +def test_ConnectionState_reuse() -> None: + cs = ConnectionState() + + with pytest.raises(LocalProtocolError): + cs.start_next_cycle() + + cs.process_event(CLIENT, Request) + cs.process_event(CLIENT, EndOfMessage) + + with pytest.raises(LocalProtocolError): + cs.start_next_cycle() + + cs.process_event(SERVER, Response) + cs.process_event(SERVER, EndOfMessage) + + cs.start_next_cycle() + assert cs.states == {CLIENT: IDLE, SERVER: IDLE} + + # No keepalive + + cs.process_event(CLIENT, Request) + cs.process_keep_alive_disabled() + cs.process_event(CLIENT, EndOfMessage) + cs.process_event(SERVER, Response) + cs.process_event(SERVER, EndOfMessage) + + with pytest.raises(LocalProtocolError): + cs.start_next_cycle() + + # One side closed + + cs = ConnectionState() + cs.process_event(CLIENT, Request) + cs.process_event(CLIENT, EndOfMessage) + cs.process_event(CLIENT, ConnectionClosed) + cs.process_event(SERVER, Response) + cs.process_event(SERVER, EndOfMessage) + + with pytest.raises(LocalProtocolError): + cs.start_next_cycle() + + # Succesful protocol switch + + cs = ConnectionState() + cs.process_client_switch_proposal(_SWITCH_UPGRADE) + cs.process_event(CLIENT, Request) + cs.process_event(CLIENT, EndOfMessage) + cs.process_event(SERVER, InformationalResponse, _SWITCH_UPGRADE) + + with pytest.raises(LocalProtocolError): + cs.start_next_cycle() + + # Failed protocol switch + + cs = ConnectionState() + cs.process_client_switch_proposal(_SWITCH_UPGRADE) + cs.process_event(CLIENT, Request) + cs.process_event(CLIENT, EndOfMessage) + cs.process_event(SERVER, Response) + cs.process_event(SERVER, EndOfMessage) + + cs.start_next_cycle() + assert cs.states == {CLIENT: IDLE, SERVER: IDLE} + + +def test_server_request_is_illegal() -> None: + # There used to be a bug in how we handled the Request special case that + # made this allowed... + cs = ConnectionState() + with pytest.raises(LocalProtocolError): + cs.process_event(SERVER, Request) diff --git a/Backend/venv/lib/python3.12/site-packages/h11/tests/test_util.py b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_util.py new file mode 100644 index 00000000..79bc0951 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/h11/tests/test_util.py @@ -0,0 +1,112 @@ +import re +import sys +import traceback +from typing import NoReturn + +import pytest + +from .._util import ( + bytesify, + LocalProtocolError, + ProtocolError, + RemoteProtocolError, + Sentinel, + validate, +) + + +def test_ProtocolError() -> None: + with pytest.raises(TypeError): + ProtocolError("abstract base class") + + +def test_LocalProtocolError() -> None: + try: + raise LocalProtocolError("foo") + except LocalProtocolError as e: + assert str(e) == "foo" + assert e.error_status_hint == 400 + + try: + raise LocalProtocolError("foo", error_status_hint=418) + except LocalProtocolError as e: + assert str(e) == "foo" + assert e.error_status_hint == 418 + + def thunk() -> NoReturn: + raise LocalProtocolError("a", error_status_hint=420) + + try: + try: + thunk() + except LocalProtocolError as exc1: + orig_traceback = "".join(traceback.format_tb(sys.exc_info()[2])) + exc1._reraise_as_remote_protocol_error() + except RemoteProtocolError as exc2: + assert type(exc2) is RemoteProtocolError + assert exc2.args == ("a",) + assert exc2.error_status_hint == 420 + new_traceback = "".join(traceback.format_tb(sys.exc_info()[2])) + assert new_traceback.endswith(orig_traceback) + + +def test_validate() -> None: + my_re = re.compile(rb"(?P[0-9]+)\.(?P[0-9]+)") + with pytest.raises(LocalProtocolError): + validate(my_re, b"0.") + + groups = validate(my_re, b"0.1") + assert groups == {"group1": b"0", "group2": b"1"} + + # successful partial matches are an error - must match whole string + with pytest.raises(LocalProtocolError): + validate(my_re, b"0.1xx") + with pytest.raises(LocalProtocolError): + validate(my_re, b"0.1\n") + + +def test_validate_formatting() -> None: + my_re = re.compile(rb"foo") + + with pytest.raises(LocalProtocolError) as excinfo: + validate(my_re, b"", "oops") + assert "oops" in str(excinfo.value) + + with pytest.raises(LocalProtocolError) as excinfo: + validate(my_re, b"", "oops {}") + assert "oops {}" in str(excinfo.value) + + with pytest.raises(LocalProtocolError) as excinfo: + validate(my_re, b"", "oops {} xx", 10) + assert "oops 10 xx" in str(excinfo.value) + + +def test_make_sentinel() -> None: + class S(Sentinel, metaclass=Sentinel): + pass + + assert repr(S) == "S" + assert S == S + assert type(S).__name__ == "S" + assert S in {S} + assert type(S) is S + + class S2(Sentinel, metaclass=Sentinel): + pass + + assert repr(S2) == "S2" + assert S != S2 + assert S not in {S2} + assert type(S) is not type(S2) + + +def test_bytesify() -> None: + assert bytesify(b"123") == b"123" + assert bytesify(bytearray(b"123")) == b"123" + assert bytesify("123") == b"123" + + with pytest.raises(UnicodeEncodeError): + bytesify("\u1234") + + with pytest.raises(TypeError): + bytesify(10) diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/INSTALLER b/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/LICENSE.md b/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/LICENSE.md new file mode 100644 index 00000000..311b2b56 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/LICENSE.md @@ -0,0 +1,27 @@ +Copyright © 2020, [Encode OSS Ltd](https://www.encode.io/). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/METADATA b/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/METADATA new file mode 100644 index 00000000..3bcd8aee --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/METADATA @@ -0,0 +1,542 @@ +Metadata-Version: 2.1 +Name: httpcore +Version: 0.17.3 +Summary: A minimal low-level HTTP client. +Home-page: https://github.com/encode/httpcore +Author: Tom Christie +Author-email: tom@tomchristie.com +License: BSD +Project-URL: Documentation, https://www.encode.io/httpcore +Project-URL: Source, https://github.com/encode/httpcore +Classifier: Development Status :: 3 - Alpha +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Topic :: Internet :: WWW/HTTP +Classifier: Framework :: AsyncIO +Classifier: Framework :: Trio +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.7 +Description-Content-Type: text/markdown +License-File: LICENSE.md +Requires-Dist: h11 (<0.15,>=0.13) +Requires-Dist: sniffio (==1.*) +Requires-Dist: anyio (<5.0,>=3.0) +Requires-Dist: certifi +Provides-Extra: http2 +Requires-Dist: h2 (<5,>=3) ; extra == 'http2' +Provides-Extra: socks +Requires-Dist: socksio (==1.*) ; extra == 'socks' + +# HTTP Core + +[![Test Suite](https://github.com/encode/httpcore/workflows/Test%20Suite/badge.svg)](https://github.com/encode/httpcore/actions) +[![Package version](https://badge.fury.io/py/httpcore.svg)](https://pypi.org/project/httpcore/) + +> *Do one thing, and do it well.* + +The HTTP Core package provides a minimal low-level HTTP client, which does +one thing only. Sending HTTP requests. + +It does not provide any high level model abstractions over the API, +does not handle redirects, multipart uploads, building authentication headers, +transparent HTTP caching, URL parsing, session cookie handling, +content or charset decoding, handling JSON, environment based configuration +defaults, or any of that Jazz. + +Some things HTTP Core does do: + +* Sending HTTP requests. +* Thread-safe / task-safe connection pooling. +* HTTP(S) proxy & SOCKS proxy support. +* Supports HTTP/1.1 and HTTP/2. +* Provides both sync and async interfaces. +* Async backend support for `asyncio` and `trio`. + +## Requirements + +Python 3.7+ + +## Installation + +For HTTP/1.1 only support, install with: + +```shell +$ pip install httpcore +``` + +For HTTP/1.1 and HTTP/2 support, install with: + +```shell +$ pip install httpcore[http2] +``` + +For SOCKS proxy support, install with: + +```shell +$ pip install httpcore[socks] +``` + +# Sending requests + +Send an HTTP request: + +```python +import httpcore + +response = httpcore.request("GET", "https://www.example.com/") + +print(response) +# +print(response.status) +# 200 +print(response.headers) +# [(b'Accept-Ranges', b'bytes'), (b'Age', b'557328'), (b'Cache-Control', b'max-age=604800'), ...] +print(response.content) +# b'\n\n\nExample Domain\n\n\n ...' +``` + +The top-level `httpcore.request()` function is provided for convenience. In practice whenever you're working with `httpcore` you'll want to use the connection pooling functionality that it provides. + +```python +import httpcore + +http = httpcore.ConnectionPool() +response = http.request("GET", "https://www.example.com/") +``` + +Once you're ready to get going, [head over to the documentation](https://www.encode.io/httpcore/). + +## Motivation + +You *probably* don't want to be using HTTP Core directly. It might make sense if +you're writing something like a proxy service in Python, and you just want +something at the lowest possible level, but more typically you'll want to use +a higher level client library, such as `httpx`. + +The motivation for `httpcore` is: + +* To provide a reusable low-level client library, that other packages can then build on top of. +* To provide a *really clear interface split* between the networking code and client logic, + so that each is easier to understand and reason about in isolation. + + +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## 0.17.3 (5th July 2023) + +- Support async cancellations, ensuring that the connection pool is left in a clean state when cancellations occur. (#726) +- The networking backend interface has [been added to the public API](https://www.encode.io/httpcore/network-backends). Some classes which were previously private implementation detail are now part of the top-level public API. (#699) +- Graceful handling of HTTP/2 GoAway frames, with requests being transparently retried on a new connection. (#730) +- Add exceptions when a synchronous `trace callback` is passed to an asynchronous request or an asynchronous `trace callback` is passed to a synchronous request. (#717) + +## 0.17.2 (May 23th, 2023) + +- Add `socket_options` argument to `ConnectionPool` and `HTTProxy` classes. (#668) +- Improve logging with per-module logger names. (#690) +- Add `sni_hostname` request extension. (#696) +- Resolve race condition during import of `anyio` package. (#692) +- Enable TCP_NODELAY for all synchronous sockets. (#651) + +## 0.17.1 (May 17th, 2023) + +- If 'retries' is set, then allow retries if an SSL handshake error occurs. (#669) +- Improve correctness of tracebacks on network exceptions, by raising properly chained exceptions. (#678) +- Prevent connection-hanging behaviour when HTTP/2 connections are closed by a server-sent 'GoAway' frame. (#679) +- Fix edge-case exception when removing requests from the connection pool. (#680) +- Fix pool timeout edge-case. (#688) + +## 0.17.0 (March 16th, 2023) + +- Add DEBUG level logging. (#648) +- Respect HTTP/2 max concurrent streams when settings updates are sent by server. (#652) +- Increase the allowable HTTP header size to 100kB. (#647) +- Add `retries` option to SOCKS proxy classes. (#643) + +## 0.16.3 (December 20th, 2022) + +- Allow `ws` and `wss` schemes. Allows us to properly support websocket upgrade connections. (#625) +- Forwarding HTTP proxies use a connection-per-remote-host. Required by some proxy implementations. (#637) +- Don't raise `RuntimeError` when closing a connection pool with active connections. Removes some error cases when cancellations are used. (#631) +- Lazy import `anyio`, so that it's no longer a hard dependancy, and isn't imported if unused. (#639) + +## 0.16.2 (November 25th, 2022) + +- Revert 'Fix async cancellation behaviour', which introduced race conditions. (#627) +- Raise `RuntimeError` if attempting to us UNIX domain sockets on Windows. (#619) + +## 0.16.1 (November 17th, 2022) + +- Fix HTTP/1.1 interim informational responses, such as "100 Continue". (#605) + +## 0.16.0 (October 11th, 2022) + +- Support HTTP/1.1 informational responses. (#581) +- Fix async cancellation behaviour. (#580) +- Support `h11` 0.14. (#579) + +## 0.15.0 (May 17th, 2022) + +- Drop Python 3.6 support (#535) +- Ensure HTTP proxy CONNECT requests include `timeout` configuration. (#506) +- Switch to explicit `typing.Optional` for type hints. (#513) +- For `trio` map OSError exceptions to `ConnectError`. (#543) + +## 0.14.7 (February 4th, 2022) + +- Requests which raise a PoolTimeout need to be removed from the pool queue. (#502) +- Fix AttributeError that happened when Socks5Connection were terminated. (#501) + +## 0.14.6 (February 1st, 2022) + +- Fix SOCKS support for `http://` URLs. (#492) +- Resolve race condition around exceptions during streaming a response. (#491) + +## 0.14.5 (January 18th, 2022) + +- SOCKS proxy support. (#478) +- Add proxy_auth argument to HTTPProxy. (#481) +- Improve error message on 'RemoteProtocolError' exception when server disconnects without sending a response. (#479) + +## 0.14.4 (January 5th, 2022) + +- Support HTTP/2 on HTTPS tunnelling proxies. (#468) +- Fix proxy headers missing on HTTP forwarding. (#456) +- Only instantiate SSL context if required. (#457) +- More robust HTTP/2 handling. (#253, #439, #440, #441) + +## 0.14.3 (November 17th, 2021) + +- Fix race condition when removing closed connections from the pool. (#437) + +## 0.14.2 (November 16th, 2021) + +- Failed connections no longer remain in the pool. (Pull #433) + +## 0.14.1 (November 12th, 2021) + +- `max_connections` becomes optional. (Pull #429) +- `certifi` is now included in the install dependancies. (Pull #428) +- `h2` is now strictly optional. (Pull #428) + +## 0.14.0 (November 11th, 2021) + +The 0.14 release is a complete reworking of `httpcore`, comprehensively addressing some underlying issues in the connection pooling, as well as substantially redesigning the API to be more user friendly. + +Some of the lower-level API design also makes the components more easily testable in isolation, and the package now has 100% test coverage. + +See [discussion #419](https://github.com/encode/httpcore/discussions/419) for a little more background. + +There's some other neat bits in there too, such as the "trace" extension, which gives a hook into inspecting the internal events that occur during the request/response cycle. This extension is needed for the HTTPX cli, in order to... + +* Log the point at which the connection is established, and the IP/port on which it is made. +* Determine if the outgoing request should log as HTTP/1.1 or HTTP/2, rather than having to assume it's HTTP/2 if the --http2 flag was passed. (Which may not actually be true.) +* Log SSL version info / certificate info. + +Note that `curio` support is not currently available in 0.14.0. If you're using `httpcore` with `curio` please get in touch, so we can assess if we ought to prioritize it as a feature or not. + +## 0.13.7 (September 13th, 2021) + +- Fix broken error messaging when URL scheme is missing, or a non HTTP(S) scheme is used. (Pull #403) + +## 0.13.6 (June 15th, 2021) + +### Fixed + +- Close sockets when read or write timeouts occur. (Pull #365) + +## 0.13.5 (June 14th, 2021) + +### Fixed + +- Resolved niggles with AnyIO EOF behaviours. (Pull #358, #362) + +## 0.13.4 (June 9th, 2021) + +### Added + +- Improved error messaging when URL scheme is missing, or a non HTTP(S) scheme is used. (Pull #354) + +### Fixed + +- Switched to `anyio` as the default backend implementation when running with `asyncio`. Resolves some awkward [TLS timeout issues](https://github.com/encode/httpx/discussions/1511). + +## 0.13.3 (May 6th, 2021) + +### Added + +- Support HTTP/2 prior knowledge, using `httpcore.SyncConnectionPool(http1=False)`. (Pull #333) + +### Fixed + +- Handle cases where environment does not provide `select.poll` support. (Pull #331) + +## 0.13.2 (April 29th, 2021) + +### Added + +- Improve error message for specific case of `RemoteProtocolError` where server disconnects without sending a response. (Pull #313) + +## 0.13.1 (April 28th, 2021) + +### Fixed + +- More resiliant testing for closed connections. (Pull #311) +- Don't raise exceptions on ungraceful connection closes. (Pull #310) + +## 0.13.0 (April 21st, 2021) + +The 0.13 release updates the core API in order to match the HTTPX Transport API, +introduced in HTTPX 0.18 onwards. + +An example of making requests with the new interface is: + +```python +with httpcore.SyncConnectionPool() as http: + status_code, headers, stream, extensions = http.handle_request( + method=b'GET', + url=(b'https', b'example.org', 443, b'/'), + headers=[(b'host', b'example.org'), (b'user-agent', b'httpcore')] + stream=httpcore.ByteStream(b''), + extensions={} + ) + body = stream.read() + print(status_code, body) +``` + +### Changed + +- The `.request()` method is now `handle_request()`. (Pull #296) +- The `.arequest()` method is now `.handle_async_request()`. (Pull #296) +- The `headers` argument is no longer optional. (Pull #296) +- The `stream` argument is no longer optional. (Pull #296) +- The `ext` argument is now named `extensions`, and is no longer optional. (Pull #296) +- The `"reason"` extension keyword is now named `"reason_phrase"`. (Pull #296) +- The `"reason_phrase"` and `"http_version"` extensions now use byte strings for their values. (Pull #296) +- The `httpcore.PlainByteStream()` class becomes `httpcore.ByteStream()`. (Pull #296) + +### Added + +- Streams now support a `.read()` interface. (Pull #296) + +### Fixed + +- Task cancellation no longer leaks connections from the connection pool. (Pull #305) + +## 0.12.3 (December 7th, 2020) + +### Fixed + +- Abort SSL connections on close rather than waiting for remote EOF when using `asyncio`. (Pull #167) +- Fix exception raised in case of connect timeouts when using the `anyio` backend. (Pull #236) +- Fix `Host` header precedence for `:authority` in HTTP/2. (Pull #241, #243) +- Handle extra edge case when detecting for socket readability when using `asyncio`. (Pull #242, #244) +- Fix `asyncio` SSL warning when using proxy tunneling. (Pull #249) + +## 0.12.2 (November 20th, 2020) + +### Fixed + +- Properly wrap connect errors on the asyncio backend. (Pull #235) +- Fix `ImportError` occurring on Python 3.9 when using the HTTP/1.1 sync client in a multithreaded context. (Pull #237) + +## 0.12.1 (November 7th, 2020) + +### Added + +- Add connect retries. (Pull #221) + +### Fixed + +- Tweak detection of dropped connections, resolving an issue with open files limits on Linux. (Pull #185) +- Avoid leaking connections when establishing an HTTP tunnel to a proxy has failed. (Pull #223) +- Properly wrap OS errors when using `trio`. (Pull #225) + +## 0.12.0 (October 6th, 2020) + +### Changed + +- HTTP header casing is now preserved, rather than always sent in lowercase. (#216 and python-hyper/h11#104) + +### Added + +- Add Python 3.9 to officially supported versions. + +### Fixed + +- Gracefully handle a stdlib asyncio bug when a connection is closed while it is in a paused-for-reading state. (#201) + +## 0.11.1 (September 28nd, 2020) + +### Fixed + +- Add await to async semaphore release() coroutine (#197) +- Drop incorrect curio classifier (#192) + +## 0.11.0 (September 22nd, 2020) + +The Transport API with 0.11.0 has a couple of significant changes. + +Firstly we've moved changed the request interface in order to allow extensions, which will later enable us to support features +such as trailing headers, HTTP/2 server push, and CONNECT/Upgrade connections. + +The interface changes from: + +```python +def request(method, url, headers, stream, timeout): + return (http_version, status_code, reason, headers, stream) +``` + +To instead including an optional dictionary of extensions on the request and response: + +```python +def request(method, url, headers, stream, ext): + return (status_code, headers, stream, ext) +``` + +Having an open-ended extensions point will allow us to add later support for various optional features, that wouldn't otherwise be supported without these API changes. + +In particular: + +* Trailing headers support. +* HTTP/2 Server Push +* sendfile. +* Exposing raw connection on CONNECT, Upgrade, HTTP/2 bi-di streaming. +* Exposing debug information out of the API, including template name, template context. + +Currently extensions are limited to: + +* request: `timeout` - Optional. Timeout dictionary. +* response: `http_version` - Optional. Include the HTTP version used on the response. +* response: `reason` - Optional. Include the reason phrase used on the response. Only valid with HTTP/1.*. + +See https://github.com/encode/httpx/issues/1274#issuecomment-694884553 for the history behind this. + +Secondly, the async version of `request` is now namespaced as `arequest`. + +This allows concrete transports to support both sync and async implementations on the same class. + +### Added + +- Add curio support. (Pull #168) +- Add anyio support, with `backend="anyio"`. (Pull #169) + +### Changed + +- Update the Transport API to use 'ext' for optional extensions. (Pull #190) +- Update the Transport API to use `.request` and `.arequest` so implementations can support both sync and async. (Pull #189) + +## 0.10.2 (August 20th, 2020) + +### Added + +- Added Unix Domain Socket support. (Pull #139) + +### Fixed + +- Always include the port on proxy CONNECT requests. (Pull #154) +- Fix `max_keepalive_connections` configuration. (Pull #153) +- Fixes behaviour in HTTP/1.1 where server disconnects can be used to signal the end of the response body. (Pull #164) + +## 0.10.1 (August 7th, 2020) + +- Include `max_keepalive_connections` on `AsyncHTTPProxy`/`SyncHTTPProxy` classes. + +## 0.10.0 (August 7th, 2020) + +The most notable change in the 0.10.0 release is that HTTP/2 support is now fully optional. + +Use either `pip install httpcore` for HTTP/1.1 support only, or `pip install httpcore[http2]` for HTTP/1.1 and HTTP/2 support. + +### Added + +- HTTP/2 support becomes optional. (Pull #121, #130) +- Add `local_address=...` support. (Pull #100, #134) +- Add `PlainByteStream`, `IteratorByteStream`, `AsyncIteratorByteStream`. The `AsyncByteSteam` and `SyncByteStream` classes are now pure interface classes. (#133) +- Add `LocalProtocolError`, `RemoteProtocolError` exceptions. (Pull #129) +- Add `UnsupportedProtocol` exception. (Pull #128) +- Add `.get_connection_info()` method. (Pull #102, #137) +- Add better TRACE logs. (Pull #101) + +### Changed + +- `max_keepalive` is deprecated in favour of `max_keepalive_connections`. (Pull #140) + +### Fixed + +- Improve handling of server disconnects. (Pull #112) + +## 0.9.1 (May 27th, 2020) + +### Fixed + +- Proper host resolution for sync case, including IPv6 support. (Pull #97) +- Close outstanding connections when connection pool is closed. (Pull #98) + +## 0.9.0 (May 21th, 2020) + +### Changed + +- URL port becomes an `Optional[int]` instead of `int`. (Pull #92) + +### Fixed + +- Honor HTTP/2 max concurrent streams settings. (Pull #89, #90) +- Remove incorrect debug log. (Pull #83) + +## 0.8.4 (May 11th, 2020) + +### Added + +- Logging via HTTPCORE_LOG_LEVEL and HTTPX_LOG_LEVEL environment variables +and TRACE level logging. (Pull #79) + +### Fixed + +- Reuse of connections on HTTP/2 in close concurrency situations. (Pull #81) + +## 0.8.3 (May 6rd, 2020) + +### Fixed + +- Include `Host` and `Accept` headers on proxy "CONNECT" requests. +- De-duplicate any headers also contained in proxy_headers. +- HTTP/2 flag not being passed down to proxy connections. + +## 0.8.2 (May 3rd, 2020) + +### Fixed + +- Fix connections using proxy forwarding requests not being added to the +connection pool properly. (Pull #70) + +## 0.8.1 (April 30th, 2020) + +### Changed + +- Allow inherintance of both `httpcore.AsyncByteStream`, `httpcore.SyncByteStream` without type conflicts. + +## 0.8.0 (April 30th, 2020) + +### Fixed + +- Fixed tunnel proxy support. + +### Added + +- New `TimeoutException` base class. + +## 0.7.0 (March 5th, 2020) + +- First integration with HTTPX. diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/RECORD b/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/RECORD new file mode 100644 index 00000000..8f8da5d5 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/RECORD @@ -0,0 +1,69 @@ +httpcore-0.17.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +httpcore-0.17.3.dist-info/LICENSE.md,sha256=_ctZFUx0y6uhahEkL3dAvqnyPW_rVUeRfYxflKgDkqU,1518 +httpcore-0.17.3.dist-info/METADATA,sha256=FXYdgFJ2kxh_T0yVw4qIdD031yF4wtYjTlU0TLrNjIk,18594 +httpcore-0.17.3.dist-info/RECORD,, +httpcore-0.17.3.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92 +httpcore-0.17.3.dist-info/top_level.txt,sha256=kYeSB6l1hBNp7JwgSwLajcsxRlrSCVKOhYKSkdgx798,59 +httpcore/__init__.py,sha256=Dza2gJlD90bgsFlu61Fo9RpTqTj7-mxGdJVA1X-MG_U,3338 +httpcore/__pycache__/__init__.cpython-312.pyc,, +httpcore/__pycache__/_api.cpython-312.pyc,, +httpcore/__pycache__/_exceptions.cpython-312.pyc,, +httpcore/__pycache__/_models.cpython-312.pyc,, +httpcore/__pycache__/_ssl.cpython-312.pyc,, +httpcore/__pycache__/_synchronization.cpython-312.pyc,, +httpcore/__pycache__/_trace.cpython-312.pyc,, +httpcore/__pycache__/_utils.cpython-312.pyc,, +httpcore/_api.py,sha256=IBR18qZQ8ETcghJXC1Gd-30WuKYRS0EyF2eC80_OBQ8,3167 +httpcore/_async/__init__.py,sha256=EWdl2v4thnAHzJpqjU4h2a8DUiGAvNiWrkii9pfhTf0,1221 +httpcore/_async/__pycache__/__init__.cpython-312.pyc,, +httpcore/_async/__pycache__/connection.cpython-312.pyc,, +httpcore/_async/__pycache__/connection_pool.cpython-312.pyc,, +httpcore/_async/__pycache__/http11.cpython-312.pyc,, +httpcore/_async/__pycache__/http2.cpython-312.pyc,, +httpcore/_async/__pycache__/http_proxy.cpython-312.pyc,, +httpcore/_async/__pycache__/interfaces.cpython-312.pyc,, +httpcore/_async/__pycache__/socks_proxy.cpython-312.pyc,, +httpcore/_async/connection.py,sha256=0LKFUXPkxusvJAUyHSJpy4mMkgf71BtOjtlaMBL4sUs,8420 +httpcore/_async/connection_pool.py,sha256=hj1viqcWZivNmoRu-QZjyuOvAFx3-Ae2rMpuK6OZhEM,14305 +httpcore/_async/http11.py,sha256=z58glbEF4YrDM03KVHkuNXNRpAQaJQ4qyblapA-mk4o,11968 +httpcore/_async/http2.py,sha256=KXwWZxZ-43vxIWzr1aTLErhaCodDzFr-XAvzc4fUb10,23879 +httpcore/_async/http_proxy.py,sha256=6jdp87k6_iNCAaM7bJF8wOw_4mX_xrXGU_c4qDjJxLk,13999 +httpcore/_async/interfaces.py,sha256=J2iq9rs7x3nKS6iCfntjHY0Woast6V_HuXuE8rs3HmA,4486 +httpcore/_async/socks_proxy.py,sha256=7tFg_GuAL6WoV5-emaBaiDEmZBHdVODaQXd7nkOoGC8,13810 +httpcore/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +httpcore/_backends/__pycache__/__init__.cpython-312.pyc,, +httpcore/_backends/__pycache__/anyio.cpython-312.pyc,, +httpcore/_backends/__pycache__/auto.cpython-312.pyc,, +httpcore/_backends/__pycache__/base.cpython-312.pyc,, +httpcore/_backends/__pycache__/mock.cpython-312.pyc,, +httpcore/_backends/__pycache__/sync.cpython-312.pyc,, +httpcore/_backends/__pycache__/trio.cpython-312.pyc,, +httpcore/_backends/anyio.py,sha256=mU8gtunBSLxESGkU0Iy1ZMgumDlAeMkwBjFE3kZiCnc,5208 +httpcore/_backends/auto.py,sha256=8r0ipGxSwXoCb_xKQAyRwL1UzfXVbO4Ee2y8vYQv3Ic,1654 +httpcore/_backends/base.py,sha256=Qsb8b_PSiVP1ldHHGXHxQzJ1Qlzj2r8KR9KQeANkSbE,3218 +httpcore/_backends/mock.py,sha256=S4IADhC6kE22ge_jR_WHlEUkD6QAsXnwz26DSWZLcG4,4179 +httpcore/_backends/sync.py,sha256=Q2skeGyuAt6ETqPjZkiw-iUU0zh_nFXvCFkrsT-Y9GI,4444 +httpcore/_backends/trio.py,sha256=INOeHEkA8pO6AsSqjColWcayM0FQSyGi1hpaQghjrCs,6078 +httpcore/_exceptions.py,sha256=7zb3KNiG0qmfUNIdFgdaUSbn2Pu3oztghi6Vg7i-LJU,1185 +httpcore/_models.py,sha256=1aM8l5D3CbP5QKXCBsdzAWVCHSm0t7UVrCNVTaXUPI8,16343 +httpcore/_ssl.py,sha256=srqmSNU4iOUvWF-SrJvb8G_YEbHFELOXQOwdDIBTS9c,187 +httpcore/_sync/__init__.py,sha256=JBDIgXt5la1LCJ1sLQeKhjKFpLnpNr8Svs6z2ni3fgg,1141 +httpcore/_sync/__pycache__/__init__.cpython-312.pyc,, +httpcore/_sync/__pycache__/connection.cpython-312.pyc,, +httpcore/_sync/__pycache__/connection_pool.cpython-312.pyc,, +httpcore/_sync/__pycache__/http11.cpython-312.pyc,, +httpcore/_sync/__pycache__/http2.cpython-312.pyc,, +httpcore/_sync/__pycache__/http_proxy.cpython-312.pyc,, +httpcore/_sync/__pycache__/interfaces.cpython-312.pyc,, +httpcore/_sync/__pycache__/socks_proxy.cpython-312.pyc,, +httpcore/_sync/connection.py,sha256=8IOzYLwK8_GuUPz9fF3z0EARb-ueGeKW6ZDXRPdNluQ,8209 +httpcore/_sync/connection_pool.py,sha256=1iwYLdiq3pi9LBvpMZ8O8gWdb56qqPlm6rp35zeORBQ,13928 +httpcore/_sync/http11.py,sha256=FTg8wAzMu1kSDjCQqQUXIslJ90aFrWnO6eL459K8SYs,11629 +httpcore/_sync/http2.py,sha256=lkpHesGkrwzIA4oHLyClJf5IAwRLcaAFMnmffAahAK4,23343 +httpcore/_sync/http_proxy.py,sha256=PcTIz3XuYT3rKvdaruAtH5W7EQvjofOcUHTv9YXiOc0,13761 +httpcore/_sync/interfaces.py,sha256=EM4PTf-rgkclzisFcrTyx1G8FwraoffE8rbckOznX_o,4365 +httpcore/_sync/socks_proxy.py,sha256=BLRF27DHvsfpdZ7WVzK3Ba3vxN6zk0iD_3xRCzDt-2Q,13595 +httpcore/_synchronization.py,sha256=_d_vHqylvzm1Jh58_0G7i-1VwCg3Gu39Cgd4nWASvP0,8751 +httpcore/_trace.py,sha256=akf5PsWVq3rZjqmXniomU59OY37K7JHoeNDCQ4GU84E,3954 +httpcore/_utils.py,sha256=9QPh5ib4JilWX4dBCC_XO6wdBY4b0kbUGgfV3QfBANc,1525 +httpcore/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/WHEEL b/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/WHEEL new file mode 100644 index 00000000..1f37c02f --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.40.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/top_level.txt b/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/top_level.txt new file mode 100644 index 00000000..613e4350 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore-0.17.3.dist-info/top_level.txt @@ -0,0 +1,4 @@ +httpcore +httpcore/_async +httpcore/_backends +httpcore/_sync diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/__init__.py b/Backend/venv/lib/python3.12/site-packages/httpcore/__init__.py new file mode 100644 index 00000000..da95f8d0 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/__init__.py @@ -0,0 +1,139 @@ +from ._api import request, stream +from ._async import ( + AsyncConnectionInterface, + AsyncConnectionPool, + AsyncHTTP2Connection, + AsyncHTTP11Connection, + AsyncHTTPConnection, + AsyncHTTPProxy, + AsyncSOCKSProxy, +) +from ._backends.base import ( + SOCKET_OPTION, + AsyncNetworkBackend, + AsyncNetworkStream, + NetworkBackend, + NetworkStream, +) +from ._backends.mock import AsyncMockBackend, AsyncMockStream, MockBackend, MockStream +from ._backends.sync import SyncBackend +from ._exceptions import ( + ConnectError, + ConnectionNotAvailable, + ConnectTimeout, + LocalProtocolError, + NetworkError, + PoolTimeout, + ProtocolError, + ProxyError, + ReadError, + ReadTimeout, + RemoteProtocolError, + TimeoutException, + UnsupportedProtocol, + WriteError, + WriteTimeout, +) +from ._models import URL, Origin, Request, Response +from ._ssl import default_ssl_context +from ._sync import ( + ConnectionInterface, + ConnectionPool, + HTTP2Connection, + HTTP11Connection, + HTTPConnection, + HTTPProxy, + SOCKSProxy, +) + +# The 'httpcore.AnyIOBackend' class is conditional on 'anyio' being installed. +try: + from ._backends.anyio import AnyIOBackend +except ImportError: # pragma: nocover + + class AnyIOBackend: # type: ignore + def __init__(self, *args, **kwargs): # type: ignore + msg = ( + "Attempted to use 'httpcore.AnyIOBackend' but 'anyio' is not installed." + ) + raise RuntimeError(msg) + + +# The 'httpcore.TrioBackend' class is conditional on 'trio' being installed. +try: + from ._backends.trio import TrioBackend +except ImportError: # pragma: nocover + + class TrioBackend: # type: ignore + def __init__(self, *args, **kwargs): # type: ignore + msg = "Attempted to use 'httpcore.TrioBackend' but 'trio' is not installed." + raise RuntimeError(msg) + + +__all__ = [ + # top-level requests + "request", + "stream", + # models + "Origin", + "URL", + "Request", + "Response", + # async + "AsyncHTTPConnection", + "AsyncConnectionPool", + "AsyncHTTPProxy", + "AsyncHTTP11Connection", + "AsyncHTTP2Connection", + "AsyncConnectionInterface", + "AsyncSOCKSProxy", + # sync + "HTTPConnection", + "ConnectionPool", + "HTTPProxy", + "HTTP11Connection", + "HTTP2Connection", + "ConnectionInterface", + "SOCKSProxy", + # network backends, implementations + "SyncBackend", + "AnyIOBackend", + "TrioBackend", + # network backends, mock implementations + "AsyncMockBackend", + "AsyncMockStream", + "MockBackend", + "MockStream", + # network backends, interface + "AsyncNetworkStream", + "AsyncNetworkBackend", + "NetworkStream", + "NetworkBackend", + # util + "default_ssl_context", + "SOCKET_OPTION", + # exceptions + "ConnectionNotAvailable", + "ProxyError", + "ProtocolError", + "LocalProtocolError", + "RemoteProtocolError", + "UnsupportedProtocol", + "TimeoutException", + "PoolTimeout", + "ConnectTimeout", + "ReadTimeout", + "WriteTimeout", + "NetworkError", + "ConnectError", + "ReadError", + "WriteError", +] + +__version__ = "0.17.3" + + +__locals = locals() +for __name in __all__: + if not __name.startswith("__"): + setattr(__locals[__name], "__module__", "httpcore") # noqa diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..41b781240d3c168099360a682fa8f85cf026d4d9 GIT binary patch literal 3145 zcmbVO&2JmW6`$SZ_qRz=mTlRlK5T}zMM{=qJCWiq+G&isk|8TiGDW*skz;wIon2;j zX-P&42(1qR;zMrnG04sErGGrMzhj1gEKUXvowcuG>`MNfD2L=u@2B8F47V%(IGrU%eXA% zQEQkU#0TX#W{uE8_z*pe57Q&~2pz?v^e8?`kKtqVI6h7*xI)M97#+vsbOKM%NjynU z;1l#DK1r*%D))$6Z_ygA(Np*oJ&jM(Gx&^@C#)%2$8|YQS`9jlr|Aryp=a?~I*VuN zIed=3jo+r{@p%O$A>x#EfnLNHgIU&km(Jli1+AiX;Tr|dw-4a=R75U44XDGH$^3Qn z%Q1xT6>{a%--Xon-|ga4!7e;Zc~ll_uaV2-${{gtm)b*5HK{@hdNdJ1o8<{a-kC%! zs~{Eq1hvx-Q9JWg^=u5v!1g$IkEOWv#ziT-sEOBUA8~z7)P*ZuFku z&5LqbAiN`^;kkV_**)r7gcszZpE<4FZTzo;B-kT8s7>`1HHD||2&RJX*YZ3Q1a*Tk zhw- zQoZMSotDE$ebA|DWu@y?s)oI3I@OBlR&2+sn6~Q~mPOk2CnAQWuI-7Mk-2yh;SrZu zcX`BMt1geNt;@GY-BmXt+^UG@&955w97-Ci_D176ao0Sj)A&Htfi-o_an?+GwGj}~ zxKHf+4a;0Y@08 zaKt?*U<>`N@NrK=*}}hKsC4|X`@A^u$IQgGP@x48irC0DDA9GE(sk;zyB3Tyy8c<$ zuzU@h7q(9jdr!RB9024+!g(=p;{qO{Z8egd^ld;5ExOE1=f@?!DR%hriNF4T5WBxZqKf<3PR z^vQ%+836ot$y7r!L0X$;8d!lN>?FVmfGWUS07(G&2f^SE21@`yT38ey4NwO-4bT9X z0yqP33Sb(b3?Oq0A7uvsARJZ#7y`%vh`%m~6#$?YSq&fq00qa60ziKsl! z;0VA7z;S>{fWrX80LK6(1U!gM*Jm!&XSu5D?1~WZMA!!lwI+|~M#t=P!ehDtH>V%2 zEB_%RmNra?H5+);;lvKnXK39@8U_;WxDR?~)9O~dx1pf?!L9VG7|ImCxVV*gkt@GOsxrUz{)_a1Kcz>%OOHNY z{ysha=S2R?#N*-b3YD+V{66)4V(wK`SpBEBrlLgYH4>ZdM558jZ6u%<71RIr4ogiG sG#Y3G^rB+=#)WS9R*f|^^ldWM96`^te6y@Q8&UvA^37A)vzj9Ce?9X-jQ{`u literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_api.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_api.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce707b9ee334fd3397e0d3b242519e4346919995 GIT binary patch literal 3789 zcmeHKO>7fK6rS~F*Y?JC5(1E z-+S}sy|-`XeQ)x8I;|mi_8rdFrc((0%$exv3yHOagwQQyB2%)EEtMpR(=jV%%O%}W>aS7s#@wJ(tm7OCK~qv_WQ1?%V8o){Y^F3 z|A2Ej$#d7abrTKn78+$CodjKkIAb ziym)j$;E7^!3RChea{Du!h>vFi}*&f>E0ULM^r#m$wo=cbBh&P`s(mPdh^7%!X-sHt-oiXn9| zA_~RVC%6n;m9wYE&yE+zv*rATJ=o(QE;vuzAy3lg1m^L04P+D>BDXb%d4^L7V%ta+ ztnCUByukwIQGKTA5!Ue+p1e)D2=k1?nJo^nSrZ4>ppcQ-j=in<2%H(%sxeQz!-9Lz zYry`nEsHuqgr`0TB4nbMkHVR8&F1D<#Cts1ub9HMuw*-KIyE(h3tg|`hGmUGi5j@( zz*(>jUZg-*tcFQUI0IWw$Ht!VGce^cR-3U1o+nKfErA{OYBq5j9^Yc2e_kB_O=q#d z9Xx3hBghC3#~p?CLqXC3K5RvS$y|&A#3Mf*CM%~tRoCyS1L!an)Opw#KPu@}*CzU$ zv#6gYY~FM0`h@Ed>)S2ZvYt2UoX` z@V}uK+r6lN_Zm|BwRIWP*17IOK0IRF)JHoH$ji#n_+z-Y2PW5U!|*1c(!a+(KuWcx zek$LRpu=^cU^m~4b)(|P+X7VVmPcs$wJZ?R?fEB&i5CC|Y7RDVR4`$?>Vi_P)&WsK z0AkpG14B7nL@3+=&U`+oCXUnMIKNdT4sNgr&dZ_wTp7|EWp>%+k#-?StOJ zHtCb0f+~Nhj45{#Kzx$~(YH!LPJR~$_O7ZF`r>zc;~>oq_?jr~MGyKJD%9>AjM+ni{ydcWLnE%PXnp+cF4#N@lLHo8wDIuD!RCe4(v?sGUHW;dYW! z8p;gbJhrs|i({YZw~ziXsIR0CwR`zY3JnY|?ON%7sm;xT8#S}m$Ehg^#rk1k$L#@W rWUE^cbh`yn=$2DYcFSirx#cW(d#$>n3_a`vc1;G?zr}!}S;)&@VFd#D literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_exceptions.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_exceptions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..220706e0fadd8d45e43b07c6aa21101bd16d7437 GIT binary patch literal 3125 zcma);O>7%Q6vt4> z>5@i(>K+=EGzv84p)pD0KzltjE@=X2AB%V5_DY%ry30colJ)~lc_Q>lIskMx>+3{F zN;(L1kB9D(bO`9MhxSW40(8_vQ<9DW-Rn^skaQo={cND4wp-EzKo5H8prqqK(;m8~ zMl*;0#tEFssBYxE!-U~*;r6^`R4PU5nyX!|RhX-mjfxu<%$XNf0oJ-xFay%?+xiO3 zn}k9Z=&=4@D|aX*HE41=T@Mj)YKbnA8?E)~G=S4y*(vhj2q7ORge)mbsuODWS)_}~ z8c-}Y{TyavQC*~Ktv6Yuw-k-s0^KG030XUmK!QM ztZci{qFuCX$FK@4qY3;BS7p_L8<`cnoJTM0aI9@edU62DQzLtwms$3jRn1N@d(PpN z>^bhR(!^QL=U}d~XN|%fv&`&GX5Gw|idVChnsc37r%s%FK5K*QL%elMudh%E!1U(vIq9^`s=rHs@I|E^F z(a8v1)^u_kozL{ikov{=BvF5efq6(m@cb~OLQ?@ccS<`6w&H;95Jqy;Bsm4rNpS~X z=a$6^INdYcnY?KfOUBg_n^}bn%jYel%<_3Rn$MTHIbT9Qp3i?gZ| zwn1kL^I@4h(jsE0%>%zw^rGO^+O!Z{%!m{Y--AX5l2W7ih-MHSJaw9lhytsm-nH!c zN`(uDnIP!!0xvyVZZ8U;frR(UAqp(l+t%pvUHdUUntgsDM1h4;mslBcJ%HRnR6#TD zmqQd-uKxlr7^MJeX%s>8oL>!5V727sVwv%IXS%%?pS@y-Q3B0`Ukg!St=L7*OD(-; z%N)VzXioZN5CxV=w6l(VX_nAPOuKe^0;(+Eqs` z3z}E_I*5Yn%rIwxi@7cvDvROKO!;*X1=ndUP!}D!{%Fqmbr1#C(Q$dYh@8UMXfF6g z5Cs;A%EjrTA`=YFMZXH7%#a%`aLZv;2kuO`c5C>@L#Wsj?_PnYzR+* zDqez66n=ILY(u4#{zi8FPL6GmV}GPZ>Mz{Yzj^Kc)Pv#Gx7SBs-bkHUCrP+@mH01o zWP^@0lpdN`A3E9~P~U(5fw}t8Cc0ZHQATCs_5h`N{mM3hYC8gEWq8N0XLkIR^E-Zh t!sG94`UxCc+u`f|m^{`5|-l`|ec?(xU|!FQE+=Ku5cNI?Jq literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_models.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dfa3391a1d223bac59728245df212f3808158946 GIT binary patch literal 21753 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_ssl.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_ssl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..990abe206e8a1ac65567d4401327bcf6c91357aa GIT binary patch literal 612 zcmYjNy-piJ5T5m&4YKhm0g)yRq=AcMR*;lY3W$^xksEg|I9=|>_SwhnX?N{liAXN; z4nP$}i4;XVLLMO%0ts}`QPCke6;UT1n3C~p zB8Lz{$YN+tkTqFYKK1G&8}j znQI(`CsY0f4xn3n6qr&yXMEjx!)9ozPo^^%b!mXh>TWT!l!~)wfka=U7h=or$Jz@- zqH$km%dzl7Z-6pt4ZT%eef|zr zPm7d)5E|p!wh+B2Y4dGA=wTA_0VV?;N1u2))E$w$+SuIUD$-a>DZy`};vKEiKuF{s zExM5ovn8`6*=A9>Kx0cJ$|W^|Yv<{OvwrHVpE)l_?=S7jg}r)eul`*7Rr_B1!O!ij z%gW-YKBi=FZJB?l%>OMbHyR&zCZ{~~{0*|22P>rR=ils!upi@F`Gm~;QuUnFoy8ct Yg7OVGcO{E09W}n~9`F7Gq7TLY0Rl3N0ssI2 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_synchronization.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_synchronization.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..344ea37021ad08b06f1a8482aaa7da79d0a7db02 GIT binary patch literal 12565 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_trace.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/__pycache__/_trace.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..87b0cd0a3e2b1cd33ef688579efd7b045c307f86 GIT binary patch literal 5764 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_api.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_api.py new file mode 100644 index 00000000..854235f5 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_api.py @@ -0,0 +1,92 @@ +from contextlib import contextmanager +from typing import Iterator, Optional, Union + +from ._models import URL, Extensions, HeaderTypes, Response +from ._sync.connection_pool import ConnectionPool + + +def request( + method: Union[bytes, str], + url: Union[URL, bytes, str], + *, + headers: HeaderTypes = None, + content: Union[bytes, Iterator[bytes], None] = None, + extensions: Optional[Extensions] = None, +) -> Response: + """ + Sends an HTTP request, returning the response. + + ``` + response = httpcore.request("GET", "https://www.example.com/") + ``` + + Arguments: + method: The HTTP method for the request. Typically one of `"GET"`, + `"OPTIONS"`, `"HEAD"`, `"POST"`, `"PUT"`, `"PATCH"`, or `"DELETE"`. + url: The URL of the HTTP request. Either as an instance of `httpcore.URL`, + or as str/bytes. + headers: The HTTP request headers. Either as a dictionary of str/bytes, + or as a list of two-tuples of str/bytes. + content: The content of the request body. Either as bytes, + or as a bytes iterator. + extensions: A dictionary of optional extra information included on the request. + Possible keys include `"timeout"`. + + Returns: + An instance of `httpcore.Response`. + """ + with ConnectionPool() as pool: + return pool.request( + method=method, + url=url, + headers=headers, + content=content, + extensions=extensions, + ) + + +@contextmanager +def stream( + method: Union[bytes, str], + url: Union[URL, bytes, str], + *, + headers: HeaderTypes = None, + content: Union[bytes, Iterator[bytes], None] = None, + extensions: Optional[Extensions] = None, +) -> Iterator[Response]: + """ + Sends an HTTP request, returning the response within a content manager. + + ``` + with httpcore.stream("GET", "https://www.example.com/") as response: + ... + ``` + + When using the `stream()` function, the body of the response will not be + automatically read. If you want to access the response body you should + either use `content = response.read()`, or `for chunk in response.iter_content()`. + + Arguments: + method: The HTTP method for the request. Typically one of `"GET"`, + `"OPTIONS"`, `"HEAD"`, `"POST"`, `"PUT"`, `"PATCH"`, or `"DELETE"`. + url: The URL of the HTTP request. Either as an instance of `httpcore.URL`, + or as str/bytes. + headers: The HTTP request headers. Either as a dictionary of str/bytes, + or as a list of two-tuples of str/bytes. + content: The content of the request body. Either as bytes, + or as a bytes iterator. + extensions: A dictionary of optional extra information included on the request. + Possible keys include `"timeout"`. + + Returns: + An instance of `httpcore.Response`. + """ + with ConnectionPool() as pool: + with pool.stream( + method=method, + url=url, + headers=headers, + content=content, + extensions=extensions, + ) as response: + yield response diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_async/__init__.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/__init__.py new file mode 100644 index 00000000..88dc7f01 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/__init__.py @@ -0,0 +1,39 @@ +from .connection import AsyncHTTPConnection +from .connection_pool import AsyncConnectionPool +from .http11 import AsyncHTTP11Connection +from .http_proxy import AsyncHTTPProxy +from .interfaces import AsyncConnectionInterface + +try: + from .http2 import AsyncHTTP2Connection +except ImportError: # pragma: nocover + + class AsyncHTTP2Connection: # type: ignore + def __init__(self, *args, **kwargs) -> None: # type: ignore + raise RuntimeError( + "Attempted to use http2 support, but the `h2` package is not " + "installed. Use 'pip install httpcore[http2]'." + ) + + +try: + from .socks_proxy import AsyncSOCKSProxy +except ImportError: # pragma: nocover + + class AsyncSOCKSProxy: # type: ignore + def __init__(self, *args, **kwargs) -> None: # type: ignore + raise RuntimeError( + "Attempted to use SOCKS support, but the `socksio` package is not " + "installed. Use 'pip install httpcore[socks]'." + ) + + +__all__ = [ + "AsyncHTTPConnection", + "AsyncConnectionPool", + "AsyncHTTPProxy", + "AsyncHTTP11Connection", + "AsyncHTTP2Connection", + "AsyncConnectionInterface", + "AsyncSOCKSProxy", +] diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..561974529a63b3c1b5502187c12a7df38b45533f GIT binary patch literal 1625 zcmbVM&5zqe6n|qoaXuZ|ZObB9WVMJ@wB4A5E2>Bfq7~AzqV1tZ$eYH~#ER`P<4Je3 zr-Js#eXocMTJ^?>YY&|G4=i%x#4T!&K+A=Blf;oqIZ#Xf&71e-=l6ayztrm$kk5Ae zYVflG@EdMcPhly{f0Du}_~4TeBH|K)vKDHQ?&?a`LnA7<1tl9{F*03K$%U{KmEAId zGDZ}`N>p{L%UwCFN0w_57=T~AAD=Y+An)nW0Itjcz)L`?v4i+rP!)nD#}$;#|E}?K2ii_2t~V z-Ci4|!O9K$oE=T1W>aa_PIqEK`J-N+O53H}yR&xaFB_|t<^RF_lH8sG1(*2XYQFAk zCx)v}NYmgYY1-@Kf2OTu=n6rjL{Q%r%pRxI9tx3k z>~x$YjEn2`!?Cc%klF{s&Vilu`lH@}+Cgf^OxQu3ie4B}zh&Q-k=sd-taK_(pKx^Wz&&RO{BKk4sjGyV5k<#!{X7u55-d!~^KACkGQZ&)+X>(*Qv;7rSkoQg4z z_s$GV9QPvXd0EBtBIb`nlxv>%)wmZfEc_xy0msSm3kWz!1yw1`O$jIPhh~(mg%6q~ zX&S21yPw_q^seIA|K`h<%=3zTiT}kHRmV&ClCu6N4cM!ZMg>dKaBKvxOIQ(O(J1WP z@#0{u`;ZR{NzXSBRBLf;{B?vkbDe58_JJcP<6F5ARZWv46}i0ifEN+)zFSNP2czQl zny+P*etuo3h^(>QYEKX3q-h*r?BC)jJWM zMRjVjs%p(L77NWx&y$DH^IrJ7^U?4v91J&-Wd2_aM1nZkQhXini>Da?#A zv8Kk(?21~iKfh9{YG0dw&32)*@f=KI{b>{mlI9X-1-bouw)r2H^VF6f`L5W3ABtTQ Ver$9N{h3Y>%|`bP{n;jw_zz2hn>hdg literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/connection.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/connection.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e302b0c242402cc2aef907f5b6cdfa2941ae333c GIT binary patch literal 11870 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 X1k{0P`NTG@V*(j}&#ws*bO`?s^|&;4 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/connection_pool.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/connection_pool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cf79b7452884b0f78e2bb20c0ca3097f9d24e14e GIT binary patch literal 18281 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/http11.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/http11.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b6b054220f437a8682e1cfeb4a3b1e5ff98a2092 GIT binary patch literal 18032 zcmdsfS#TRimS7d?z^t z$c|mwqh3or9*f#`kLVc3T3WleWodlJOmRWp%LlbyZBXac1@&Hi(BL%$jb3BW z>BXRvyd9cD;0k|PxoT?1Ey>6P)Py#*Kr+!f888E#E--N+i zC71-u-*PvN&18TQb!c~ta|3D5Y)4vo$n7CbuH+#8t*jqQ#KqW_sG zAv+oki!!@!A`+E#`wvGa!Xf{Z%#BVT1}YXa%iLojNR`LwMX0axp_U&9O;X1aUdb)B7~WS?hZkpj6-M5S`S8}hsVOA(1b`!%NTa18dB4M zS}B8+)09Br#D+;tdzpa7%LW)P7ht{o4CmqH!lBX8hch*dMEy_&cAV%!U;^+h4)Cx? zE9-o|kUuE+e6qpk3x)&JQ;3^F|tL3N;bn0VLuC@CubM z7+>s9m@O~vOc+fs4gmJjPO(-c%;`ZRq+(e4??gi*cf01F2D(VfEToa4p}9j z4KU-nfbJE}s}JY}LqIDS$Jv1471nD~;_#jg7$I)Xikl#AAtjmx>!cg2%?-eK*){M1 zE7VYss=+Q43J&-c35XSrYp^Bw8)p?5utE9G6m_vsm`W{xR2Qi=t8`&Q{uT%&klvZw zDvXh`NR9Aac4$>upej%ZC6~j~0nZB3OIo24-WNgK4SiBMt_e5+-Qx5j`F2mQPi9UVk(DHa}2~%Vi8(RK@4+}5Y?&JBw5hZ znDPWlUQLNwRqVYK0&ml~16aRarHB@(*d(&WnhA82ID0MA9Q4!0&vG8;#Dl-RLT4Y0J z+)+;L9vmK(H$2q)L*MS<{{0Ui|KKIT4zO`h2q$c_&+w`o10W`T@L1B~6s8VxB!=N>tX)v)6QGiPk=R zRwTAGL$dX{(-U_#O3p@z7_XPs{A@eEbtNh)@mYGkr2K5tnWpPSuG8LA-b7t}ylz0M z8@Rc&a(GW=F#(3AE88^GkHCexD@-lW1DJ(-;^l{%Tt z1gSJtCy6viwQ6pdkl#E1&p;D{Hl!!BS_Vb^jQTmNmVO+jcs?d+r(TAo?>S(pH#Mi3 zf5ma|O+bmO+L{nePuxk{npY6bdTYP??YQPyN^G5@j?$B9_Q_BMm{~JLjcI&oCeA7? z4+~~9HW3Kv)`kpGT-QiY8T~lwtng1|!IwdI&HY{wkL(;hK_AmZ%hlWgE?S}DAf7bu zrA98RzZj*zjxieSF}F&cc?OIVAk5yO{|)^z)KBVU)jnRQO{p?zDyt>@AJ~$rObb-( zkJJ+FAJ~!_wf@X={gyllJ$#Hk#vS92Y3JBEZjPVRjVgIC#5{q8)l>^b<_)y*O)^GZ6h}qq zBO01|0yu_7+M1)x6Vwr!+g`l*tunu@}{PYE@Yc>I_y+M>o)zUhFk%9@3S?6Ft(E-7xnUg2V-RALugfhBnV}# zWB!oupg$CVElcVh_CN!de)&)=Ad7#Ut-N z_x5w~P1~eR+pbxAuG=dT_T~%PkBlD}<6HZrt$nfP{v<^Y(9VRjf$ zg<%RUNs2LBk_O7*iWgQ(h1IJRuW!DI0Lp*;xmTaNR@oY_?35}yuXXQ@cRwt3KOC!g zM6y1TusPzk>aT3oi$9iZoB!5UeZ8V3UePU8bf0A3Fea+%PO^V){l?j_>}-nJnpVwF zA}Kp*tI&!aN?FYIB!U|SPS7e#w=LAfOS&ce>$}(qAnxe9 zY2*s*35Pr3C`mXrCLEIwjhd~A+E$`&_DFR-H??e;{gz(ufHBd-nA{IN_FWQ3;ycoD1pF4`cajeH{_hgD zp4%{x=IyshR}gR$yY5bsg)h9Fuyv95-Q>OU#rVD(-b2G~Cv8+wKYcq{fDzJ60&e2x z|KqK8%2xhciq$)>7i?ZGsJ}^R%&pf;-LZ|WAMU)kYq@j)>MLl4@(Nn7a*2|PcuBKV z(!5;K5_hynjuzuJ30V_y+XO)4HpvwxK^Sf8I8T_A;dZ_Jpk^ctT^YPx=y<6y| zP4up2erYp<_?BMJt`6q1!w8YfmGrJw_OhErytZU_Id%B~ddSFMp%}ogFwP-6b7ccP zWZ|!ru!vU}G2KHC+1M-fNdE~-4{hQ<;gJ3lopY#>`J{p#^6;NjvWQnB{U=TI&=&TS zW~BdAPw&?7pBj+bD z&$iRMc9=f9PYd`}MhEFvjWkeRwb0PQt2Tn$`Jo!^RTn)}YPz~Xi?~~dG|=)!=IRzg z)5s4!puO5g4|STZwrdgJjx?XMG_>S%9%(+;@w-d3pIhkN1*XrfTEv}NOfT2(-OPMm zXV_cKe7@DW*U5ds(|h!WFSIP;M#r8m?H85wo+`r^ZVvHUBc^Yq_q6H0XyFm>;`i!} zU))dcrEFj9&;tG?r-k${O<3laPHwMT|7EET@oEn0`jQM@&Xx)H;AG468Vq!5%S2B? z;@U6?n~=33Ku@F<_hXtlOzHJgv=Gh*-&!`I3G4?Lyg_=y;BQj;b0E+SZtoqgI?L~SE zH$$yecuv$R>J2E01w_T+hGi`->EY?9%pDQIY={Qhpa7P{fXqi>)f1v*fjdu&KZ2S) zMsYVhWCqg<028|qpkYNs66)h~U^| zR#0(2L}$0;Uodl(g_I6)3^GI(0VpeGVM)BORw}HG7kZ>Z&pFS9N0$q`=XD8t$?1ku z4GTxkvCH? zr=E)!d!%9yNcOYVGuB&Lw#YI+xXJ)EtRJvMTPh?=#lrNt12Id*vZXVw>y&hzt7d$U zMcx4_eb=1`ZcdK(TKXA!$<|BvbugC-Dj;&HneM0AOIta_JGj0b+Dkn;z;ieXdx>z= z8~@B41+zEJQ_zM~$Z(Q6A0wJQ6c`-xQnylV>SkEoK(kS!T2@}}Vu3&4OEDbdU~`eS z4(A(+c?=3uxJqURFoz$hOoO4o{_%%|NCYhU z9zMfE;uw-6Ys%c%!RgQ;F#vc3M~Lu{Vqg;=hWPB}@54l>aleX1{1U()#zEE%3!BiU z_I~3vYj;f79kX^PN~_N9IJ0Aw(&$0M18}ky`Dy6X&|>X1$L9ILgxwjpS4sA&#f}S` zV)m+Kdr#cdBbj=V98llBVFGoBqsr=&x-?(C6^U0iOO?&bl`R*$2!r*@^98hw$B-fTWFofUgbtRw*z+sB@T^V_vF*S%~GW36ts7C-VX<;F|Z- z28?{Neg=#cCwMaRG{ed~W5w-I0vcdL0kZc|<|3<1ADk%4%rQR$Tcn6D8Yc2I*EUP$ z-|WVFO&ub(-+V1gsii!-7+tnE%xiC0>}1>3D3vr`I1npoTrSxWckGxSybi** z_MO6a%ib=#W^GE?ioUUxEOfk69k1FhRc*g!yD#C`5O>r{j@omDUpX3X*ou+`AW@SJ z%H>|HjaRix@K@YEZvsJE=sMkasxfZ&NOsS%eGAlTEnRplUfv{?H(lU9GJRmWcpzpS zjOhlGl~mCKcOt0y9q;A(i|HjT-Cx8m>G~`X$zfGw8wSi6D^LH+V$}^%IJQtienwCw z{x8@!>C-eAG1p`5JZzaIF7oUc#?vQ?B0WQ$m;tsLBW%SgY$b-FUCK&e$gq+4V@%EP z&OwOE99aRxaeOs_3H-yokAXLW_zXnWXymdp&4=k}jatKO!}nzZr2cCxcpkv_(lNgG z{CBONxfQD=9w0|=z0bJRMAgUFKDvrP9LFVa!ym?D^O3K7P8;96aOFSo%mUmXK`;pj-6+lM+6Z} z{_em;Bz3v~2M|XlU{^jJC8rSJ0Oa<=QH0_*)#NG2+Kl7KDT<#V1U7$V-!1Eh2L}gy z1HGfYiYAD%LAE?P*gN1Ge(WJ%-xH&QBeF4iP?!*vlY7}bGB`ZodvtK*;r+uSgJ^13 z7GJWD!Ofb8^GxOj(KkWF*(x3d;7RT9aZjQwo+wphCKx%eMvxFmG+Xm~qy|~*KZYVA zHvy>YrM2*SLFGGz@r~Wm#_nr955?TW%LV(<(%F%4l_{EMtK@23cD0}6(D>T{DygmL zE#|EGj9IeQCdxO)%Uh)KmUwxmRNk2YQ@CT3K#(`jw{viz7eT!GEF ztwFLioE!aHTk9>Ja+SR`_)hh^p0_>kkA7JFQT+$?Qse!xs{5s~9UpVc#XDoRoi|%R z60K^msrVUpk~*mVPQ(LWeSG6U4gHs;15W0@^s<0n($E7{>?NkJ5F%Hc+(4!FN~sR; zv9u_HT&SuLVO5qER=}E&xHe>+XQz!vK$#Y18T!0pEzN}aOl%-{UR$TEJYwqJ<~k@V z5KW7)e7bJ7L(e+3qO!6ksA83PJ>(HYFZD*#LM5NWkDC7&m^+}>JSCuh4Q)%S3LzKJ zrTq*t%~4BA?;_H^0J#GHZvO?c{rehJfMWRRQQX}7ta>Kvr1LyI?6E3md1S+=M?WOR zK?u!zV2Id1JSaX7sB+9#g;+bju>qAB1vey+Gh;JjQ-at^GN55k)(68O@DB}5jFCe? znfH^kHIIhq*)kIfABo@`OL+l_j{!bgx2`x)WSvhrpZgkUBcB3DsSm~KNtRJxbCUv) z=0cBT^SnRu%O9Tqp=9d-K0m$x)c&}mS#mUg*!EHP2i=lm`!{%s)hShUl2a^|eYZ-s zt=DX=i9#qFO^30qa7rnw z(`$f%$r+DeBPh~Rdo4_!Ez3Vcl*#pfF-Ic+GA}^z7S}6>iRcBEu-N04D#=n6w|FFr zXW6nPuG{jJZp)nr3YFt+_tSq(Z)bna-=C}W;rC;|t+)Xwj*9uDD5@(Xpk@J)hbG74 z=ardGPS0~@`d*+}&m4F65&SAp6JrRT{}wGc&X%1igC#7ct0me(&iJO=id{$i(%`u^ zWGOLel{7<>^&RJ#ti$~(AQ688AZtWq(?rDQSG~e#tMU&`Hq9L@0M#z=;4@G*2RUw5 z`A@QT#&n%`e--vV8G(NQD^q7|h5IuwTnz#4-7ba`fb z)|M>K`h|T5WtXn!KbKG04rChzHAm?P_!G@BBa}xoEE}Qg&^CfT_a2>y`z7&J1jMU? z@C#udGBz#nLbkxk47hxPrvkWCj)y%dW8a_RyMi^|p+Z0|j1W!Jm#t~$8USVEL4PFW zL>gGn5ST>*TU;sRWlCAB+L(8^4un6nR zaQ1Bc%GwpH>5A#P?w&J$Tuu;AVXv>9{aP5F32>GHKOyw2U+a0&lzI(Qjf$Y7Fmd4c zUNioW+j>tn9}`i#_Dl58RAptItz7q86astxuR$*miP-(`9t&dpf$v|L1CY(Xz+?EO zYl3PrL4E3Ha3LcO>TEe-BF1r6>;6^CbcKRP`3xILWA zfIqe3;6$_>7H@4RE={jPQ?o_6%p#Y~maNlf@DA=VQITHA2K-RqI5~Ry$x}}*7gWvj zH>^eTlMANB(RY9J_K)JV9a3#atfmum|E`KeMOD0_MXG3tmA9g|M|+ZC^oG0p6*pZ( zF8&IT^`w2i9zI5AZK?Chx%3!d|I?&jH82+F*QbNZ)`Wc4Y{jVH38y7m+ zpiq->!VOadd&QmQUM>oU#e2DzEAQbx85QwDR$$bk4X2+z^>n?>ti}j>o zer}<4VSK^6D4whN<(BhX;`KdJeNW7Tu1xpk4B>?;9K_Cb1`!$HW%#Y2`+$0)G(yf0 zi84W)^X?KoaJR@0`)&ch%AJ4qWhge`cNw90MO(HO&KtiYQvNu@(XtMF*L+dD7p6yy z_|nOG;_HUj&Bj>Bs0Qp;(%1(9Q5&1j_g!6uf4>Gui>TJA3)OkR#ea_gOIWiM!L;xT zN6AHjb);09pdLRNcWsKr0FUKgKlJLMKMlrprC;evQ!1aMf_{@OV^8xHiaH7!TLa`7 zhCK>CMOe;JW(*H|8kC7xfvJ@Ujv?qlfQBv6i~x7sGB-XIhRYWBV*>Gsa3J;sg8c{{ zMu7V&@g)R)1jth25dbnX0mc;6JH$s2pyU*15TLe^v9c&uT--5{-jms9!r>`#FUEh0 zU;)9uMer5^!ar{#_U{m!LokW}$5Z?Tf*J(12_*jEawlUl@dRB82M$C>6NzK3<%U|BIDR@25gFKkLu@VIzz z1s}-}ZQ!~tbR{WxT#T;ZBk9s}1M~$gc*Mc;;=l@df)(bz2kDy($2~+R;L^qur=CbE z5l_p7+Vho4xm6GC+lBQyKQ><#qz6Z_ZYNY--*l5H<#s`Vm5bb|7m_4`g+^YVd39!? zW+8HR_ROqQQXjL{OZqJ<91^een7Eo1H_J7E?;clo&Iy8ntGfUn1dk-oaa{|66$&0n zi;3%AfXd*JETbXafizu7p60eKv?IO^=?y2xklv8A@SI_xdWC|=ij(FX(8q0EiN+TA zyQ$~7vK#i2h4w|WRN5rjo0A-*!00&279Nrc>yuiH>e5l9Cu4NW2$(VhaPY_Amkz2T zyn+)x`>vz_R$l?`N(R_*KET0UGcLdgaMw?fxCVIEj;xzEO~M0 z!&YIsoOAZH0)#&5%BBnLqe{rV9Y$_$rPTnnBoiwtB5bm+>XeTuB1fP?ftjb1Xx_v9?re~fi1zCciPcqjN)?PNi4OgD)0g#ry9>6>g zCX1Zw0dUH;11gh?RKvJxj`!jD7Llj0W&1Ud5YKmU7%@EdQsg-izj#ERv-~}d2{^3c z4*}#^{@}3qH^{OA_d;!=r1Fiu z;HzVH-%e^E<#r6-gR`%jl#>BqP&d2(5$rMOV9e{Cw{#R7E~ASvZLUEVb=7btf#D34 zrJ5V|B5)PvRP3LU;7ksV!v6`r=D4-Lg#M|!&qDo*?ychgiUI7`<-OaOr9!s1j$JCI zduwz{E(Y;ZEv8q|K(bUrV@e&3DVvO#(n|MsuuE+m;@vcqy0ne&GisL}p!+nYOOzJy zOIj@ck^$ddG7{XP?=NO96&w2P%%zQvevZ4;LHBj*E_HHVYaQE?A60DRs)f=3o75ym3yM6{qa z8(P9tvm5WhC5pXp7qZAKl&osTug|?Umnf~AA2_j(a7WXJ{6+R}%)c?iAq`A?a}Ung z9+?x*btXiIPfmOjJI_|A^o<1Z%WGT(!y4-G=?u z2C-MDq=wKqi| z1Y?$X6_M-G%7r1`r`!c9MnVJV?8^T*5fM@Gk@aAN+ehv>DN_F+Br1LtrlFRW5ojPe zG?lgRUsVi3KPWbEGAyL0b9?`e?mp$ek=P-kFozMKBIf|Wrjn-rjxzooWdZm#Rk^Cs z)0$NZ0O+j%oD^;Rd#ZU=!(j@5RTD*Tm8jLIzZ5K&YKKt)4XW9WnpRSLe1HRGYz*WKyO(d ZWa+Y4S^F&tk=30#7v1w)3IG}R{|{%*%OU^( literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/http2.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/http2.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..43bf257a3cc46b7ba1ec50f5871d358c1e0e59f5 GIT binary patch literal 31868 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/http_proxy.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/http_proxy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc0ba44e6bf4bb465dac99e339a325cc4d04409d GIT binary patch literal 18145 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 WgCKl#Iy5ef-re)uKMCCVzWxnPu$%S( literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/socks_proxy.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/__pycache__/socks_proxy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8c2ecb4bc5cf10dcd321fb0b2025938d20b5cf47 GIT binary patch literal 16894 zcmdseTW}j!c3?N&Z;&8C0^qCpCP<3l%cQIqEs@m2q9Bb(jc4f0z!17Ag2IE-4N4*n z+45s|}~Q&Yh9$Tix>RIUFYQz_fZ z$L=||(Etd7_9m5&WLx6t)AxDqx#!+{&bbG_v|3FRJe>nA;ZL8SsQ-x{%45kPo7ZTH zxSwqw~Yb4*gh$(8G zHAgM87V@o+SfjRCThu;lk5YjB2Ze}eJPqc2fj^NfveY9b=f#9}CW3*|uiKbK(M=d<1465*E?eD_4@R@C9E7(fb z!B(-=Z)s**SSMS<*0L_v&3f26ww`TZ8{aa{wz4$a^cKxBoRMwj+SnGZoo(e*3sZ6{ z#Sl{c#osc{wQ+58DmQ%KAKS*#b0)U^Ro!ftiaJ8E9j{VsCs+5bS}r~7C8=GI>gAg9 zsol%8xBC+;;`h>$PKbxj3r~Eaf^YaVA6!br4+TT#xfttJNmU1haGDbq<1vAgtXym^&WE_b+2sT$NM_}8iH`tvRV+6bT#6(Df)ELW z;;{sGDFMkwLeLZp1U|Ggd@#g)f-{3PNLGQgQ}H-9PtT#Vy`5oY@2`0;5*;P?w1&&&a*mw0ZaxiAnW7Edt2NF;udW0|wd z48IiN1TL{sm77UsDF(9^n#T!RsW=)J5?fLnMJ@5%+>!u&n`dUG81B+yn1>JNcsv>n z##qviu(Y@s=M#|ZImog+CkRYpd6C? z;8J29gp^2`q?rd&B@K4Rw?ZS5j^`3fd@Ph(hZIV_9{ygz9%4F*I!@8B^CXmfY60cE zn75}FQL5~oMTL^ubF{MO zC~umAmh44^a%g^3x+JwwxjjFrQ(AZhcI4-QbKg_lQ2%SK7B~hqsGxDS)w!$( zDxBNvB=t&-%Z5a)lAkmbaSY|6cY>Sa1d6L@mqaaI16ZJ~QLX(0O z-bpR1R$9Ak{ED(=`zfk#N;E4ivznyw`xL89ngHtlM5~N)Cu)nvTTZIVNXYpv7~Nlk zM9Mr+{|@lvviY**E9Is|p|S8znv)iV3ks$G;^n5TFg^uS#+a9z4kf4X7R*Jnn3qnN zxAN;TUvUBEqx4zmDNUiMqM7d9p1aVuJuOKmRdXtZ_Wy6@r+@oMl`-b$XK;I&BK{3; z&s`YVo<{ZoUt+hCQ+6LPER^YsH7FEawiZc+F=-{MTCL2*%k(leN?li7d!4{b0O@PX z1fCtGuKfl4eeciJ7iq7_AEqjZ;$e&iFXfaN{Tcsp|H)_kE2f;j1BdcTJu}D0qs$^I zN4b-{mpQwXV9vqO{h01saUY9a2u8vz1II5P3r0ELVo(r34`U@AG>GjnM73h3Wj!Di zUy8^la!A(1_AuTC9#2H6f+!466pnQz1+=~S6I^0G&Po;jlL6Fc0(mVaue}5m?M2cE zpchImxw%4RK~dx^Y%mcFB;o;5PJ59LCpbw1x&|v*crL_+FK_`YBALQMI3^^5u@EQO z3(cM879-1&E-I5`A@t-_BuRaaOYlY+GOpY<9a(q#UsM{$U+tv%HP6>ikEtkq^ zGD=+w&PpIC>oJmbOBqQ&e$wxs9G{WQa=J{diASPA9cA-FAsOSCV4OY!%HHGzD23+J zlc%P>85ln~F*)tE@wk2Q?Fev_lGPav&YWaMohOjvfi1q2I2RAc&gF#37LJAD(ZaU@ z6fRkHHuADI&9M@omc#ac%in8nr1kr-< z5uG_eq`hx(S*i_$V_{I^iS2=!e9m&<`&W1vSDt! zGLbb=mWr$U9;qp#J#AaN?QSCwZ`urd+FG< zxwLJUXxp{+{91Iww(p*1f5z!fJ3B;Y$A+`(ia%qjc)j-Hm^#v$j+CY&tE1{$ z-roD>-u3F$y&H8SY3B$Ovsb0Nc^u?dG%V-i_*gSB-^~Z~Q3mgTQ)tqxvaGwYf4j*Q$Hf zoN67rZM^mT2hAzV)TUZxH)Sc6(Udh%h6-%;t&X&3K=cfxJ>#Nh{3HGN1AA4<-jp&l z0n9Wsiw4hC!}YF=!ScHCHRB(4q#YxoV`Tl(ZP$k5>3fFb8Fxe4-7UJiH{88fZ5dnj z>yg(Ys|Pl0-DyL2%FqqtW;)(}{>|stXI7uzXxfu@?}1`fwdtxhv8wInw>PSWuNv;# z-K$+cJe+PjEVdoKXFrlM97)-aWbCzRd(%huru&ZiRKxBK$CD|`lhB?#1@{fMw4qfr zw8C_WhW`67sTM}GFlkGdXz5xTUawy3h4C6qkJK8i4rZ>#^G`TIgCa9{&pedU45iFN zFhOa(N7Q@XnoBnhi;ctS#sgyGfsgbD?mL+qfj0s-zmaa=Ew=Bz)wSUqziNWXN!9JW z_4KWMx4)BeAKS1!oiaR~)l!DB>@-bjJHLQI){K6UWhi40;-0-D=B%t`hmsYfq?J|8)m)|KXx8?s zb+(eVa}L(ZRn2MGidQwW)mN!Y^sIB5@;W3taY2w1hMey!hfKIlA7l#KB(n%^3??xj zOn~YTV+AI+^$W~}aF7Yc81!t&xHSo)pQBI!mD!L2_fl zd5&8IMFdXn;;f27GVuy5OIqCX2jthFqzB8K4}%NG9+SOCfwQt#iSGpI2o;;YC>W@w zY<>vfk7zL0;VnO}*^|`oX(=kHPHK|cq%NsX8j_}@@jd$8oXG@cj5J32a7r4g)3JrC@Y1!ZFXa;6(26`343@`}*PEz@FXx zyZe1D&uy(&BsqJSgE3}$dJ6kRpPWJq4iH>a;kjiTW0Cl5Z3(G3HYYob7P(M(F3hpL zgzB8Xk9khfC;G~G0K2@;ZQ%_zcQG7^FlRaB2)nhj5*e5I2Z`b6Qk01;MbCmuEk1|D zhL(7qizVc_DI%UMylp)aCgZXc=7}Os+;4rrm}MrINN~|b!WCSuOi)>`Ogsi3`Nh1g z&5NWZ?h^X{px3a#1f%gKAW0rx1k@*R+$F*hXr(|PgXI+e>-NDH7H*-6uoD}?EJy^| zO^OO{qm%4J3d^Ixa4Z3TKyo-H%O0?0D;)(IGVOiLgzV9VfyS8`eu>*o%HUU!GWey0fctkFEni8< z;b254p(MAR?<6O=YVd=tSe zf@cx@4uWqZ_!fdQ0Pto2eIkDT>-eRYw6O9xUNQt^!!jUQitmVc^p8s>+5SWwh)3&J zGUpOd6Y^G|b0acGnkTJ(1wn)sLe)uVi$8@8+7x43G1e86JNIo21VRz;Rs;e9c!?-D zr;48|{MWFhvjA4)d#HjE*ryCE3c14X0zlJvmZprh*Yu*nm2tIZ>U%OBW0{6s8AmPt zhtKM|Y$auMKcb9aK~_@@T~{^t4DS1$_M74L^Y=W5u4=B?GiGPX+?;9d5iN~Zk6xd@ z-_X6b`&RG0h9g(~#rT7Y#!N+HR;xZi+pbRh%2E#|8q-FwO&|yEV&ddhFO*lsATh7n zeE=WD_b_n7P+SfhfT!Gz3wTGVaV0*dAp z>b|`V90+eWz1fs@`$V@d<6$zjb?Mqpv9>egs!zMTqRR_jh;+k{*f5lF)ZFNKqepqw zGwJ#Pv3}rDrLh9M8X%MW0<`Ki`u(5ar1||bhk2yd@2$a=j0-#vfMpwq##y+*1l$q` zd}k>b$>rz+0X80jWF1^M!qy^bVMmL{cx+74$~VYrxH^;!Cl^t#2u9#`cnNe|Ne#-Y zWCD$KTvk{mHRvpob}kYR!u=wi`#fs)WG(V&1@JWpmat)KZV`vLNF)&O(tH?F1-xN= zN?yP-SQNoLX7obN=dV%^RVu9^>!7TanbEzO{=v+i1DT=GOpPZ~QJZv;HQxHhptJ zbPueL{B++>_TARr7H*rwq2np{anUmMP_1&BvU*b8LefF;Z=Y;b9go2HGtR7saPlAt{h$ixEta@+&ucI{5+m6d<+4etMUn0 zW(SjPc=W|94=!6Z{B8aMzzqsF?SPU;CDBsG1wx*!_Bz=0Q~+zrd0O}?*y(U}9RTY; zm#2rXyuF z$oBpJI2!^^L|&07QX(4Vt-a)w$*U6o8L9~p09UAN4b|9<%7G`-*n{u7OkF#^U77k$ zqBLZh2k_kp+ZAXH=G+T*rN&*UhT&_+ubsH%%Yskdde6}C2vR%0|16gGWDY+qx6{;i zRr4!DJz3u99U_rGTa*QywO8P8(*tf3IdCIMFF-=+vyhjULJmuZsr+T0Dyb@4213p9 zWx#7xPTd%$UIEwWws45N+!cDfuDTL2VyHx3+6sB)wu1$0Az#5DxFEMKc4()Zf&$yy zTk6Z21j-e8SZyNjh=rA>c{jI$o`5hD6;fEnA-jMFTFJvY!VpaIL;CmWd`O5^4hgxe z1K*prusR@Q5B@2^_zzH{MM*2XVK-}6zLdQxW-ebAf^DdU4yAPA&1+aAYdWWj zsaWGV`m!OZU%w+Oo@X^s*?6HhWHlk6Zu&w4m2y*&$!Z3;hoF}zUB9( zTr1OZXvVKdXvWuDsR2;NFREzjn-mTIAZX*{CG|I{i!=$>_+(o?eeHh~;XPrpZY!cj`QwRQU1F1dCqmDr!GxdYk!b0eUSv2RQV-+A{^x)GC~a8V0E05oBI8MiMsWS`i9jME zNG7}@h@V}6s9+uyG4R?{6O{xV%7f$iTYqTqJY>tn%>p*_4&`l;OmLIH#RM?i1b#2( z|1N@S2wp>g`=z8~!BNa|au5Zcf|4#0KX;DfW!>u!Fa<(SFiJE4@ia>u|8KA^hu}0R zE`;VeAU=dC6$ns$lN`X5KrpfxL-j8a55*$_zls?)2tx?~Wqc4^O>=Rn2E0ZvK#tv7 zTGj-K>*|lO(M1FpASoFq!Jkj;QU3Sv+Y%N6d3QaJ%7j!+oMoIGI{^wO(UrVbSu6Z~ zEMdqAfFLtncm86KKPO09AS=Ylj3jepD;dI@Sh)gca}b;c5Ni|$cL2*HiIN&F8U$2Q z^1374fD9!8^ncZn- zOk~Dx>o=IAB{}4?Uu60p8nvW<3-q6H4gA%}tV;Z z0DNg46q^Sj{Kee{1P_h=wC68-R%h3$#rnS6Pv71LI=Z_PD!bd#t`X5Svi`z7*J1eX z9nJWL(!N8Y?@-!zMD!g=`~0HM|G~u#-!qxcK44;tXH^GGZ1b+w{ngy9bGI*SwE91z zRPNotN%!v5>1Q%Md)G$NJ$ps`KY42oK)QW$^>AijbanDxeIGExkGol7Z>jHIUvlF76tCq}TL%RyDtJbv?3By$9(p zD|FA*^?9~}1e~;s&i1r(P;?I7a}M9H>D{R5OI7rJ_KXVUQI^W=tg5K!9?`ky)+mID zr0wG=!+7>tQt}J%wzf>uUwj50nBj*gX^+A0;eHBI^f09lapE6m`+y%`WZEac$f^+| ztRdi2uFMTb5->IbiM$H|E7I8NXUEitKPIVaXG#jAS z9WM=i-s#m&jTr9?7%^oZ4ISJWqXECG(w%S`?;7Y6X4_q(9`OpSb=PG);nUu2HlFC# z-tE>R?o&hWcL!hhPYo|f%hi*Z*t}{RgUVHT0yj#Dh>wJ~qtuw7E_+*QLyz;6zS4 zT0}=n+Tj%)-VH}j+5%S=J+Plyx-yoY#{#-;UhaN^evcl~{J9=NprEb%lb_HGB*BE1 zWx$n5NXL>T57-2wU=x(}7nIYJSsDf75-nIsEj-do9t-61a;~W)3qCC%ROKlwXM-tI zL@p-yKZ5#Ry{sjnA|uP?O~icx9L9>`FU*qSLp&TXgRI$mzd5{Qlmb?Y;Hlr)ntk051%BMOSaC zx;LxWSZ&ZmMKy#F!daiTqnvNp`%;F!?V_Ha%{d@r$Jw-$pUoY4tA)l&o&s-W5m(EM zC3>5AJ}7`9NPZEujhTN94JlVErT({~dSl2-v^2N!lEhnfidCH{OJ`pFN!xowd(Vb_ zSIV&KpD9c9ASW28v5g%+MRqjoC|g)(;vd9{PEZCn?B0~Y`_GgWMqmZm2ia}R`1i<+ zrX86f*E;qOVux3>dpGPoDMQbfumjJ99T!VQ`NhIRjOaA{KKCEDXuN9wN@Xr+9)BQt z_~5ZAh=eyON&X{X`VoVUtSAi{RNL6~ACX<29oPjG{K%}j zv|K`3mJyf{JdfZ-1XmDTM(`4Xml3>*;9ns?ua10sipMgKRwa)%B>!Uss0hf15bl#a z?hZVz3jPHIWcYYF!G9kCvR*bqnPw*$S!#H21;LI4OkI;Cjn!iqkyVNR|w*5g( zeO3!^ux?z9Sv_Kg+$HMG!#|q(!PL6*mUg{fY&#%Uk7bRR4^ihOx1?rFwrsilv|^GC zFFfstRUlT0m}Bd8C(;0RfXTM$L~QtLw044C@61vJxi!BjBiT6>t=+W-)*!srCpPi5 zsZ(itvj&sax5{q9t*9^SQfvEGXErH#Wld^r_i8ob-H7+D4kO-+c<1#H;+qKk@FK$XSU1J~r%=BM}bpccBQ#A0<2jkX6$({cFnhYs&O%%KBT% z^INLxH`K&$sC~boPNk_+A5$G4Q(YfZo{y>ik0~#}2S(4;+O*Ll8a=7Ho(*H~6&=W? ztvh3NW~@xc+6GR83e^=02$#*BvG!!F{TXXZ#@dvzHfO9JD3GywA6A*{sw?Jf8};P= zPtE)2<8;Q-m{lVJB5$t9>IrI~%(kqNpeD*(`MU2l-)hI(y>Is3Tw3F9CPmM%XnG<` p>Vu%`Oq*6L`{)IFnx;*u8t)?tkw+2L*H!d^RQ>SZQHT(s`oEkWAyxnY literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_async/connection.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/connection.py new file mode 100644 index 00000000..9014ab95 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/connection.py @@ -0,0 +1,215 @@ +import itertools +import logging +import ssl +from types import TracebackType +from typing import Iterable, Iterator, Optional, Type + +from .._backends.auto import AutoBackend +from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream +from .._exceptions import ConnectError, ConnectionNotAvailable, ConnectTimeout +from .._models import Origin, Request, Response +from .._ssl import default_ssl_context +from .._synchronization import AsyncLock +from .._trace import Trace +from .http11 import AsyncHTTP11Connection +from .interfaces import AsyncConnectionInterface + +RETRIES_BACKOFF_FACTOR = 0.5 # 0s, 0.5s, 1s, 2s, 4s, etc. + + +logger = logging.getLogger("httpcore.connection") + + +def exponential_backoff(factor: float) -> Iterator[float]: + yield 0 + for n in itertools.count(2): + yield factor * (2 ** (n - 2)) + + +class AsyncHTTPConnection(AsyncConnectionInterface): + def __init__( + self, + origin: Origin, + ssl_context: Optional[ssl.SSLContext] = None, + keepalive_expiry: Optional[float] = None, + http1: bool = True, + http2: bool = False, + retries: int = 0, + local_address: Optional[str] = None, + uds: Optional[str] = None, + network_backend: Optional[AsyncNetworkBackend] = None, + socket_options: Optional[Iterable[SOCKET_OPTION]] = None, + ) -> None: + self._origin = origin + self._ssl_context = ssl_context + self._keepalive_expiry = keepalive_expiry + self._http1 = http1 + self._http2 = http2 + self._retries = retries + self._local_address = local_address + self._uds = uds + + self._network_backend: AsyncNetworkBackend = ( + AutoBackend() if network_backend is None else network_backend + ) + self._connection: Optional[AsyncConnectionInterface] = None + self._connect_failed: bool = False + self._request_lock = AsyncLock() + self._socket_options = socket_options + + async def handle_async_request(self, request: Request) -> Response: + if not self.can_handle_request(request.url.origin): + raise RuntimeError( + f"Attempted to send request to {request.url.origin} on connection to {self._origin}" + ) + + async with self._request_lock: + if self._connection is None: + try: + stream = await self._connect(request) + + ssl_object = stream.get_extra_info("ssl_object") + http2_negotiated = ( + ssl_object is not None + and ssl_object.selected_alpn_protocol() == "h2" + ) + if http2_negotiated or (self._http2 and not self._http1): + from .http2 import AsyncHTTP2Connection + + self._connection = AsyncHTTP2Connection( + origin=self._origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + else: + self._connection = AsyncHTTP11Connection( + origin=self._origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + except Exception as exc: + self._connect_failed = True + raise exc + elif not self._connection.is_available(): + raise ConnectionNotAvailable() + + return await self._connection.handle_async_request(request) + + async def _connect(self, request: Request) -> AsyncNetworkStream: + timeouts = request.extensions.get("timeout", {}) + sni_hostname = request.extensions.get("sni_hostname", None) + timeout = timeouts.get("connect", None) + + retries_left = self._retries + delays = exponential_backoff(factor=RETRIES_BACKOFF_FACTOR) + + while True: + try: + if self._uds is None: + kwargs = { + "host": self._origin.host.decode("ascii"), + "port": self._origin.port, + "local_address": self._local_address, + "timeout": timeout, + "socket_options": self._socket_options, + } + async with Trace("connect_tcp", logger, request, kwargs) as trace: + stream = await self._network_backend.connect_tcp(**kwargs) + trace.return_value = stream + else: + kwargs = { + "path": self._uds, + "timeout": timeout, + "socket_options": self._socket_options, + } + async with Trace( + "connect_unix_socket", logger, request, kwargs + ) as trace: + stream = await self._network_backend.connect_unix_socket( + **kwargs + ) + trace.return_value = stream + + if self._origin.scheme == b"https": + ssl_context = ( + default_ssl_context() + if self._ssl_context is None + else self._ssl_context + ) + alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"] + ssl_context.set_alpn_protocols(alpn_protocols) + + kwargs = { + "ssl_context": ssl_context, + "server_hostname": sni_hostname + or self._origin.host.decode("ascii"), + "timeout": timeout, + } + async with Trace("start_tls", logger, request, kwargs) as trace: + stream = await stream.start_tls(**kwargs) + trace.return_value = stream + return stream + except (ConnectError, ConnectTimeout): + if retries_left <= 0: + raise + retries_left -= 1 + delay = next(delays) + async with Trace("retry", logger, request, kwargs) as trace: + await self._network_backend.sleep(delay) + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._origin + + async def aclose(self) -> None: + if self._connection is not None: + async with Trace("close", logger, None, {}): + await self._connection.aclose() + + def is_available(self) -> bool: + if self._connection is None: + # If HTTP/2 support is enabled, and the resulting connection could + # end up as HTTP/2 then we should indicate the connection as being + # available to service multiple requests. + return ( + self._http2 + and (self._origin.scheme == b"https" or not self._http1) + and not self._connect_failed + ) + return self._connection.is_available() + + def has_expired(self) -> bool: + if self._connection is None: + return self._connect_failed + return self._connection.has_expired() + + def is_idle(self) -> bool: + if self._connection is None: + return self._connect_failed + return self._connection.is_idle() + + def is_closed(self) -> bool: + if self._connection is None: + return self._connect_failed + return self._connection.is_closed() + + def info(self) -> str: + if self._connection is None: + return "CONNECTION FAILED" if self._connect_failed else "CONNECTING" + return self._connection.info() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.info()}]>" + + # These context managers are not used in the standard flow, but are + # useful for testing or working with connection instances directly. + + async def __aenter__(self) -> "AsyncHTTPConnection": + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + await self.aclose() diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_async/connection_pool.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/connection_pool.py new file mode 100644 index 00000000..ddc0510e --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/connection_pool.py @@ -0,0 +1,356 @@ +import ssl +import sys +from types import TracebackType +from typing import AsyncIterable, AsyncIterator, Iterable, List, Optional, Type + +from .._backends.auto import AutoBackend +from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend +from .._exceptions import ConnectionNotAvailable, UnsupportedProtocol +from .._models import Origin, Request, Response +from .._synchronization import AsyncEvent, AsyncLock, AsyncShieldCancellation +from .connection import AsyncHTTPConnection +from .interfaces import AsyncConnectionInterface, AsyncRequestInterface + + +class RequestStatus: + def __init__(self, request: Request): + self.request = request + self.connection: Optional[AsyncConnectionInterface] = None + self._connection_acquired = AsyncEvent() + + def set_connection(self, connection: AsyncConnectionInterface) -> None: + assert self.connection is None + self.connection = connection + self._connection_acquired.set() + + def unset_connection(self) -> None: + assert self.connection is not None + self.connection = None + self._connection_acquired = AsyncEvent() + + async def wait_for_connection( + self, timeout: Optional[float] = None + ) -> AsyncConnectionInterface: + if self.connection is None: + await self._connection_acquired.wait(timeout=timeout) + assert self.connection is not None + return self.connection + + +class AsyncConnectionPool(AsyncRequestInterface): + """ + A connection pool for making HTTP requests. + """ + + def __init__( + self, + ssl_context: Optional[ssl.SSLContext] = None, + max_connections: Optional[int] = 10, + max_keepalive_connections: Optional[int] = None, + keepalive_expiry: Optional[float] = None, + http1: bool = True, + http2: bool = False, + retries: int = 0, + local_address: Optional[str] = None, + uds: Optional[str] = None, + network_backend: Optional[AsyncNetworkBackend] = None, + socket_options: Optional[Iterable[SOCKET_OPTION]] = None, + ) -> None: + """ + A connection pool for making HTTP requests. + + Parameters: + ssl_context: An SSL context to use for verifying connections. + If not specified, the default `httpcore.default_ssl_context()` + will be used. + max_connections: The maximum number of concurrent HTTP connections that + the pool should allow. Any attempt to send a request on a pool that + would exceed this amount will block until a connection is available. + max_keepalive_connections: The maximum number of idle HTTP connections + that will be maintained in the pool. + keepalive_expiry: The duration in seconds that an idle HTTP connection + may be maintained for before being expired from the pool. + http1: A boolean indicating if HTTP/1.1 requests should be supported + by the connection pool. Defaults to True. + http2: A boolean indicating if HTTP/2 requests should be supported by + the connection pool. Defaults to False. + retries: The maximum number of retries when trying to establish a + connection. + local_address: Local address to connect from. Can also be used to connect + using a particular address family. Using `local_address="0.0.0.0"` + will connect using an `AF_INET` address (IPv4), while using + `local_address="::"` will connect using an `AF_INET6` address (IPv6). + uds: Path to a Unix Domain Socket to use instead of TCP sockets. + network_backend: A backend instance to use for handling network I/O. + socket_options: Socket options that have to be included + in the TCP socket when the connection was established. + """ + self._ssl_context = ssl_context + + self._max_connections = ( + sys.maxsize if max_connections is None else max_connections + ) + self._max_keepalive_connections = ( + sys.maxsize + if max_keepalive_connections is None + else max_keepalive_connections + ) + self._max_keepalive_connections = min( + self._max_connections, self._max_keepalive_connections + ) + + self._keepalive_expiry = keepalive_expiry + self._http1 = http1 + self._http2 = http2 + self._retries = retries + self._local_address = local_address + self._uds = uds + + self._pool: List[AsyncConnectionInterface] = [] + self._requests: List[RequestStatus] = [] + self._pool_lock = AsyncLock() + self._network_backend = ( + AutoBackend() if network_backend is None else network_backend + ) + self._socket_options = socket_options + + def create_connection(self, origin: Origin) -> AsyncConnectionInterface: + return AsyncHTTPConnection( + origin=origin, + ssl_context=self._ssl_context, + keepalive_expiry=self._keepalive_expiry, + http1=self._http1, + http2=self._http2, + retries=self._retries, + local_address=self._local_address, + uds=self._uds, + network_backend=self._network_backend, + socket_options=self._socket_options, + ) + + @property + def connections(self) -> List[AsyncConnectionInterface]: + """ + Return a list of the connections currently in the pool. + + For example: + + ```python + >>> pool.connections + [ + , + , + , + ] + ``` + """ + return list(self._pool) + + async def _attempt_to_acquire_connection(self, status: RequestStatus) -> bool: + """ + Attempt to provide a connection that can handle the given origin. + """ + origin = status.request.url.origin + + # If there are queued requests in front of us, then don't acquire a + # connection. We handle requests strictly in order. + waiting = [s for s in self._requests if s.connection is None] + if waiting and waiting[0] is not status: + return False + + # Reuse an existing connection if one is currently available. + for idx, connection in enumerate(self._pool): + if connection.can_handle_request(origin) and connection.is_available(): + self._pool.pop(idx) + self._pool.insert(0, connection) + status.set_connection(connection) + return True + + # If the pool is currently full, attempt to close one idle connection. + if len(self._pool) >= self._max_connections: + for idx, connection in reversed(list(enumerate(self._pool))): + if connection.is_idle(): + await connection.aclose() + self._pool.pop(idx) + break + + # If the pool is still full, then we cannot acquire a connection. + if len(self._pool) >= self._max_connections: + return False + + # Otherwise create a new connection. + connection = self.create_connection(origin) + self._pool.insert(0, connection) + status.set_connection(connection) + return True + + async def _close_expired_connections(self) -> None: + """ + Clean up the connection pool by closing off any connections that have expired. + """ + # Close any connections that have expired their keep-alive time. + for idx, connection in reversed(list(enumerate(self._pool))): + if connection.has_expired(): + await connection.aclose() + self._pool.pop(idx) + + # If the pool size exceeds the maximum number of allowed keep-alive connections, + # then close off idle connections as required. + pool_size = len(self._pool) + for idx, connection in reversed(list(enumerate(self._pool))): + if connection.is_idle() and pool_size > self._max_keepalive_connections: + await connection.aclose() + self._pool.pop(idx) + pool_size -= 1 + + async def handle_async_request(self, request: Request) -> Response: + """ + Send an HTTP request, and return an HTTP response. + + This is the core implementation that is called into by `.request()` or `.stream()`. + """ + scheme = request.url.scheme.decode() + if scheme == "": + raise UnsupportedProtocol( + "Request URL is missing an 'http://' or 'https://' protocol." + ) + if scheme not in ("http", "https", "ws", "wss"): + raise UnsupportedProtocol( + f"Request URL has an unsupported protocol '{scheme}://'." + ) + + status = RequestStatus(request) + + async with self._pool_lock: + self._requests.append(status) + await self._close_expired_connections() + await self._attempt_to_acquire_connection(status) + + while True: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("pool", None) + try: + connection = await status.wait_for_connection(timeout=timeout) + except BaseException as exc: + # If we timeout here, or if the task is cancelled, then make + # sure to remove the request from the queue before bubbling + # up the exception. + async with self._pool_lock: + # Ensure only remove when task exists. + if status in self._requests: + self._requests.remove(status) + raise exc + + try: + response = await connection.handle_async_request(request) + except ConnectionNotAvailable: + # The ConnectionNotAvailable exception is a special case, that + # indicates we need to retry the request on a new connection. + # + # The most common case where this can occur is when multiple + # requests are queued waiting for a single connection, which + # might end up as an HTTP/2 connection, but which actually ends + # up as HTTP/1.1. + async with self._pool_lock: + # Maintain our position in the request queue, but reset the + # status so that the request becomes queued again. + status.unset_connection() + await self._attempt_to_acquire_connection(status) + except BaseException as exc: + with AsyncShieldCancellation(): + await self.response_closed(status) + raise exc + else: + break + + # When we return the response, we wrap the stream in a special class + # that handles notifying the connection pool once the response + # has been released. + assert isinstance(response.stream, AsyncIterable) + return Response( + status=response.status, + headers=response.headers, + content=ConnectionPoolByteStream(response.stream, self, status), + extensions=response.extensions, + ) + + async def response_closed(self, status: RequestStatus) -> None: + """ + This method acts as a callback once the request/response cycle is complete. + + It is called into from the `ConnectionPoolByteStream.aclose()` method. + """ + assert status.connection is not None + connection = status.connection + + async with self._pool_lock: + # Update the state of the connection pool. + if status in self._requests: + self._requests.remove(status) + + if connection.is_closed() and connection in self._pool: + self._pool.remove(connection) + + # Since we've had a response closed, it's possible we'll now be able + # to service one or more requests that are currently pending. + for status in self._requests: + if status.connection is None: + acquired = await self._attempt_to_acquire_connection(status) + # If we could not acquire a connection for a queued request + # then we don't need to check anymore requests that are + # queued later behind it. + if not acquired: + break + + # Housekeeping. + await self._close_expired_connections() + + async def aclose(self) -> None: + """ + Close any connections in the pool. + """ + async with self._pool_lock: + for connection in self._pool: + await connection.aclose() + self._pool = [] + self._requests = [] + + async def __aenter__(self) -> "AsyncConnectionPool": + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + await self.aclose() + + +class ConnectionPoolByteStream: + """ + A wrapper around the response byte stream, that additionally handles + notifying the connection pool when the response has been closed. + """ + + def __init__( + self, + stream: AsyncIterable[bytes], + pool: AsyncConnectionPool, + status: RequestStatus, + ) -> None: + self._stream = stream + self._pool = pool + self._status = status + + async def __aiter__(self) -> AsyncIterator[bytes]: + async for part in self._stream: + yield part + + async def aclose(self) -> None: + try: + if hasattr(self._stream, "aclose"): + await self._stream.aclose() + finally: + with AsyncShieldCancellation(): + await self._pool.response_closed(self._status) diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_async/http11.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/http11.py new file mode 100644 index 00000000..7ad36642 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/http11.py @@ -0,0 +1,331 @@ +import enum +import logging +import time +from types import TracebackType +from typing import ( + AsyncIterable, + AsyncIterator, + List, + Optional, + Tuple, + Type, + Union, + cast, +) + +import h11 + +from .._backends.base import AsyncNetworkStream +from .._exceptions import ( + ConnectionNotAvailable, + LocalProtocolError, + RemoteProtocolError, + map_exceptions, +) +from .._models import Origin, Request, Response +from .._synchronization import AsyncLock, AsyncShieldCancellation +from .._trace import Trace +from .interfaces import AsyncConnectionInterface + +logger = logging.getLogger("httpcore.http11") + + +# A subset of `h11.Event` types supported by `_send_event` +H11SendEvent = Union[ + h11.Request, + h11.Data, + h11.EndOfMessage, +] + + +class HTTPConnectionState(enum.IntEnum): + NEW = 0 + ACTIVE = 1 + IDLE = 2 + CLOSED = 3 + + +class AsyncHTTP11Connection(AsyncConnectionInterface): + READ_NUM_BYTES = 64 * 1024 + MAX_INCOMPLETE_EVENT_SIZE = 100 * 1024 + + def __init__( + self, + origin: Origin, + stream: AsyncNetworkStream, + keepalive_expiry: Optional[float] = None, + ) -> None: + self._origin = origin + self._network_stream = stream + self._keepalive_expiry: Optional[float] = keepalive_expiry + self._expire_at: Optional[float] = None + self._state = HTTPConnectionState.NEW + self._state_lock = AsyncLock() + self._request_count = 0 + self._h11_state = h11.Connection( + our_role=h11.CLIENT, + max_incomplete_event_size=self.MAX_INCOMPLETE_EVENT_SIZE, + ) + + async def handle_async_request(self, request: Request) -> Response: + if not self.can_handle_request(request.url.origin): + raise RuntimeError( + f"Attempted to send request to {request.url.origin} on connection " + f"to {self._origin}" + ) + + async with self._state_lock: + if self._state in (HTTPConnectionState.NEW, HTTPConnectionState.IDLE): + self._request_count += 1 + self._state = HTTPConnectionState.ACTIVE + self._expire_at = None + else: + raise ConnectionNotAvailable() + + try: + kwargs = {"request": request} + async with Trace("send_request_headers", logger, request, kwargs) as trace: + await self._send_request_headers(**kwargs) + async with Trace("send_request_body", logger, request, kwargs) as trace: + await self._send_request_body(**kwargs) + async with Trace( + "receive_response_headers", logger, request, kwargs + ) as trace: + ( + http_version, + status, + reason_phrase, + headers, + ) = await self._receive_response_headers(**kwargs) + trace.return_value = ( + http_version, + status, + reason_phrase, + headers, + ) + + return Response( + status=status, + headers=headers, + content=HTTP11ConnectionByteStream(self, request), + extensions={ + "http_version": http_version, + "reason_phrase": reason_phrase, + "network_stream": self._network_stream, + }, + ) + except BaseException as exc: + with AsyncShieldCancellation(): + async with Trace("response_closed", logger, request) as trace: + await self._response_closed() + raise exc + + # Sending the request... + + async def _send_request_headers(self, request: Request) -> None: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("write", None) + + with map_exceptions({h11.LocalProtocolError: LocalProtocolError}): + event = h11.Request( + method=request.method, + target=request.url.target, + headers=request.headers, + ) + await self._send_event(event, timeout=timeout) + + async def _send_request_body(self, request: Request) -> None: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("write", None) + + assert isinstance(request.stream, AsyncIterable) + async for chunk in request.stream: + event = h11.Data(data=chunk) + await self._send_event(event, timeout=timeout) + + await self._send_event(h11.EndOfMessage(), timeout=timeout) + + async def _send_event( + self, event: h11.Event, timeout: Optional[float] = None + ) -> None: + bytes_to_send = self._h11_state.send(event) + if bytes_to_send is not None: + await self._network_stream.write(bytes_to_send, timeout=timeout) + + # Receiving the response... + + async def _receive_response_headers( + self, request: Request + ) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]]]: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("read", None) + + while True: + event = await self._receive_event(timeout=timeout) + if isinstance(event, h11.Response): + break + if ( + isinstance(event, h11.InformationalResponse) + and event.status_code == 101 + ): + break + + http_version = b"HTTP/" + event.http_version + + # h11 version 0.11+ supports a `raw_items` interface to get the + # raw header casing, rather than the enforced lowercase headers. + headers = event.headers.raw_items() + + return http_version, event.status_code, event.reason, headers + + async def _receive_response_body(self, request: Request) -> AsyncIterator[bytes]: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("read", None) + + while True: + event = await self._receive_event(timeout=timeout) + if isinstance(event, h11.Data): + yield bytes(event.data) + elif isinstance(event, (h11.EndOfMessage, h11.PAUSED)): + break + + async def _receive_event( + self, timeout: Optional[float] = None + ) -> Union[h11.Event, Type[h11.PAUSED]]: + while True: + with map_exceptions({h11.RemoteProtocolError: RemoteProtocolError}): + event = self._h11_state.next_event() + + if event is h11.NEED_DATA: + data = await self._network_stream.read( + self.READ_NUM_BYTES, timeout=timeout + ) + + # If we feed this case through h11 we'll raise an exception like: + # + # httpcore.RemoteProtocolError: can't handle event type + # ConnectionClosed when role=SERVER and state=SEND_RESPONSE + # + # Which is accurate, but not very informative from an end-user + # perspective. Instead we handle this case distinctly and treat + # it as a ConnectError. + if data == b"" and self._h11_state.their_state == h11.SEND_RESPONSE: + msg = "Server disconnected without sending a response." + raise RemoteProtocolError(msg) + + self._h11_state.receive_data(data) + else: + # mypy fails to narrow the type in the above if statement above + return cast(Union[h11.Event, Type[h11.PAUSED]], event) + + async def _response_closed(self) -> None: + async with self._state_lock: + if ( + self._h11_state.our_state is h11.DONE + and self._h11_state.their_state is h11.DONE + ): + self._state = HTTPConnectionState.IDLE + self._h11_state.start_next_cycle() + if self._keepalive_expiry is not None: + now = time.monotonic() + self._expire_at = now + self._keepalive_expiry + else: + await self.aclose() + + # Once the connection is no longer required... + + async def aclose(self) -> None: + # Note that this method unilaterally closes the connection, and does + # not have any kind of locking in place around it. + self._state = HTTPConnectionState.CLOSED + await self._network_stream.aclose() + + # The AsyncConnectionInterface methods provide information about the state of + # the connection, allowing for a connection pooling implementation to + # determine when to reuse and when to close the connection... + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._origin + + def is_available(self) -> bool: + # Note that HTTP/1.1 connections in the "NEW" state are not treated as + # being "available". The control flow which created the connection will + # be able to send an outgoing request, but the connection will not be + # acquired from the connection pool for any other request. + return self._state == HTTPConnectionState.IDLE + + def has_expired(self) -> bool: + now = time.monotonic() + keepalive_expired = self._expire_at is not None and now > self._expire_at + + # If the HTTP connection is idle but the socket is readable, then the + # only valid state is that the socket is about to return b"", indicating + # a server-initiated disconnect. + server_disconnected = ( + self._state == HTTPConnectionState.IDLE + and self._network_stream.get_extra_info("is_readable") + ) + + return keepalive_expired or server_disconnected + + def is_idle(self) -> bool: + return self._state == HTTPConnectionState.IDLE + + def is_closed(self) -> bool: + return self._state == HTTPConnectionState.CLOSED + + def info(self) -> str: + origin = str(self._origin) + return ( + f"{origin!r}, HTTP/1.1, {self._state.name}, " + f"Request Count: {self._request_count}" + ) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + origin = str(self._origin) + return ( + f"<{class_name} [{origin!r}, {self._state.name}, " + f"Request Count: {self._request_count}]>" + ) + + # These context managers are not used in the standard flow, but are + # useful for testing or working with connection instances directly. + + async def __aenter__(self) -> "AsyncHTTP11Connection": + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + await self.aclose() + + +class HTTP11ConnectionByteStream: + def __init__(self, connection: AsyncHTTP11Connection, request: Request) -> None: + self._connection = connection + self._request = request + self._closed = False + + async def __aiter__(self) -> AsyncIterator[bytes]: + kwargs = {"request": self._request} + try: + async with Trace("receive_response_body", logger, self._request, kwargs): + async for chunk in self._connection._receive_response_body(**kwargs): + yield chunk + except BaseException as exc: + # If we get an exception while streaming the response, + # we want to close the response (and possibly the connection) + # before raising that exception. + with AsyncShieldCancellation(): + await self.aclose() + raise exc + + async def aclose(self) -> None: + if not self._closed: + self._closed = True + async with Trace("response_closed", logger, self._request): + await self._connection._response_closed() diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_async/http2.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/http2.py new file mode 100644 index 00000000..8dc776ff --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/http2.py @@ -0,0 +1,589 @@ +import enum +import logging +import time +import types +import typing + +import h2.config +import h2.connection +import h2.events +import h2.exceptions +import h2.settings + +from .._backends.base import AsyncNetworkStream +from .._exceptions import ( + ConnectionNotAvailable, + LocalProtocolError, + RemoteProtocolError, +) +from .._models import Origin, Request, Response +from .._synchronization import AsyncLock, AsyncSemaphore, AsyncShieldCancellation +from .._trace import Trace +from .interfaces import AsyncConnectionInterface + +logger = logging.getLogger("httpcore.http2") + + +def has_body_headers(request: Request) -> bool: + return any( + k.lower() == b"content-length" or k.lower() == b"transfer-encoding" + for k, v in request.headers + ) + + +class HTTPConnectionState(enum.IntEnum): + ACTIVE = 1 + IDLE = 2 + CLOSED = 3 + + +class AsyncHTTP2Connection(AsyncConnectionInterface): + READ_NUM_BYTES = 64 * 1024 + CONFIG = h2.config.H2Configuration(validate_inbound_headers=False) + + def __init__( + self, + origin: Origin, + stream: AsyncNetworkStream, + keepalive_expiry: typing.Optional[float] = None, + ): + self._origin = origin + self._network_stream = stream + self._keepalive_expiry: typing.Optional[float] = keepalive_expiry + self._h2_state = h2.connection.H2Connection(config=self.CONFIG) + self._state = HTTPConnectionState.IDLE + self._expire_at: typing.Optional[float] = None + self._request_count = 0 + self._init_lock = AsyncLock() + self._state_lock = AsyncLock() + self._read_lock = AsyncLock() + self._write_lock = AsyncLock() + self._sent_connection_init = False + self._used_all_stream_ids = False + self._connection_error = False + + # Mapping from stream ID to response stream events. + self._events: typing.Dict[ + int, + typing.Union[ + h2.events.ResponseReceived, + h2.events.DataReceived, + h2.events.StreamEnded, + h2.events.StreamReset, + ], + ] = {} + + # Connection terminated events are stored as state since + # we need to handle them for all streams. + self._connection_terminated: typing.Optional[ + h2.events.ConnectionTerminated + ] = None + + self._read_exception: typing.Optional[Exception] = None + self._write_exception: typing.Optional[Exception] = None + + async def handle_async_request(self, request: Request) -> Response: + if not self.can_handle_request(request.url.origin): + # This cannot occur in normal operation, since the connection pool + # will only send requests on connections that handle them. + # It's in place simply for resilience as a guard against incorrect + # usage, for anyone working directly with httpcore connections. + raise RuntimeError( + f"Attempted to send request to {request.url.origin} on connection " + f"to {self._origin}" + ) + + async with self._state_lock: + if self._state in (HTTPConnectionState.ACTIVE, HTTPConnectionState.IDLE): + self._request_count += 1 + self._expire_at = None + self._state = HTTPConnectionState.ACTIVE + else: + raise ConnectionNotAvailable() + + async with self._init_lock: + if not self._sent_connection_init: + try: + kwargs = {"request": request} + async with Trace("send_connection_init", logger, request, kwargs): + await self._send_connection_init(**kwargs) + except BaseException as exc: + with AsyncShieldCancellation(): + await self.aclose() + raise exc + + self._sent_connection_init = True + + # Initially start with just 1 until the remote server provides + # its max_concurrent_streams value + self._max_streams = 1 + + local_settings_max_streams = ( + self._h2_state.local_settings.max_concurrent_streams + ) + self._max_streams_semaphore = AsyncSemaphore(local_settings_max_streams) + + for _ in range(local_settings_max_streams - self._max_streams): + await self._max_streams_semaphore.acquire() + + await self._max_streams_semaphore.acquire() + + try: + stream_id = self._h2_state.get_next_available_stream_id() + self._events[stream_id] = [] + except h2.exceptions.NoAvailableStreamIDError: # pragma: nocover + self._used_all_stream_ids = True + self._request_count -= 1 + raise ConnectionNotAvailable() + + try: + kwargs = {"request": request, "stream_id": stream_id} + async with Trace("send_request_headers", logger, request, kwargs): + await self._send_request_headers(request=request, stream_id=stream_id) + async with Trace("send_request_body", logger, request, kwargs): + await self._send_request_body(request=request, stream_id=stream_id) + async with Trace( + "receive_response_headers", logger, request, kwargs + ) as trace: + status, headers = await self._receive_response( + request=request, stream_id=stream_id + ) + trace.return_value = (status, headers) + + return Response( + status=status, + headers=headers, + content=HTTP2ConnectionByteStream(self, request, stream_id=stream_id), + extensions={ + "http_version": b"HTTP/2", + "network_stream": self._network_stream, + "stream_id": stream_id, + }, + ) + except BaseException as exc: # noqa: PIE786 + with AsyncShieldCancellation(): + kwargs = {"stream_id": stream_id} + async with Trace("response_closed", logger, request, kwargs): + await self._response_closed(stream_id=stream_id) + + if isinstance(exc, h2.exceptions.ProtocolError): + # One case where h2 can raise a protocol error is when a + # closed frame has been seen by the state machine. + # + # This happens when one stream is reading, and encounters + # a GOAWAY event. Other flows of control may then raise + # a protocol error at any point they interact with the 'h2_state'. + # + # In this case we'll have stored the event, and should raise + # it as a RemoteProtocolError. + if self._connection_terminated: # pragma: nocover + raise RemoteProtocolError(self._connection_terminated) + # If h2 raises a protocol error in some other state then we + # must somehow have made a protocol violation. + raise LocalProtocolError(exc) # pragma: nocover + + raise exc + + async def _send_connection_init(self, request: Request) -> None: + """ + The HTTP/2 connection requires some initial setup before we can start + using individual request/response streams on it. + """ + # Need to set these manually here instead of manipulating via + # __setitem__() otherwise the H2Connection will emit SettingsUpdate + # frames in addition to sending the undesired defaults. + self._h2_state.local_settings = h2.settings.Settings( + client=True, + initial_values={ + # Disable PUSH_PROMISE frames from the server since we don't do anything + # with them for now. Maybe when we support caching? + h2.settings.SettingCodes.ENABLE_PUSH: 0, + # These two are taken from h2 for safe defaults + h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 100, + h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: 65536, + }, + ) + + # Some websites (*cough* Yahoo *cough*) balk at this setting being + # present in the initial handshake since it's not defined in the original + # RFC despite the RFC mandating ignoring settings you don't know about. + del self._h2_state.local_settings[ + h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL + ] + + self._h2_state.initiate_connection() + self._h2_state.increment_flow_control_window(2**24) + await self._write_outgoing_data(request) + + # Sending the request... + + async def _send_request_headers(self, request: Request, stream_id: int) -> None: + """ + Send the request headers to a given stream ID. + """ + end_stream = not has_body_headers(request) + + # In HTTP/2 the ':authority' pseudo-header is used instead of 'Host'. + # In order to gracefully handle HTTP/1.1 and HTTP/2 we always require + # HTTP/1.1 style headers, and map them appropriately if we end up on + # an HTTP/2 connection. + authority = [v for k, v in request.headers if k.lower() == b"host"][0] + + headers = [ + (b":method", request.method), + (b":authority", authority), + (b":scheme", request.url.scheme), + (b":path", request.url.target), + ] + [ + (k.lower(), v) + for k, v in request.headers + if k.lower() + not in ( + b"host", + b"transfer-encoding", + ) + ] + + self._h2_state.send_headers(stream_id, headers, end_stream=end_stream) + self._h2_state.increment_flow_control_window(2**24, stream_id=stream_id) + await self._write_outgoing_data(request) + + async def _send_request_body(self, request: Request, stream_id: int) -> None: + """ + Iterate over the request body sending it to a given stream ID. + """ + if not has_body_headers(request): + return + + assert isinstance(request.stream, typing.AsyncIterable) + async for data in request.stream: + await self._send_stream_data(request, stream_id, data) + await self._send_end_stream(request, stream_id) + + async def _send_stream_data( + self, request: Request, stream_id: int, data: bytes + ) -> None: + """ + Send a single chunk of data in one or more data frames. + """ + while data: + max_flow = await self._wait_for_outgoing_flow(request, stream_id) + chunk_size = min(len(data), max_flow) + chunk, data = data[:chunk_size], data[chunk_size:] + self._h2_state.send_data(stream_id, chunk) + await self._write_outgoing_data(request) + + async def _send_end_stream(self, request: Request, stream_id: int) -> None: + """ + Send an empty data frame on on a given stream ID with the END_STREAM flag set. + """ + self._h2_state.end_stream(stream_id) + await self._write_outgoing_data(request) + + # Receiving the response... + + async def _receive_response( + self, request: Request, stream_id: int + ) -> typing.Tuple[int, typing.List[typing.Tuple[bytes, bytes]]]: + """ + Return the response status code and headers for a given stream ID. + """ + while True: + event = await self._receive_stream_event(request, stream_id) + if isinstance(event, h2.events.ResponseReceived): + break + + status_code = 200 + headers = [] + for k, v in event.headers: + if k == b":status": + status_code = int(v.decode("ascii", errors="ignore")) + elif not k.startswith(b":"): + headers.append((k, v)) + + return (status_code, headers) + + async def _receive_response_body( + self, request: Request, stream_id: int + ) -> typing.AsyncIterator[bytes]: + """ + Iterator that returns the bytes of the response body for a given stream ID. + """ + while True: + event = await self._receive_stream_event(request, stream_id) + if isinstance(event, h2.events.DataReceived): + amount = event.flow_controlled_length + self._h2_state.acknowledge_received_data(amount, stream_id) + await self._write_outgoing_data(request) + yield event.data + elif isinstance(event, h2.events.StreamEnded): + break + + async def _receive_stream_event( + self, request: Request, stream_id: int + ) -> typing.Union[ + h2.events.ResponseReceived, h2.events.DataReceived, h2.events.StreamEnded + ]: + """ + Return the next available event for a given stream ID. + + Will read more data from the network if required. + """ + while not self._events.get(stream_id): + await self._receive_events(request, stream_id) + event = self._events[stream_id].pop(0) + if isinstance(event, h2.events.StreamReset): + raise RemoteProtocolError(event) + return event + + async def _receive_events( + self, request: Request, stream_id: typing.Optional[int] = None + ) -> None: + """ + Read some data from the network until we see one or more events + for a given stream ID. + """ + async with self._read_lock: + if self._connection_terminated is not None: + last_stream_id = self._connection_terminated.last_stream_id + if stream_id and last_stream_id and stream_id > last_stream_id: + self._request_count -= 1 + raise ConnectionNotAvailable() + raise RemoteProtocolError(self._connection_terminated) + + # This conditional is a bit icky. We don't want to block reading if we've + # actually got an event to return for a given stream. We need to do that + # check *within* the atomic read lock. Though it also need to be optional, + # because when we call it from `_wait_for_outgoing_flow` we *do* want to + # block until we've available flow control, event when we have events + # pending for the stream ID we're attempting to send on. + if stream_id is None or not self._events.get(stream_id): + events = await self._read_incoming_data(request) + for event in events: + if isinstance(event, h2.events.RemoteSettingsChanged): + async with Trace( + "receive_remote_settings", logger, request + ) as trace: + await self._receive_remote_settings_change(event) + trace.return_value = event + + elif isinstance( + event, + ( + h2.events.ResponseReceived, + h2.events.DataReceived, + h2.events.StreamEnded, + h2.events.StreamReset, + ), + ): + if event.stream_id in self._events: + self._events[event.stream_id].append(event) + + elif isinstance(event, h2.events.ConnectionTerminated): + self._connection_terminated = event + + await self._write_outgoing_data(request) + + async def _receive_remote_settings_change(self, event: h2.events.Event) -> None: + max_concurrent_streams = event.changed_settings.get( + h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS + ) + if max_concurrent_streams: + new_max_streams = min( + max_concurrent_streams.new_value, + self._h2_state.local_settings.max_concurrent_streams, + ) + if new_max_streams and new_max_streams != self._max_streams: + while new_max_streams > self._max_streams: + await self._max_streams_semaphore.release() + self._max_streams += 1 + while new_max_streams < self._max_streams: + await self._max_streams_semaphore.acquire() + self._max_streams -= 1 + + async def _response_closed(self, stream_id: int) -> None: + await self._max_streams_semaphore.release() + del self._events[stream_id] + async with self._state_lock: + if self._connection_terminated and not self._events: + await self.aclose() + + elif self._state == HTTPConnectionState.ACTIVE and not self._events: + self._state = HTTPConnectionState.IDLE + if self._keepalive_expiry is not None: + now = time.monotonic() + self._expire_at = now + self._keepalive_expiry + if self._used_all_stream_ids: # pragma: nocover + await self.aclose() + + async def aclose(self) -> None: + # Note that this method unilaterally closes the connection, and does + # not have any kind of locking in place around it. + self._h2_state.close_connection() + self._state = HTTPConnectionState.CLOSED + await self._network_stream.aclose() + + # Wrappers around network read/write operations... + + async def _read_incoming_data( + self, request: Request + ) -> typing.List[h2.events.Event]: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("read", None) + + if self._read_exception is not None: + raise self._read_exception # pragma: nocover + + try: + data = await self._network_stream.read(self.READ_NUM_BYTES, timeout) + if data == b"": + raise RemoteProtocolError("Server disconnected") + except Exception as exc: + # If we get a network error we should: + # + # 1. Save the exception and just raise it immediately on any future reads. + # (For example, this means that a single read timeout or disconnect will + # immediately close all pending streams. Without requiring multiple + # sequential timeouts.) + # 2. Mark the connection as errored, so that we don't accept any other + # incoming requests. + self._read_exception = exc + self._connection_error = True + raise exc + + events: typing.List[h2.events.Event] = self._h2_state.receive_data(data) + + return events + + async def _write_outgoing_data(self, request: Request) -> None: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("write", None) + + async with self._write_lock: + data_to_send = self._h2_state.data_to_send() + + if self._write_exception is not None: + raise self._write_exception # pragma: nocover + + try: + await self._network_stream.write(data_to_send, timeout) + except Exception as exc: # pragma: nocover + # If we get a network error we should: + # + # 1. Save the exception and just raise it immediately on any future write. + # (For example, this means that a single write timeout or disconnect will + # immediately close all pending streams. Without requiring multiple + # sequential timeouts.) + # 2. Mark the connection as errored, so that we don't accept any other + # incoming requests. + self._write_exception = exc + self._connection_error = True + raise exc + + # Flow control... + + async def _wait_for_outgoing_flow(self, request: Request, stream_id: int) -> int: + """ + Returns the maximum allowable outgoing flow for a given stream. + + If the allowable flow is zero, then waits on the network until + WindowUpdated frames have increased the flow rate. + https://tools.ietf.org/html/rfc7540#section-6.9 + """ + local_flow: int = self._h2_state.local_flow_control_window(stream_id) + max_frame_size: int = self._h2_state.max_outbound_frame_size + flow = min(local_flow, max_frame_size) + while flow == 0: + await self._receive_events(request) + local_flow = self._h2_state.local_flow_control_window(stream_id) + max_frame_size = self._h2_state.max_outbound_frame_size + flow = min(local_flow, max_frame_size) + return flow + + # Interface for connection pooling... + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._origin + + def is_available(self) -> bool: + return ( + self._state != HTTPConnectionState.CLOSED + and not self._connection_error + and not self._used_all_stream_ids + and not ( + self._h2_state.state_machine.state + == h2.connection.ConnectionState.CLOSED + ) + ) + + def has_expired(self) -> bool: + now = time.monotonic() + return self._expire_at is not None and now > self._expire_at + + def is_idle(self) -> bool: + return self._state == HTTPConnectionState.IDLE + + def is_closed(self) -> bool: + return self._state == HTTPConnectionState.CLOSED + + def info(self) -> str: + origin = str(self._origin) + return ( + f"{origin!r}, HTTP/2, {self._state.name}, " + f"Request Count: {self._request_count}" + ) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + origin = str(self._origin) + return ( + f"<{class_name} [{origin!r}, {self._state.name}, " + f"Request Count: {self._request_count}]>" + ) + + # These context managers are not used in the standard flow, but are + # useful for testing or working with connection instances directly. + + async def __aenter__(self) -> "AsyncHTTP2Connection": + return self + + async def __aexit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]] = None, + exc_value: typing.Optional[BaseException] = None, + traceback: typing.Optional[types.TracebackType] = None, + ) -> None: + await self.aclose() + + +class HTTP2ConnectionByteStream: + def __init__( + self, connection: AsyncHTTP2Connection, request: Request, stream_id: int + ) -> None: + self._connection = connection + self._request = request + self._stream_id = stream_id + self._closed = False + + async def __aiter__(self) -> typing.AsyncIterator[bytes]: + kwargs = {"request": self._request, "stream_id": self._stream_id} + try: + async with Trace("receive_response_body", logger, self._request, kwargs): + async for chunk in self._connection._receive_response_body( + request=self._request, stream_id=self._stream_id + ): + yield chunk + except BaseException as exc: + # If we get an exception while streaming the response, + # we want to close the response (and possibly the connection) + # before raising that exception. + with AsyncShieldCancellation(): + await self.aclose() + raise exc + + async def aclose(self) -> None: + if not self._closed: + self._closed = True + kwargs = {"stream_id": self._stream_id} + async with Trace("response_closed", logger, self._request, kwargs): + await self._connection._response_closed(stream_id=self._stream_id) diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_async/http_proxy.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/http_proxy.py new file mode 100644 index 00000000..62f51097 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/http_proxy.py @@ -0,0 +1,350 @@ +import logging +import ssl +from base64 import b64encode +from typing import Iterable, List, Mapping, Optional, Sequence, Tuple, Union + +from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend +from .._exceptions import ProxyError +from .._models import ( + URL, + Origin, + Request, + Response, + enforce_bytes, + enforce_headers, + enforce_url, +) +from .._ssl import default_ssl_context +from .._synchronization import AsyncLock +from .._trace import Trace +from .connection import AsyncHTTPConnection +from .connection_pool import AsyncConnectionPool +from .http11 import AsyncHTTP11Connection +from .interfaces import AsyncConnectionInterface + +HeadersAsSequence = Sequence[Tuple[Union[bytes, str], Union[bytes, str]]] +HeadersAsMapping = Mapping[Union[bytes, str], Union[bytes, str]] + + +logger = logging.getLogger("httpcore.proxy") + + +def merge_headers( + default_headers: Optional[Sequence[Tuple[bytes, bytes]]] = None, + override_headers: Optional[Sequence[Tuple[bytes, bytes]]] = None, +) -> List[Tuple[bytes, bytes]]: + """ + Append default_headers and override_headers, de-duplicating if a key exists + in both cases. + """ + default_headers = [] if default_headers is None else list(default_headers) + override_headers = [] if override_headers is None else list(override_headers) + has_override = set(key.lower() for key, value in override_headers) + default_headers = [ + (key, value) + for key, value in default_headers + if key.lower() not in has_override + ] + return default_headers + override_headers + + +def build_auth_header(username: bytes, password: bytes) -> bytes: + userpass = username + b":" + password + return b"Basic " + b64encode(userpass) + + +class AsyncHTTPProxy(AsyncConnectionPool): + """ + A connection pool that sends requests via an HTTP proxy. + """ + + def __init__( + self, + proxy_url: Union[URL, bytes, str], + proxy_auth: Optional[Tuple[Union[bytes, str], Union[bytes, str]]] = None, + proxy_headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, + ssl_context: Optional[ssl.SSLContext] = None, + max_connections: Optional[int] = 10, + max_keepalive_connections: Optional[int] = None, + keepalive_expiry: Optional[float] = None, + http1: bool = True, + http2: bool = False, + retries: int = 0, + local_address: Optional[str] = None, + uds: Optional[str] = None, + network_backend: Optional[AsyncNetworkBackend] = None, + socket_options: Optional[Iterable[SOCKET_OPTION]] = None, + ) -> None: + """ + A connection pool for making HTTP requests. + + Parameters: + proxy_url: The URL to use when connecting to the proxy server. + For example `"http://127.0.0.1:8080/"`. + proxy_auth: Any proxy authentication as a two-tuple of + (username, password). May be either bytes or ascii-only str. + proxy_headers: Any HTTP headers to use for the proxy requests. + For example `{"Proxy-Authorization": "Basic :"}`. + ssl_context: An SSL context to use for verifying connections. + If not specified, the default `httpcore.default_ssl_context()` + will be used. + max_connections: The maximum number of concurrent HTTP connections that + the pool should allow. Any attempt to send a request on a pool that + would exceed this amount will block until a connection is available. + max_keepalive_connections: The maximum number of idle HTTP connections + that will be maintained in the pool. + keepalive_expiry: The duration in seconds that an idle HTTP connection + may be maintained for before being expired from the pool. + http1: A boolean indicating if HTTP/1.1 requests should be supported + by the connection pool. Defaults to True. + http2: A boolean indicating if HTTP/2 requests should be supported by + the connection pool. Defaults to False. + retries: The maximum number of retries when trying to establish + a connection. + local_address: Local address to connect from. Can also be used to + connect using a particular address family. Using + `local_address="0.0.0.0"` will connect using an `AF_INET` address + (IPv4), while using `local_address="::"` will connect using an + `AF_INET6` address (IPv6). + uds: Path to a Unix Domain Socket to use instead of TCP sockets. + network_backend: A backend instance to use for handling network I/O. + """ + super().__init__( + ssl_context=ssl_context, + max_connections=max_connections, + max_keepalive_connections=max_keepalive_connections, + keepalive_expiry=keepalive_expiry, + http1=http1, + http2=http2, + network_backend=network_backend, + retries=retries, + local_address=local_address, + uds=uds, + socket_options=socket_options, + ) + self._ssl_context = ssl_context + self._proxy_url = enforce_url(proxy_url, name="proxy_url") + self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers") + if proxy_auth is not None: + username = enforce_bytes(proxy_auth[0], name="proxy_auth") + password = enforce_bytes(proxy_auth[1], name="proxy_auth") + authorization = build_auth_header(username, password) + self._proxy_headers = [ + (b"Proxy-Authorization", authorization) + ] + self._proxy_headers + + def create_connection(self, origin: Origin) -> AsyncConnectionInterface: + if origin.scheme == b"http": + return AsyncForwardHTTPConnection( + proxy_origin=self._proxy_url.origin, + proxy_headers=self._proxy_headers, + remote_origin=origin, + keepalive_expiry=self._keepalive_expiry, + network_backend=self._network_backend, + ) + return AsyncTunnelHTTPConnection( + proxy_origin=self._proxy_url.origin, + proxy_headers=self._proxy_headers, + remote_origin=origin, + ssl_context=self._ssl_context, + keepalive_expiry=self._keepalive_expiry, + http1=self._http1, + http2=self._http2, + network_backend=self._network_backend, + ) + + +class AsyncForwardHTTPConnection(AsyncConnectionInterface): + def __init__( + self, + proxy_origin: Origin, + remote_origin: Origin, + proxy_headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, + keepalive_expiry: Optional[float] = None, + network_backend: Optional[AsyncNetworkBackend] = None, + socket_options: Optional[Iterable[SOCKET_OPTION]] = None, + ) -> None: + self._connection = AsyncHTTPConnection( + origin=proxy_origin, + keepalive_expiry=keepalive_expiry, + network_backend=network_backend, + socket_options=socket_options, + ) + self._proxy_origin = proxy_origin + self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers") + self._remote_origin = remote_origin + + async def handle_async_request(self, request: Request) -> Response: + headers = merge_headers(self._proxy_headers, request.headers) + url = URL( + scheme=self._proxy_origin.scheme, + host=self._proxy_origin.host, + port=self._proxy_origin.port, + target=bytes(request.url), + ) + proxy_request = Request( + method=request.method, + url=url, + headers=headers, + content=request.stream, + extensions=request.extensions, + ) + return await self._connection.handle_async_request(proxy_request) + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._remote_origin + + async def aclose(self) -> None: + await self._connection.aclose() + + def info(self) -> str: + return self._connection.info() + + def is_available(self) -> bool: + return self._connection.is_available() + + def has_expired(self) -> bool: + return self._connection.has_expired() + + def is_idle(self) -> bool: + return self._connection.is_idle() + + def is_closed(self) -> bool: + return self._connection.is_closed() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.info()}]>" + + +class AsyncTunnelHTTPConnection(AsyncConnectionInterface): + def __init__( + self, + proxy_origin: Origin, + remote_origin: Origin, + ssl_context: Optional[ssl.SSLContext] = None, + proxy_headers: Optional[Sequence[Tuple[bytes, bytes]]] = None, + keepalive_expiry: Optional[float] = None, + http1: bool = True, + http2: bool = False, + network_backend: Optional[AsyncNetworkBackend] = None, + socket_options: Optional[Iterable[SOCKET_OPTION]] = None, + ) -> None: + self._connection: AsyncConnectionInterface = AsyncHTTPConnection( + origin=proxy_origin, + keepalive_expiry=keepalive_expiry, + network_backend=network_backend, + socket_options=socket_options, + ) + self._proxy_origin = proxy_origin + self._remote_origin = remote_origin + self._ssl_context = ssl_context + self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers") + self._keepalive_expiry = keepalive_expiry + self._http1 = http1 + self._http2 = http2 + self._connect_lock = AsyncLock() + self._connected = False + + async def handle_async_request(self, request: Request) -> Response: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("connect", None) + + async with self._connect_lock: + if not self._connected: + target = b"%b:%d" % (self._remote_origin.host, self._remote_origin.port) + + connect_url = URL( + scheme=self._proxy_origin.scheme, + host=self._proxy_origin.host, + port=self._proxy_origin.port, + target=target, + ) + connect_headers = merge_headers( + [(b"Host", target), (b"Accept", b"*/*")], self._proxy_headers + ) + connect_request = Request( + method=b"CONNECT", + url=connect_url, + headers=connect_headers, + extensions=request.extensions, + ) + connect_response = await self._connection.handle_async_request( + connect_request + ) + + if connect_response.status < 200 or connect_response.status > 299: + reason_bytes = connect_response.extensions.get("reason_phrase", b"") + reason_str = reason_bytes.decode("ascii", errors="ignore") + msg = "%d %s" % (connect_response.status, reason_str) + await self._connection.aclose() + raise ProxyError(msg) + + stream = connect_response.extensions["network_stream"] + + # Upgrade the stream to SSL + ssl_context = ( + default_ssl_context() + if self._ssl_context is None + else self._ssl_context + ) + alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"] + ssl_context.set_alpn_protocols(alpn_protocols) + + kwargs = { + "ssl_context": ssl_context, + "server_hostname": self._remote_origin.host.decode("ascii"), + "timeout": timeout, + } + async with Trace("start_tls", logger, request, kwargs) as trace: + stream = await stream.start_tls(**kwargs) + trace.return_value = stream + + # Determine if we should be using HTTP/1.1 or HTTP/2 + ssl_object = stream.get_extra_info("ssl_object") + http2_negotiated = ( + ssl_object is not None + and ssl_object.selected_alpn_protocol() == "h2" + ) + + # Create the HTTP/1.1 or HTTP/2 connection + if http2_negotiated or (self._http2 and not self._http1): + from .http2 import AsyncHTTP2Connection + + self._connection = AsyncHTTP2Connection( + origin=self._remote_origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + else: + self._connection = AsyncHTTP11Connection( + origin=self._remote_origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + + self._connected = True + return await self._connection.handle_async_request(request) + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._remote_origin + + async def aclose(self) -> None: + await self._connection.aclose() + + def info(self) -> str: + return self._connection.info() + + def is_available(self) -> bool: + return self._connection.is_available() + + def has_expired(self) -> bool: + return self._connection.has_expired() + + def is_idle(self) -> bool: + return self._connection.is_idle() + + def is_closed(self) -> bool: + return self._connection.is_closed() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.info()}]>" diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_async/interfaces.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/interfaces.py new file mode 100644 index 00000000..c998dd27 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/interfaces.py @@ -0,0 +1,135 @@ +from contextlib import asynccontextmanager +from typing import AsyncIterator, Optional, Union + +from .._models import ( + URL, + Extensions, + HeaderTypes, + Origin, + Request, + Response, + enforce_bytes, + enforce_headers, + enforce_url, + include_request_headers, +) + + +class AsyncRequestInterface: + async def request( + self, + method: Union[bytes, str], + url: Union[URL, bytes, str], + *, + headers: HeaderTypes = None, + content: Union[bytes, AsyncIterator[bytes], None] = None, + extensions: Optional[Extensions] = None, + ) -> Response: + # Strict type checking on our parameters. + method = enforce_bytes(method, name="method") + url = enforce_url(url, name="url") + headers = enforce_headers(headers, name="headers") + + # Include Host header, and optionally Content-Length or Transfer-Encoding. + headers = include_request_headers(headers, url=url, content=content) + + request = Request( + method=method, + url=url, + headers=headers, + content=content, + extensions=extensions, + ) + response = await self.handle_async_request(request) + try: + await response.aread() + finally: + await response.aclose() + return response + + @asynccontextmanager + async def stream( + self, + method: Union[bytes, str], + url: Union[URL, bytes, str], + *, + headers: HeaderTypes = None, + content: Union[bytes, AsyncIterator[bytes], None] = None, + extensions: Optional[Extensions] = None, + ) -> AsyncIterator[Response]: + # Strict type checking on our parameters. + method = enforce_bytes(method, name="method") + url = enforce_url(url, name="url") + headers = enforce_headers(headers, name="headers") + + # Include Host header, and optionally Content-Length or Transfer-Encoding. + headers = include_request_headers(headers, url=url, content=content) + + request = Request( + method=method, + url=url, + headers=headers, + content=content, + extensions=extensions, + ) + response = await self.handle_async_request(request) + try: + yield response + finally: + await response.aclose() + + async def handle_async_request(self, request: Request) -> Response: + raise NotImplementedError() # pragma: nocover + + +class AsyncConnectionInterface(AsyncRequestInterface): + async def aclose(self) -> None: + raise NotImplementedError() # pragma: nocover + + def info(self) -> str: + raise NotImplementedError() # pragma: nocover + + def can_handle_request(self, origin: Origin) -> bool: + raise NotImplementedError() # pragma: nocover + + def is_available(self) -> bool: + """ + Return `True` if the connection is currently able to accept an + outgoing request. + + An HTTP/1.1 connection will only be available if it is currently idle. + + An HTTP/2 connection will be available so long as the stream ID space is + not yet exhausted, and the connection is not in an error state. + + While the connection is being established we may not yet know if it is going + to result in an HTTP/1.1 or HTTP/2 connection. The connection should be + treated as being available, but might ultimately raise `NewConnectionRequired` + required exceptions if multiple requests are attempted over a connection + that ends up being established as HTTP/1.1. + """ + raise NotImplementedError() # pragma: nocover + + def has_expired(self) -> bool: + """ + Return `True` if the connection is in a state where it should be closed. + + This either means that the connection is idle and it has passed the + expiry time on its keep-alive, or that server has sent an EOF. + """ + raise NotImplementedError() # pragma: nocover + + def is_idle(self) -> bool: + """ + Return `True` if the connection is currently idle. + """ + raise NotImplementedError() # pragma: nocover + + def is_closed(self) -> bool: + """ + Return `True` if the connection has been closed. + + Used when a response is closed to determine if the connection may be + returned to the connection pool or not. + """ + raise NotImplementedError() # pragma: nocover diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_async/socks_proxy.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/socks_proxy.py new file mode 100644 index 00000000..f12cb373 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_async/socks_proxy.py @@ -0,0 +1,340 @@ +import logging +import ssl +import typing + +from socksio import socks5 + +from .._backends.auto import AutoBackend +from .._backends.base import AsyncNetworkBackend, AsyncNetworkStream +from .._exceptions import ConnectionNotAvailable, ProxyError +from .._models import URL, Origin, Request, Response, enforce_bytes, enforce_url +from .._ssl import default_ssl_context +from .._synchronization import AsyncLock +from .._trace import Trace +from .connection_pool import AsyncConnectionPool +from .http11 import AsyncHTTP11Connection +from .interfaces import AsyncConnectionInterface + +logger = logging.getLogger("httpcore.socks") + + +AUTH_METHODS = { + b"\x00": "NO AUTHENTICATION REQUIRED", + b"\x01": "GSSAPI", + b"\x02": "USERNAME/PASSWORD", + b"\xff": "NO ACCEPTABLE METHODS", +} + +REPLY_CODES = { + b"\x00": "Succeeded", + b"\x01": "General SOCKS server failure", + b"\x02": "Connection not allowed by ruleset", + b"\x03": "Network unreachable", + b"\x04": "Host unreachable", + b"\x05": "Connection refused", + b"\x06": "TTL expired", + b"\x07": "Command not supported", + b"\x08": "Address type not supported", +} + + +async def _init_socks5_connection( + stream: AsyncNetworkStream, + *, + host: bytes, + port: int, + auth: typing.Optional[typing.Tuple[bytes, bytes]] = None, +) -> None: + conn = socks5.SOCKS5Connection() + + # Auth method request + auth_method = ( + socks5.SOCKS5AuthMethod.NO_AUTH_REQUIRED + if auth is None + else socks5.SOCKS5AuthMethod.USERNAME_PASSWORD + ) + conn.send(socks5.SOCKS5AuthMethodsRequest([auth_method])) + outgoing_bytes = conn.data_to_send() + await stream.write(outgoing_bytes) + + # Auth method response + incoming_bytes = await stream.read(max_bytes=4096) + response = conn.receive_data(incoming_bytes) + assert isinstance(response, socks5.SOCKS5AuthReply) + if response.method != auth_method: + requested = AUTH_METHODS.get(auth_method, "UNKNOWN") + responded = AUTH_METHODS.get(response.method, "UNKNOWN") + raise ProxyError( + f"Requested {requested} from proxy server, but got {responded}." + ) + + if response.method == socks5.SOCKS5AuthMethod.USERNAME_PASSWORD: + # Username/password request + assert auth is not None + username, password = auth + conn.send(socks5.SOCKS5UsernamePasswordRequest(username, password)) + outgoing_bytes = conn.data_to_send() + await stream.write(outgoing_bytes) + + # Username/password response + incoming_bytes = await stream.read(max_bytes=4096) + response = conn.receive_data(incoming_bytes) + assert isinstance(response, socks5.SOCKS5UsernamePasswordReply) + if not response.success: + raise ProxyError("Invalid username/password") + + # Connect request + conn.send( + socks5.SOCKS5CommandRequest.from_address( + socks5.SOCKS5Command.CONNECT, (host, port) + ) + ) + outgoing_bytes = conn.data_to_send() + await stream.write(outgoing_bytes) + + # Connect response + incoming_bytes = await stream.read(max_bytes=4096) + response = conn.receive_data(incoming_bytes) + assert isinstance(response, socks5.SOCKS5Reply) + if response.reply_code != socks5.SOCKS5ReplyCode.SUCCEEDED: + reply_code = REPLY_CODES.get(response.reply_code, "UNKOWN") + raise ProxyError(f"Proxy Server could not connect: {reply_code}.") + + +class AsyncSOCKSProxy(AsyncConnectionPool): + """ + A connection pool that sends requests via an HTTP proxy. + """ + + def __init__( + self, + proxy_url: typing.Union[URL, bytes, str], + proxy_auth: typing.Optional[ + typing.Tuple[typing.Union[bytes, str], typing.Union[bytes, str]] + ] = None, + ssl_context: typing.Optional[ssl.SSLContext] = None, + max_connections: typing.Optional[int] = 10, + max_keepalive_connections: typing.Optional[int] = None, + keepalive_expiry: typing.Optional[float] = None, + http1: bool = True, + http2: bool = False, + retries: int = 0, + network_backend: typing.Optional[AsyncNetworkBackend] = None, + ) -> None: + """ + A connection pool for making HTTP requests. + + Parameters: + proxy_url: The URL to use when connecting to the proxy server. + For example `"http://127.0.0.1:8080/"`. + ssl_context: An SSL context to use for verifying connections. + If not specified, the default `httpcore.default_ssl_context()` + will be used. + max_connections: The maximum number of concurrent HTTP connections that + the pool should allow. Any attempt to send a request on a pool that + would exceed this amount will block until a connection is available. + max_keepalive_connections: The maximum number of idle HTTP connections + that will be maintained in the pool. + keepalive_expiry: The duration in seconds that an idle HTTP connection + may be maintained for before being expired from the pool. + http1: A boolean indicating if HTTP/1.1 requests should be supported + by the connection pool. Defaults to True. + http2: A boolean indicating if HTTP/2 requests should be supported by + the connection pool. Defaults to False. + retries: The maximum number of retries when trying to establish + a connection. + local_address: Local address to connect from. Can also be used to + connect using a particular address family. Using + `local_address="0.0.0.0"` will connect using an `AF_INET` address + (IPv4), while using `local_address="::"` will connect using an + `AF_INET6` address (IPv6). + uds: Path to a Unix Domain Socket to use instead of TCP sockets. + network_backend: A backend instance to use for handling network I/O. + """ + super().__init__( + ssl_context=ssl_context, + max_connections=max_connections, + max_keepalive_connections=max_keepalive_connections, + keepalive_expiry=keepalive_expiry, + http1=http1, + http2=http2, + network_backend=network_backend, + retries=retries, + ) + self._ssl_context = ssl_context + self._proxy_url = enforce_url(proxy_url, name="proxy_url") + if proxy_auth is not None: + username, password = proxy_auth + username_bytes = enforce_bytes(username, name="proxy_auth") + password_bytes = enforce_bytes(password, name="proxy_auth") + self._proxy_auth: typing.Optional[typing.Tuple[bytes, bytes]] = ( + username_bytes, + password_bytes, + ) + else: + self._proxy_auth = None + + def create_connection(self, origin: Origin) -> AsyncConnectionInterface: + return AsyncSocks5Connection( + proxy_origin=self._proxy_url.origin, + remote_origin=origin, + proxy_auth=self._proxy_auth, + ssl_context=self._ssl_context, + keepalive_expiry=self._keepalive_expiry, + http1=self._http1, + http2=self._http2, + network_backend=self._network_backend, + ) + + +class AsyncSocks5Connection(AsyncConnectionInterface): + def __init__( + self, + proxy_origin: Origin, + remote_origin: Origin, + proxy_auth: typing.Optional[typing.Tuple[bytes, bytes]] = None, + ssl_context: typing.Optional[ssl.SSLContext] = None, + keepalive_expiry: typing.Optional[float] = None, + http1: bool = True, + http2: bool = False, + network_backend: typing.Optional[AsyncNetworkBackend] = None, + ) -> None: + self._proxy_origin = proxy_origin + self._remote_origin = remote_origin + self._proxy_auth = proxy_auth + self._ssl_context = ssl_context + self._keepalive_expiry = keepalive_expiry + self._http1 = http1 + self._http2 = http2 + + self._network_backend: AsyncNetworkBackend = ( + AutoBackend() if network_backend is None else network_backend + ) + self._connect_lock = AsyncLock() + self._connection: typing.Optional[AsyncConnectionInterface] = None + self._connect_failed = False + + async def handle_async_request(self, request: Request) -> Response: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("connect", None) + + async with self._connect_lock: + if self._connection is None: + try: + # Connect to the proxy + kwargs = { + "host": self._proxy_origin.host.decode("ascii"), + "port": self._proxy_origin.port, + "timeout": timeout, + } + with Trace("connect_tcp", logger, request, kwargs) as trace: + stream = await self._network_backend.connect_tcp(**kwargs) + trace.return_value = stream + + # Connect to the remote host using socks5 + kwargs = { + "stream": stream, + "host": self._remote_origin.host.decode("ascii"), + "port": self._remote_origin.port, + "auth": self._proxy_auth, + } + with Trace( + "setup_socks5_connection", logger, request, kwargs + ) as trace: + await _init_socks5_connection(**kwargs) + trace.return_value = stream + + # Upgrade the stream to SSL + if self._remote_origin.scheme == b"https": + ssl_context = ( + default_ssl_context() + if self._ssl_context is None + else self._ssl_context + ) + alpn_protocols = ( + ["http/1.1", "h2"] if self._http2 else ["http/1.1"] + ) + ssl_context.set_alpn_protocols(alpn_protocols) + + kwargs = { + "ssl_context": ssl_context, + "server_hostname": self._remote_origin.host.decode("ascii"), + "timeout": timeout, + } + async with Trace("start_tls", logger, request, kwargs) as trace: + stream = await stream.start_tls(**kwargs) + trace.return_value = stream + + # Determine if we should be using HTTP/1.1 or HTTP/2 + ssl_object = stream.get_extra_info("ssl_object") + http2_negotiated = ( + ssl_object is not None + and ssl_object.selected_alpn_protocol() == "h2" + ) + + # Create the HTTP/1.1 or HTTP/2 connection + if http2_negotiated or ( + self._http2 and not self._http1 + ): # pragma: nocover + from .http2 import AsyncHTTP2Connection + + self._connection = AsyncHTTP2Connection( + origin=self._remote_origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + else: + self._connection = AsyncHTTP11Connection( + origin=self._remote_origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + except Exception as exc: + self._connect_failed = True + raise exc + elif not self._connection.is_available(): # pragma: nocover + raise ConnectionNotAvailable() + + return await self._connection.handle_async_request(request) + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._remote_origin + + async def aclose(self) -> None: + if self._connection is not None: + await self._connection.aclose() + + def is_available(self) -> bool: + if self._connection is None: # pragma: nocover + # If HTTP/2 support is enabled, and the resulting connection could + # end up as HTTP/2 then we should indicate the connection as being + # available to service multiple requests. + return ( + self._http2 + and (self._remote_origin.scheme == b"https" or not self._http1) + and not self._connect_failed + ) + return self._connection.is_available() + + def has_expired(self) -> bool: + if self._connection is None: # pragma: nocover + return self._connect_failed + return self._connection.has_expired() + + def is_idle(self) -> bool: + if self._connection is None: # pragma: nocover + return self._connect_failed + return self._connection.is_idle() + + def is_closed(self) -> bool: + if self._connection is None: # pragma: nocover + return self._connect_failed + return self._connection.is_closed() + + def info(self) -> str: + if self._connection is None: # pragma: nocover + return "CONNECTION FAILED" if self._connect_failed else "CONNECTING" + return self._connection.info() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.info()}]>" diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__init__.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ee995e8588c3626a7a5f49e7c8b4a20f6d7245e1 GIT binary patch literal 201 zcmX@j%ge<81nP!LnIQTxh(HIQS%4zb87dhx8U0o=6fpsLpFwJVW$0(*=cekX=T+#t zq!wqFx8=sM-+XJ_W6>pLYTXQ$?+=$EDDmFeeXCg~ScmSp7T8S5Du=@(~~ zr0Ny`6(pvo7VBq}loTZA7p3aQCqeWS>&M4u=4F<|$LkeT{^GF7%}*)KNwq6t1zOAq R#Kj=SM`lJw#v*1Q3jhWfHEI9= literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/anyio.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/anyio.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4049a7f8aabdd416e4d3493f556aa7a0d3e16e71 GIT binary patch literal 8902 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 Oo;82)w*;>A75@)4Zs^ni literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/auto.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/auto.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bfdc76a63cdf958d318f1aa20b9d30c2cd7a1177 GIT binary patch literal 2779 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 J4mx`~{snWJZ7=`; literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/mock.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/mock.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd0d7e03f4d183da136e3130648070e676444642 GIT binary patch literal 8105 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 MXF9$o@X^-#Z!DH&0{{R3 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/trio.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/trio.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f42306a6c44217868e8731f16be24d701d16d34 GIT binary patch literal 9428 zcmeHNTWk|qnm*-nRk?hP<2W~Rzd)P-Ng&Xn8+w))=o=GOB%0lhdhi%m1sr_IR8@d5 z&d{RKD6M68h-NgQwJmj!kN`2OZfRe7rPc0|yo|IjF=1q@ng^umhi$F2GLx(_VtS?h z|5Gm8F$P*}KkkWq>eM;^`Om3S|M%Z2|L$_x36vd!9npWOBjgWwV-_yoSicI5MIsT2 ziIEB8%1kiy-ZEj4ED{@IMWdinZkYw~53)BId?hGaq z2|1!2Rg|QnyYqMB(YTzP(e$dL7bEht7ELC87@nSvCeG;MNjWST#13qll=i<;qMB?B zIBDA)amT~cA=%`ke#79xtnL~+5q$6Hc<97mj=ytaRQDZLXA_Z8S-X%_&W(g4=j4Q> zSC@8vcp<#k-%J^l%_q5KuVevUJzM$X9Xz!McH)O|c4}vZaf?(1<8`xa zphmZkE77F!26SGfZ$jr4S({N3qY)G(vtS#Aa2?NYk))Uu37KPLmfU4V1B@<&3>J4S zx>c29lL`{6IJK?qsnmZq8JGLdBrf(Jk=1ira=QQRq$bC9jU3Z)a6@!49=;elJ*&y8E@%cuB6-N+I1%9Y5&YJd zKp@{E44#1HLx+YkPm9Fd%eNCu$}-1lg|nNonl#0$#+&UDx(b4=6$HD4&S}mqUW;^5 zcFwAK%#oDU+|@qVvsQSbsVbAAculdTv?)>vC{z9-z1d1JzqOb%KLQ2#Gg##});)#kj_PbQF&SW#CfH@bp>vb5WLVR!C?vXbJeeGeB`;8&rr@dTz7Zt}is_`R zCTEn0q3slW=ejQ#ORBQ8b;N-q_Q`NG779;lvZB;thf|Rwa`e0$QbEiEyiyOHbOTB= z4!iSm6-p`~p_94*sxt&CRJTfDEv%wMw9%rVD$uQ<6s1d^5*fx0FhfNBN2vaT{OLoI z<4Nz}oz_qNH)cL<`tr=Y`)g-a#@U>9Hh=oFN7(F2JG*X0OPV>s+HIc~HtfXRbmOfx zSJxB5icOEa{>9dX)}@v;@4ypcaW!STdTt-Tc|6l~DBX4Fcj9W-p|!4)nU<5+t=YP5 zn;cN&kZhBM$&|z6mXm+lWX=Ak#nDaVPf5O2M`7Pr4mSkd%>8Y(K?{3-ux(@~x9nv` zdTqjN$WcyTCga8cT^>c9%M zozjUFs%WJPE5w@8jTHr%O$GlU6pqCLoB^;t972F8JFr4~sG@UL0wwMOx-gfjO0YzG zkgIwJDhj42JgKV9I;$TC1}Z};Rm9O$NwWf^EaHK!kYZd1vbBwiv4vQswl7`V_eJe$ zZQok$aHeW_wQ6{ig>gWScmQxC9(Y_e3|NA$9D(X-ao}6^ZP@IU!%aaebHAl_WSG4_ zL=jX&5#+ZbUK(uBzuYbYO&Nl)f0zq0!tx;jMk58=1x(R0d<4Jsaj5e4jZ8tiyi6fw zS-F?LFKB;@q5T=~3qV#l)X553j)28@iF`PMS{38~b)5 z>T>#~vhM0%#y*Z^-0f+1`wK!BVZ74P!z?k|tvC4|Mt7)cEEGv5H2I>Y zdsJCDFDs$5NmWaP<8q|bE*I=O_}Aw1v(L5fXk));`yRE-aV7S-f{h1@?1l#{E8vIG zAmg{zd-;%Lj@P^aFo(0jn|yiz|De(yP!5RYAVu(_t-oIwpg>86u`eB{|?O*KYWJ4DE)J zr;B4_#|_7uf}*JCbf=UIoed`>^=$Z@tc!(EjiN)rmm9Dd%CE4QG7YHPQS$TdJPVG7 zif#^7iBK61hNx;`MGI*$^-@g<;0nFNFhl)Ms0>%b`CM1y=S5{0%W*9AFKh(V-Km9B zS!dNRkAHkTTi=;&>dQ8_XX~3c9DJ>P-oD{3x*)vRn6q%M#z+2!#n3`%>C~Ek=m}xG zL)pfzOygj>ad541Z>DDNYRz6yjNYM54rTy@s>U0=Yo6{^TX#+*{vp(w-XYMM-l0E# z?Q8lYxG0UNcF!%G`&B$s)tRp9%+@ws7yq0C52o?yrdZD9TUCMQy>d8kglF#eF~I@$ z{w{yeYFW0_j0|(jwamzV+j5;1+uLl|-{}L&<^2ryplTJVYPGCb#o#V(#pMh3a4T(0 zu+z5EE?~Qd!~R_?a9$bUj6$Qw}e|LBB*ND&x4}Da7$v&fo)S>FNPcd zhJq3T%6!oZYHg|zWSC>5;Djr+m1kFquE%h73thbjW;u%7%$5njsS0Xqst}Br)Ub60?TVT=NO%DXjI*e9Kq@C9G>e3A2?cXy?aOdJasqq&*T4k z>Yq+M>^crnSxx=o!G(hvUtikSx9aRe(nAj%ZCPJk#@Czn^{)ALW_)|ozP%aW57NFL ztoaVFIuCz$s^JHYZCTKjA1r(@f9z3p(~Te98ozz&=BbC(L)nJbOv6CBVcL!TXkThB5vgaz{S1(( zC!hkcf>^w1p=q9fKV#<{PQmQqDX3{$j(T>NWdz}M< zTnqUN<_HkGISA3V<>+<$&fCeG$uH#e_Jg$JvFKXofMC6i;kt58GPp0>yZedQz-@o* zuSYj0$3Yu#-`Mt9>nE*ueCdW=IRU$Dq`ID_R{Ux2jvUU&*@?Yr-GQzB)m+We-W-9; zo&BG`e)sitZ0346u6F52rfXliYu^Te8{;U@+C|`3D6V{Ky&eLnVv-ya(2y~6Vft5M zWk`X`TqScM3pC}-J!h1qqx^G5xk?#CS;RSXo*>&Nostbw)egEFH+-&OU$T?`%UU)_ zzC+p`((ZyJNDQRFosi;o!DkVV?m+0}bIYT;72`kMil)QJ%ei97NEi}%VAW(*)!lhJ zl%#^KDkyW2QctQ7D*g_RJs&D_A6m{}vhzhDBmaGej9S6l+LHbiO47fA8EAcn^lu?3 zE2FIxD1rx)vO?N7uS^SpznS*6rMM&H#~`tDqJ^YvQx!a#l=ycLo?Y`!u6D&U{y$EQ9niJ^TLQ$3SAYW z7-5_P@*DV_57)a?S0d`WO9rN4qWTq7Ml4rDoQ8{eS?vc~?>kld< zx_GgFYod#D^*C*B>E3>%V?1Tho30 zSf-{sUDLgE{z1*|ClI{#Z;HTSlLfv;c(;Ac)4pnJ&pBW^gm*BQBTzw7tenJ5j`@Ne z%zb}t(8=EKYYGx>Szt!qv@P2jvE6Bd{^dTTS$>m&o)y9cMRA1_pnt{51zUv`uNOu^ zL4>ull~Y8^MclI#_v{Nl1%2A*+hl40A25?MlZPISueN)Ce^nrUS7Doq^9foa^Mbk zfM4Cv`kChw&mI1A&t1=#u5{0_bp2a7%S9Q>~#}Plq8bOivi|_r@`!x}XPHcEV3>2aRa#P35OR0+xoTL8x*p!!TbF`&Y#E z2jbYYii~BGK(&t5W({H9VbbL74gP(Gsa^GUZV+s2T(sP7hFhT literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/anyio.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/anyio.py new file mode 100644 index 00000000..1ed5228d --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/anyio.py @@ -0,0 +1,145 @@ +import ssl +import typing + +import anyio + +from .._exceptions import ( + ConnectError, + ConnectTimeout, + ReadError, + ReadTimeout, + WriteError, + WriteTimeout, + map_exceptions, +) +from .._utils import is_socket_readable +from .base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream + + +class AnyIOStream(AsyncNetworkStream): + def __init__(self, stream: anyio.abc.ByteStream) -> None: + self._stream = stream + + async def read( + self, max_bytes: int, timeout: typing.Optional[float] = None + ) -> bytes: + exc_map = { + TimeoutError: ReadTimeout, + anyio.BrokenResourceError: ReadError, + anyio.ClosedResourceError: ReadError, + } + with map_exceptions(exc_map): + with anyio.fail_after(timeout): + try: + return await self._stream.receive(max_bytes=max_bytes) + except anyio.EndOfStream: # pragma: nocover + return b"" + + async def write( + self, buffer: bytes, timeout: typing.Optional[float] = None + ) -> None: + if not buffer: + return + + exc_map = { + TimeoutError: WriteTimeout, + anyio.BrokenResourceError: WriteError, + anyio.ClosedResourceError: WriteError, + } + with map_exceptions(exc_map): + with anyio.fail_after(timeout): + await self._stream.send(item=buffer) + + async def aclose(self) -> None: + await self._stream.aclose() + + async def start_tls( + self, + ssl_context: ssl.SSLContext, + server_hostname: typing.Optional[str] = None, + timeout: typing.Optional[float] = None, + ) -> AsyncNetworkStream: + exc_map = { + TimeoutError: ConnectTimeout, + anyio.BrokenResourceError: ConnectError, + } + with map_exceptions(exc_map): + try: + with anyio.fail_after(timeout): + ssl_stream = await anyio.streams.tls.TLSStream.wrap( + self._stream, + ssl_context=ssl_context, + hostname=server_hostname, + standard_compatible=False, + server_side=False, + ) + except Exception as exc: # pragma: nocover + await self.aclose() + raise exc + return AnyIOStream(ssl_stream) + + def get_extra_info(self, info: str) -> typing.Any: + if info == "ssl_object": + return self._stream.extra(anyio.streams.tls.TLSAttribute.ssl_object, None) + if info == "client_addr": + return self._stream.extra(anyio.abc.SocketAttribute.local_address, None) + if info == "server_addr": + return self._stream.extra(anyio.abc.SocketAttribute.remote_address, None) + if info == "socket": + return self._stream.extra(anyio.abc.SocketAttribute.raw_socket, None) + if info == "is_readable": + sock = self._stream.extra(anyio.abc.SocketAttribute.raw_socket, None) + return is_socket_readable(sock) + return None + + +class AnyIOBackend(AsyncNetworkBackend): + async def connect_tcp( + self, + host: str, + port: int, + timeout: typing.Optional[float] = None, + local_address: typing.Optional[str] = None, + socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, + ) -> AsyncNetworkStream: + if socket_options is None: + socket_options = [] # pragma: no cover + exc_map = { + TimeoutError: ConnectTimeout, + OSError: ConnectError, + anyio.BrokenResourceError: ConnectError, + } + with map_exceptions(exc_map): + with anyio.fail_after(timeout): + stream: anyio.abc.ByteStream = await anyio.connect_tcp( + remote_host=host, + remote_port=port, + local_host=local_address, + ) + # By default TCP sockets opened in `asyncio` include TCP_NODELAY. + for option in socket_options: + stream._raw_socket.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover + return AnyIOStream(stream) + + async def connect_unix_socket( + self, + path: str, + timeout: typing.Optional[float] = None, + socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, + ) -> AsyncNetworkStream: # pragma: nocover + if socket_options is None: + socket_options = [] + exc_map = { + TimeoutError: ConnectTimeout, + OSError: ConnectError, + anyio.BrokenResourceError: ConnectError, + } + with map_exceptions(exc_map): + with anyio.fail_after(timeout): + stream: anyio.abc.ByteStream = await anyio.connect_unix(path) + for option in socket_options: + stream._raw_socket.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover + return AnyIOStream(stream) + + async def sleep(self, seconds: float) -> None: + await anyio.sleep(seconds) # pragma: nocover diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/auto.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/auto.py new file mode 100644 index 00000000..b612ba07 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/auto.py @@ -0,0 +1,52 @@ +import typing +from typing import Optional + +import sniffio + +from .base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream + + +class AutoBackend(AsyncNetworkBackend): + async def _init_backend(self) -> None: + if not (hasattr(self, "_backend")): + backend = sniffio.current_async_library() + if backend == "trio": + from .trio import TrioBackend + + self._backend: AsyncNetworkBackend = TrioBackend() + else: + from .anyio import AnyIOBackend + + self._backend = AnyIOBackend() + + async def connect_tcp( + self, + host: str, + port: int, + timeout: Optional[float] = None, + local_address: Optional[str] = None, + socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, + ) -> AsyncNetworkStream: + await self._init_backend() + return await self._backend.connect_tcp( + host, + port, + timeout=timeout, + local_address=local_address, + socket_options=socket_options, + ) + + async def connect_unix_socket( + self, + path: str, + timeout: Optional[float] = None, + socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, + ) -> AsyncNetworkStream: # pragma: nocover + await self._init_backend() + return await self._backend.connect_unix_socket( + path, timeout=timeout, socket_options=socket_options + ) + + async def sleep(self, seconds: float) -> None: # pragma: nocover + await self._init_backend() + return await self._backend.sleep(seconds) diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/base.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/base.py new file mode 100644 index 00000000..6cadedb5 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/base.py @@ -0,0 +1,103 @@ +import ssl +import time +import typing + +SOCKET_OPTION = typing.Union[ + typing.Tuple[int, int, int], + typing.Tuple[int, int, typing.Union[bytes, bytearray]], + typing.Tuple[int, int, None, int], +] + + +class NetworkStream: + def read(self, max_bytes: int, timeout: typing.Optional[float] = None) -> bytes: + raise NotImplementedError() # pragma: nocover + + def write(self, buffer: bytes, timeout: typing.Optional[float] = None) -> None: + raise NotImplementedError() # pragma: nocover + + def close(self) -> None: + raise NotImplementedError() # pragma: nocover + + def start_tls( + self, + ssl_context: ssl.SSLContext, + server_hostname: typing.Optional[str] = None, + timeout: typing.Optional[float] = None, + ) -> "NetworkStream": + raise NotImplementedError() # pragma: nocover + + def get_extra_info(self, info: str) -> typing.Any: + return None # pragma: nocover + + +class NetworkBackend: + def connect_tcp( + self, + host: str, + port: int, + timeout: typing.Optional[float] = None, + local_address: typing.Optional[str] = None, + socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, + ) -> NetworkStream: + raise NotImplementedError() # pragma: nocover + + def connect_unix_socket( + self, + path: str, + timeout: typing.Optional[float] = None, + socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, + ) -> NetworkStream: + raise NotImplementedError() # pragma: nocover + + def sleep(self, seconds: float) -> None: + time.sleep(seconds) # pragma: nocover + + +class AsyncNetworkStream: + async def read( + self, max_bytes: int, timeout: typing.Optional[float] = None + ) -> bytes: + raise NotImplementedError() # pragma: nocover + + async def write( + self, buffer: bytes, timeout: typing.Optional[float] = None + ) -> None: + raise NotImplementedError() # pragma: nocover + + async def aclose(self) -> None: + raise NotImplementedError() # pragma: nocover + + async def start_tls( + self, + ssl_context: ssl.SSLContext, + server_hostname: typing.Optional[str] = None, + timeout: typing.Optional[float] = None, + ) -> "AsyncNetworkStream": + raise NotImplementedError() # pragma: nocover + + def get_extra_info(self, info: str) -> typing.Any: + return None # pragma: nocover + + +class AsyncNetworkBackend: + async def connect_tcp( + self, + host: str, + port: int, + timeout: typing.Optional[float] = None, + local_address: typing.Optional[str] = None, + socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, + ) -> AsyncNetworkStream: + raise NotImplementedError() # pragma: nocover + + async def connect_unix_socket( + self, + path: str, + timeout: typing.Optional[float] = None, + socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, + ) -> AsyncNetworkStream: + raise NotImplementedError() # pragma: nocover + + async def sleep(self, seconds: float) -> None: + raise NotImplementedError() # pragma: nocover diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/mock.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/mock.py new file mode 100644 index 00000000..f7aefebf --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/mock.py @@ -0,0 +1,142 @@ +import ssl +import typing +from typing import Optional + +from .._exceptions import ReadError +from .base import ( + SOCKET_OPTION, + AsyncNetworkBackend, + AsyncNetworkStream, + NetworkBackend, + NetworkStream, +) + + +class MockSSLObject: + def __init__(self, http2: bool): + self._http2 = http2 + + def selected_alpn_protocol(self) -> str: + return "h2" if self._http2 else "http/1.1" + + +class MockStream(NetworkStream): + def __init__(self, buffer: typing.List[bytes], http2: bool = False) -> None: + self._buffer = buffer + self._http2 = http2 + self._closed = False + + def read(self, max_bytes: int, timeout: Optional[float] = None) -> bytes: + if self._closed: + raise ReadError("Connection closed") + if not self._buffer: + return b"" + return self._buffer.pop(0) + + def write(self, buffer: bytes, timeout: Optional[float] = None) -> None: + pass + + def close(self) -> None: + self._closed = True + + def start_tls( + self, + ssl_context: ssl.SSLContext, + server_hostname: Optional[str] = None, + timeout: Optional[float] = None, + ) -> NetworkStream: + return self + + def get_extra_info(self, info: str) -> typing.Any: + return MockSSLObject(http2=self._http2) if info == "ssl_object" else None + + def __repr__(self) -> str: + return "" + + +class MockBackend(NetworkBackend): + def __init__(self, buffer: typing.List[bytes], http2: bool = False) -> None: + self._buffer = buffer + self._http2 = http2 + + def connect_tcp( + self, + host: str, + port: int, + timeout: Optional[float] = None, + local_address: Optional[str] = None, + socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, + ) -> NetworkStream: + return MockStream(list(self._buffer), http2=self._http2) + + def connect_unix_socket( + self, + path: str, + timeout: Optional[float] = None, + socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, + ) -> NetworkStream: + return MockStream(list(self._buffer), http2=self._http2) + + def sleep(self, seconds: float) -> None: + pass + + +class AsyncMockStream(AsyncNetworkStream): + def __init__(self, buffer: typing.List[bytes], http2: bool = False) -> None: + self._buffer = buffer + self._http2 = http2 + self._closed = False + + async def read(self, max_bytes: int, timeout: Optional[float] = None) -> bytes: + if self._closed: + raise ReadError("Connection closed") + if not self._buffer: + return b"" + return self._buffer.pop(0) + + async def write(self, buffer: bytes, timeout: Optional[float] = None) -> None: + pass + + async def aclose(self) -> None: + self._closed = True + + async def start_tls( + self, + ssl_context: ssl.SSLContext, + server_hostname: Optional[str] = None, + timeout: Optional[float] = None, + ) -> AsyncNetworkStream: + return self + + def get_extra_info(self, info: str) -> typing.Any: + return MockSSLObject(http2=self._http2) if info == "ssl_object" else None + + def __repr__(self) -> str: + return "" + + +class AsyncMockBackend(AsyncNetworkBackend): + def __init__(self, buffer: typing.List[bytes], http2: bool = False) -> None: + self._buffer = buffer + self._http2 = http2 + + async def connect_tcp( + self, + host: str, + port: int, + timeout: Optional[float] = None, + local_address: Optional[str] = None, + socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, + ) -> AsyncNetworkStream: + return AsyncMockStream(list(self._buffer), http2=self._http2) + + async def connect_unix_socket( + self, + path: str, + timeout: Optional[float] = None, + socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, + ) -> AsyncNetworkStream: + return AsyncMockStream(list(self._buffer), http2=self._http2) + + async def sleep(self, seconds: float) -> None: + pass diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/sync.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/sync.py new file mode 100644 index 00000000..a4c85f04 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/sync.py @@ -0,0 +1,133 @@ +import socket +import ssl +import sys +import typing + +from .._exceptions import ( + ConnectError, + ConnectTimeout, + ExceptionMapping, + ReadError, + ReadTimeout, + WriteError, + WriteTimeout, + map_exceptions, +) +from .._utils import is_socket_readable +from .base import SOCKET_OPTION, NetworkBackend, NetworkStream + + +class SyncStream(NetworkStream): + def __init__(self, sock: socket.socket) -> None: + self._sock = sock + + def read(self, max_bytes: int, timeout: typing.Optional[float] = None) -> bytes: + exc_map: ExceptionMapping = {socket.timeout: ReadTimeout, OSError: ReadError} + with map_exceptions(exc_map): + self._sock.settimeout(timeout) + return self._sock.recv(max_bytes) + + def write(self, buffer: bytes, timeout: typing.Optional[float] = None) -> None: + if not buffer: + return + + exc_map: ExceptionMapping = {socket.timeout: WriteTimeout, OSError: WriteError} + with map_exceptions(exc_map): + while buffer: + self._sock.settimeout(timeout) + n = self._sock.send(buffer) + buffer = buffer[n:] + + def close(self) -> None: + self._sock.close() + + def start_tls( + self, + ssl_context: ssl.SSLContext, + server_hostname: typing.Optional[str] = None, + timeout: typing.Optional[float] = None, + ) -> NetworkStream: + exc_map: ExceptionMapping = { + socket.timeout: ConnectTimeout, + OSError: ConnectError, + } + with map_exceptions(exc_map): + try: + self._sock.settimeout(timeout) + sock = ssl_context.wrap_socket( + self._sock, server_hostname=server_hostname + ) + except Exception as exc: # pragma: nocover + self.close() + raise exc + return SyncStream(sock) + + def get_extra_info(self, info: str) -> typing.Any: + if info == "ssl_object" and isinstance(self._sock, ssl.SSLSocket): + return self._sock._sslobj # type: ignore + if info == "client_addr": + return self._sock.getsockname() + if info == "server_addr": + return self._sock.getpeername() + if info == "socket": + return self._sock + if info == "is_readable": + return is_socket_readable(self._sock) + return None + + +class SyncBackend(NetworkBackend): + def connect_tcp( + self, + host: str, + port: int, + timeout: typing.Optional[float] = None, + local_address: typing.Optional[str] = None, + socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, + ) -> NetworkStream: + # Note that we automatically include `TCP_NODELAY` + # in addition to any other custom socket options. + if socket_options is None: + socket_options = [] # pragma: no cover + address = (host, port) + source_address = None if local_address is None else (local_address, 0) + exc_map: ExceptionMapping = { + socket.timeout: ConnectTimeout, + OSError: ConnectError, + } + + with map_exceptions(exc_map): + sock = socket.create_connection( + address, + timeout, + source_address=source_address, + ) + for option in socket_options: + sock.setsockopt(*option) # pragma: no cover + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + return SyncStream(sock) + + def connect_unix_socket( + self, + path: str, + timeout: typing.Optional[float] = None, + socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, + ) -> NetworkStream: # pragma: nocover + if sys.platform == "win32": + raise RuntimeError( + "Attempted to connect to a UNIX socket on a Windows system." + ) + if socket_options is None: + socket_options = [] + + exc_map: ExceptionMapping = { + socket.timeout: ConnectTimeout, + OSError: ConnectError, + } + with map_exceptions(exc_map): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + for option in socket_options: + sock.setsockopt(*option) + sock.settimeout(timeout) + sock.connect(path) + return SyncStream(sock) diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/trio.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/trio.py new file mode 100644 index 00000000..b1626d28 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_backends/trio.py @@ -0,0 +1,161 @@ +import ssl +import typing + +import trio + +from .._exceptions import ( + ConnectError, + ConnectTimeout, + ExceptionMapping, + ReadError, + ReadTimeout, + WriteError, + WriteTimeout, + map_exceptions, +) +from .base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream + + +class TrioStream(AsyncNetworkStream): + def __init__(self, stream: trio.abc.Stream) -> None: + self._stream = stream + + async def read( + self, max_bytes: int, timeout: typing.Optional[float] = None + ) -> bytes: + timeout_or_inf = float("inf") if timeout is None else timeout + exc_map: ExceptionMapping = { + trio.TooSlowError: ReadTimeout, + trio.BrokenResourceError: ReadError, + trio.ClosedResourceError: ReadError, + } + with map_exceptions(exc_map): + with trio.fail_after(timeout_or_inf): + data: bytes = await self._stream.receive_some(max_bytes=max_bytes) + return data + + async def write( + self, buffer: bytes, timeout: typing.Optional[float] = None + ) -> None: + if not buffer: + return + + timeout_or_inf = float("inf") if timeout is None else timeout + exc_map: ExceptionMapping = { + trio.TooSlowError: WriteTimeout, + trio.BrokenResourceError: WriteError, + trio.ClosedResourceError: WriteError, + } + with map_exceptions(exc_map): + with trio.fail_after(timeout_or_inf): + await self._stream.send_all(data=buffer) + + async def aclose(self) -> None: + await self._stream.aclose() + + async def start_tls( + self, + ssl_context: ssl.SSLContext, + server_hostname: typing.Optional[str] = None, + timeout: typing.Optional[float] = None, + ) -> AsyncNetworkStream: + timeout_or_inf = float("inf") if timeout is None else timeout + exc_map: ExceptionMapping = { + trio.TooSlowError: ConnectTimeout, + trio.BrokenResourceError: ConnectError, + } + ssl_stream = trio.SSLStream( + self._stream, + ssl_context=ssl_context, + server_hostname=server_hostname, + https_compatible=True, + server_side=False, + ) + with map_exceptions(exc_map): + try: + with trio.fail_after(timeout_or_inf): + await ssl_stream.do_handshake() + except Exception as exc: # pragma: nocover + await self.aclose() + raise exc + return TrioStream(ssl_stream) + + def get_extra_info(self, info: str) -> typing.Any: + if info == "ssl_object" and isinstance(self._stream, trio.SSLStream): + # Type checkers cannot see `_ssl_object` attribute because trio._ssl.SSLStream uses __getattr__/__setattr__. + # Tracked at https://github.com/python-trio/trio/issues/542 + return self._stream._ssl_object # type: ignore[attr-defined] + if info == "client_addr": + return self._get_socket_stream().socket.getsockname() + if info == "server_addr": + return self._get_socket_stream().socket.getpeername() + if info == "socket": + stream = self._stream + while isinstance(stream, trio.SSLStream): + stream = stream.transport_stream + assert isinstance(stream, trio.SocketStream) + return stream.socket + if info == "is_readable": + socket = self.get_extra_info("socket") + return socket.is_readable() + return None + + def _get_socket_stream(self) -> trio.SocketStream: + stream = self._stream + while isinstance(stream, trio.SSLStream): + stream = stream.transport_stream + assert isinstance(stream, trio.SocketStream) + return stream + + +class TrioBackend(AsyncNetworkBackend): + async def connect_tcp( + self, + host: str, + port: int, + timeout: typing.Optional[float] = None, + local_address: typing.Optional[str] = None, + socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, + ) -> AsyncNetworkStream: + # By default for TCP sockets, trio enables TCP_NODELAY. + # https://trio.readthedocs.io/en/stable/reference-io.html#trio.SocketStream + if socket_options is None: + socket_options = [] # pragma: no cover + timeout_or_inf = float("inf") if timeout is None else timeout + exc_map: ExceptionMapping = { + trio.TooSlowError: ConnectTimeout, + trio.BrokenResourceError: ConnectError, + OSError: ConnectError, + } + with map_exceptions(exc_map): + with trio.fail_after(timeout_or_inf): + stream: trio.abc.Stream = await trio.open_tcp_stream( + host=host, port=port, local_address=local_address + ) + for option in socket_options: + stream.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover + return TrioStream(stream) + + async def connect_unix_socket( + self, + path: str, + timeout: typing.Optional[float] = None, + socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, + ) -> AsyncNetworkStream: # pragma: nocover + if socket_options is None: + socket_options = [] + timeout_or_inf = float("inf") if timeout is None else timeout + exc_map: ExceptionMapping = { + trio.TooSlowError: ConnectTimeout, + trio.BrokenResourceError: ConnectError, + OSError: ConnectError, + } + with map_exceptions(exc_map): + with trio.fail_after(timeout_or_inf): + stream: trio.abc.Stream = await trio.open_unix_socket(path) + for option in socket_options: + stream.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover + return TrioStream(stream) + + async def sleep(self, seconds: float) -> None: + await trio.sleep(seconds) # pragma: nocover diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_exceptions.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_exceptions.py new file mode 100644 index 00000000..81e7fc61 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_exceptions.py @@ -0,0 +1,81 @@ +import contextlib +from typing import Iterator, Mapping, Type + +ExceptionMapping = Mapping[Type[Exception], Type[Exception]] + + +@contextlib.contextmanager +def map_exceptions(map: ExceptionMapping) -> Iterator[None]: + try: + yield + except Exception as exc: # noqa: PIE786 + for from_exc, to_exc in map.items(): + if isinstance(exc, from_exc): + raise to_exc(exc) from exc + raise # pragma: nocover + + +class ConnectionNotAvailable(Exception): + pass + + +class ProxyError(Exception): + pass + + +class UnsupportedProtocol(Exception): + pass + + +class ProtocolError(Exception): + pass + + +class RemoteProtocolError(ProtocolError): + pass + + +class LocalProtocolError(ProtocolError): + pass + + +# Timeout errors + + +class TimeoutException(Exception): + pass + + +class PoolTimeout(TimeoutException): + pass + + +class ConnectTimeout(TimeoutException): + pass + + +class ReadTimeout(TimeoutException): + pass + + +class WriteTimeout(TimeoutException): + pass + + +# Network errors + + +class NetworkError(Exception): + pass + + +class ConnectError(NetworkError): + pass + + +class ReadError(NetworkError): + pass + + +class WriteError(NetworkError): + pass diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_models.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_models.py new file mode 100644 index 00000000..e15305ee --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_models.py @@ -0,0 +1,483 @@ +from typing import ( + Any, + AsyncIterable, + AsyncIterator, + Iterable, + Iterator, + List, + Mapping, + Optional, + Sequence, + Tuple, + Union, +) +from urllib.parse import urlparse + +# Functions for typechecking... + + +HeadersAsSequence = Sequence[Tuple[Union[bytes, str], Union[bytes, str]]] +HeadersAsMapping = Mapping[Union[bytes, str], Union[bytes, str]] +HeaderTypes = Union[HeadersAsSequence, HeadersAsMapping, None] + +Extensions = Mapping[str, Any] + + +def enforce_bytes(value: Union[bytes, str], *, name: str) -> bytes: + """ + Any arguments that are ultimately represented as bytes can be specified + either as bytes or as strings. + + However we enforce that any string arguments must only contain characters in + the plain ASCII range. chr(0)...chr(127). If you need to use characters + outside that range then be precise, and use a byte-wise argument. + """ + if isinstance(value, str): + try: + return value.encode("ascii") + except UnicodeEncodeError: + raise TypeError(f"{name} strings may not include unicode characters.") + elif isinstance(value, bytes): + return value + + seen_type = type(value).__name__ + raise TypeError(f"{name} must be bytes or str, but got {seen_type}.") + + +def enforce_url(value: Union["URL", bytes, str], *, name: str) -> "URL": + """ + Type check for URL parameters. + """ + if isinstance(value, (bytes, str)): + return URL(value) + elif isinstance(value, URL): + return value + + seen_type = type(value).__name__ + raise TypeError(f"{name} must be a URL, bytes, or str, but got {seen_type}.") + + +def enforce_headers( + value: Union[HeadersAsMapping, HeadersAsSequence, None] = None, *, name: str +) -> List[Tuple[bytes, bytes]]: + """ + Convienence function that ensure all items in request or response headers + are either bytes or strings in the plain ASCII range. + """ + if value is None: + return [] + elif isinstance(value, Mapping): + return [ + ( + enforce_bytes(k, name="header name"), + enforce_bytes(v, name="header value"), + ) + for k, v in value.items() + ] + elif isinstance(value, Sequence): + return [ + ( + enforce_bytes(k, name="header name"), + enforce_bytes(v, name="header value"), + ) + for k, v in value + ] + + seen_type = type(value).__name__ + raise TypeError( + f"{name} must be a mapping or sequence of two-tuples, but got {seen_type}." + ) + + +def enforce_stream( + value: Union[bytes, Iterable[bytes], AsyncIterable[bytes], None], *, name: str +) -> Union[Iterable[bytes], AsyncIterable[bytes]]: + if value is None: + return ByteStream(b"") + elif isinstance(value, bytes): + return ByteStream(value) + return value + + +# * https://tools.ietf.org/html/rfc3986#section-3.2.3 +# * https://url.spec.whatwg.org/#url-miscellaneous +# * https://url.spec.whatwg.org/#scheme-state +DEFAULT_PORTS = { + b"ftp": 21, + b"http": 80, + b"https": 443, + b"ws": 80, + b"wss": 443, +} + + +def include_request_headers( + headers: List[Tuple[bytes, bytes]], + *, + url: "URL", + content: Union[None, bytes, Iterable[bytes], AsyncIterable[bytes]], +) -> List[Tuple[bytes, bytes]]: + headers_set = set(k.lower() for k, v in headers) + + if b"host" not in headers_set: + default_port = DEFAULT_PORTS.get(url.scheme) + if url.port is None or url.port == default_port: + header_value = url.host + else: + header_value = b"%b:%d" % (url.host, url.port) + headers = [(b"Host", header_value)] + headers + + if ( + content is not None + and b"content-length" not in headers_set + and b"transfer-encoding" not in headers_set + ): + if isinstance(content, bytes): + content_length = str(len(content)).encode("ascii") + headers += [(b"Content-Length", content_length)] + else: + headers += [(b"Transfer-Encoding", b"chunked")] # pragma: nocover + + return headers + + +# Interfaces for byte streams... + + +class ByteStream: + """ + A container for non-streaming content, and that supports both sync and async + stream iteration. + """ + + def __init__(self, content: bytes) -> None: + self._content = content + + def __iter__(self) -> Iterator[bytes]: + yield self._content + + async def __aiter__(self) -> AsyncIterator[bytes]: + yield self._content + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{len(self._content)} bytes]>" + + +class Origin: + def __init__(self, scheme: bytes, host: bytes, port: int) -> None: + self.scheme = scheme + self.host = host + self.port = port + + def __eq__(self, other: Any) -> bool: + return ( + isinstance(other, Origin) + and self.scheme == other.scheme + and self.host == other.host + and self.port == other.port + ) + + def __str__(self) -> str: + scheme = self.scheme.decode("ascii") + host = self.host.decode("ascii") + port = str(self.port) + return f"{scheme}://{host}:{port}" + + +class URL: + """ + Represents the URL against which an HTTP request may be made. + + The URL may either be specified as a plain string, for convienence: + + ```python + url = httpcore.URL("https://www.example.com/") + ``` + + Or be constructed with explicitily pre-parsed components: + + ```python + url = httpcore.URL(scheme=b'https', host=b'www.example.com', port=None, target=b'/') + ``` + + Using this second more explicit style allows integrations that are using + `httpcore` to pass through URLs that have already been parsed in order to use + libraries such as `rfc-3986` rather than relying on the stdlib. It also ensures + that URL parsing is treated identically at both the networking level and at any + higher layers of abstraction. + + The four components are important here, as they allow the URL to be precisely + specified in a pre-parsed format. They also allow certain types of request to + be created that could not otherwise be expressed. + + For example, an HTTP request to `http://www.example.com/` forwarded via a proxy + at `http://localhost:8080`... + + ```python + # Constructs an HTTP request with a complete URL as the target: + # GET https://www.example.com/ HTTP/1.1 + url = httpcore.URL( + scheme=b'http', + host=b'localhost', + port=8080, + target=b'https://www.example.com/' + ) + request = httpcore.Request( + method="GET", + url=url + ) + ``` + + Another example is constructing an `OPTIONS *` request... + + ```python + # Constructs an 'OPTIONS *' HTTP request: + # OPTIONS * HTTP/1.1 + url = httpcore.URL(scheme=b'https', host=b'www.example.com', target=b'*') + request = httpcore.Request(method="OPTIONS", url=url) + ``` + + This kind of request is not possible to formulate with a URL string, + because the `/` delimiter is always used to demark the target from the + host/port portion of the URL. + + For convenience, string-like arguments may be specified either as strings or + as bytes. However, once a request is being issue over-the-wire, the URL + components are always ultimately required to be a bytewise representation. + + In order to avoid any ambiguity over character encodings, when strings are used + as arguments, they must be strictly limited to the ASCII range `chr(0)`-`chr(127)`. + If you require a bytewise representation that is outside this range you must + handle the character encoding directly, and pass a bytes instance. + """ + + def __init__( + self, + url: Union[bytes, str] = "", + *, + scheme: Union[bytes, str] = b"", + host: Union[bytes, str] = b"", + port: Optional[int] = None, + target: Union[bytes, str] = b"", + ) -> None: + """ + Parameters: + url: The complete URL as a string or bytes. + scheme: The URL scheme as a string or bytes. + Typically either `"http"` or `"https"`. + host: The URL host as a string or bytes. Such as `"www.example.com"`. + port: The port to connect to. Either an integer or `None`. + target: The target of the HTTP request. Such as `"/items?search=red"`. + """ + if url: + parsed = urlparse(enforce_bytes(url, name="url")) + self.scheme = parsed.scheme + self.host = parsed.hostname or b"" + self.port = parsed.port + self.target = (parsed.path or b"/") + ( + b"?" + parsed.query if parsed.query else b"" + ) + else: + self.scheme = enforce_bytes(scheme, name="scheme") + self.host = enforce_bytes(host, name="host") + self.port = port + self.target = enforce_bytes(target, name="target") + + @property + def origin(self) -> Origin: + default_port = { + b"http": 80, + b"https": 443, + b"ws": 80, + b"wss": 443, + b"socks5": 1080, + }[self.scheme] + return Origin( + scheme=self.scheme, host=self.host, port=self.port or default_port + ) + + def __eq__(self, other: Any) -> bool: + return ( + isinstance(other, URL) + and other.scheme == self.scheme + and other.host == self.host + and other.port == self.port + and other.target == self.target + ) + + def __bytes__(self) -> bytes: + if self.port is None: + return b"%b://%b%b" % (self.scheme, self.host, self.target) + return b"%b://%b:%d%b" % (self.scheme, self.host, self.port, self.target) + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(scheme={self.scheme!r}, " + f"host={self.host!r}, port={self.port!r}, target={self.target!r})" + ) + + +class Request: + """ + An HTTP request. + """ + + def __init__( + self, + method: Union[bytes, str], + url: Union[URL, bytes, str], + *, + headers: HeaderTypes = None, + content: Union[bytes, Iterable[bytes], AsyncIterable[bytes], None] = None, + extensions: Optional[Extensions] = None, + ) -> None: + """ + Parameters: + method: The HTTP request method, either as a string or bytes. + For example: `GET`. + url: The request URL, either as a `URL` instance, or as a string or bytes. + For example: `"https://www.example.com".` + headers: The HTTP request headers. + content: The content of the response body. + extensions: A dictionary of optional extra information included on + the request. Possible keys include `"timeout"`, and `"trace"`. + """ + self.method: bytes = enforce_bytes(method, name="method") + self.url: URL = enforce_url(url, name="url") + self.headers: List[Tuple[bytes, bytes]] = enforce_headers( + headers, name="headers" + ) + self.stream: Union[Iterable[bytes], AsyncIterable[bytes]] = enforce_stream( + content, name="content" + ) + self.extensions = {} if extensions is None else extensions + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.method!r}]>" + + +class Response: + """ + An HTTP response. + """ + + def __init__( + self, + status: int, + *, + headers: HeaderTypes = None, + content: Union[bytes, Iterable[bytes], AsyncIterable[bytes], None] = None, + extensions: Optional[Extensions] = None, + ) -> None: + """ + Parameters: + status: The HTTP status code of the response. For example `200`. + headers: The HTTP response headers. + content: The content of the response body. + extensions: A dictionary of optional extra information included on + the responseself.Possible keys include `"http_version"`, + `"reason_phrase"`, and `"network_stream"`. + """ + self.status: int = status + self.headers: List[Tuple[bytes, bytes]] = enforce_headers( + headers, name="headers" + ) + self.stream: Union[Iterable[bytes], AsyncIterable[bytes]] = enforce_stream( + content, name="content" + ) + self.extensions = {} if extensions is None else extensions + + self._stream_consumed = False + + @property + def content(self) -> bytes: + if not hasattr(self, "_content"): + if isinstance(self.stream, Iterable): + raise RuntimeError( + "Attempted to access 'response.content' on a streaming response. " + "Call 'response.read()' first." + ) + else: + raise RuntimeError( + "Attempted to access 'response.content' on a streaming response. " + "Call 'await response.aread()' first." + ) + return self._content + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.status}]>" + + # Sync interface... + + def read(self) -> bytes: + if not isinstance(self.stream, Iterable): # pragma: nocover + raise RuntimeError( + "Attempted to read an asynchronous response using 'response.read()'. " + "You should use 'await response.aread()' instead." + ) + if not hasattr(self, "_content"): + self._content = b"".join([part for part in self.iter_stream()]) + return self._content + + def iter_stream(self) -> Iterator[bytes]: + if not isinstance(self.stream, Iterable): # pragma: nocover + raise RuntimeError( + "Attempted to stream an asynchronous response using 'for ... in " + "response.iter_stream()'. " + "You should use 'async for ... in response.aiter_stream()' instead." + ) + if self._stream_consumed: + raise RuntimeError( + "Attempted to call 'for ... in response.iter_stream()' more than once." + ) + self._stream_consumed = True + for chunk in self.stream: + yield chunk + + def close(self) -> None: + if not isinstance(self.stream, Iterable): # pragma: nocover + raise RuntimeError( + "Attempted to close an asynchronous response using 'response.close()'. " + "You should use 'await response.aclose()' instead." + ) + if hasattr(self.stream, "close"): + self.stream.close() + + # Async interface... + + async def aread(self) -> bytes: + if not isinstance(self.stream, AsyncIterable): # pragma: nocover + raise RuntimeError( + "Attempted to read an synchronous response using " + "'await response.aread()'. " + "You should use 'response.read()' instead." + ) + if not hasattr(self, "_content"): + self._content = b"".join([part async for part in self.aiter_stream()]) + return self._content + + async def aiter_stream(self) -> AsyncIterator[bytes]: + if not isinstance(self.stream, AsyncIterable): # pragma: nocover + raise RuntimeError( + "Attempted to stream an synchronous response using 'async for ... in " + "response.aiter_stream()'. " + "You should use 'for ... in response.iter_stream()' instead." + ) + if self._stream_consumed: + raise RuntimeError( + "Attempted to call 'async for ... in response.aiter_stream()' " + "more than once." + ) + self._stream_consumed = True + async for chunk in self.stream: + yield chunk + + async def aclose(self) -> None: + if not isinstance(self.stream, AsyncIterable): # pragma: nocover + raise RuntimeError( + "Attempted to close a synchronous response using " + "'await response.aclose()'. " + "You should use 'response.close()' instead." + ) + if hasattr(self.stream, "aclose"): + await self.stream.aclose() diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_ssl.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_ssl.py new file mode 100644 index 00000000..c99c5a67 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_ssl.py @@ -0,0 +1,9 @@ +import ssl + +import certifi + + +def default_ssl_context() -> ssl.SSLContext: + context = ssl.create_default_context() + context.load_verify_locations(certifi.where()) + return context diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__init__.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__init__.py new file mode 100644 index 00000000..b476d76d --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__init__.py @@ -0,0 +1,39 @@ +from .connection import HTTPConnection +from .connection_pool import ConnectionPool +from .http11 import HTTP11Connection +from .http_proxy import HTTPProxy +from .interfaces import ConnectionInterface + +try: + from .http2 import HTTP2Connection +except ImportError: # pragma: nocover + + class HTTP2Connection: # type: ignore + def __init__(self, *args, **kwargs) -> None: # type: ignore + raise RuntimeError( + "Attempted to use http2 support, but the `h2` package is not " + "installed. Use 'pip install httpcore[http2]'." + ) + + +try: + from .socks_proxy import SOCKSProxy +except ImportError: # pragma: nocover + + class SOCKSProxy: # type: ignore + def __init__(self, *args, **kwargs) -> None: # type: ignore + raise RuntimeError( + "Attempted to use SOCKS support, but the `socksio` package is not " + "installed. Use 'pip install httpcore[socks]'." + ) + + +__all__ = [ + "HTTPConnection", + "ConnectionPool", + "HTTPProxy", + "HTTP11Connection", + "HTTP2Connection", + "ConnectionInterface", + "SOCKSProxy", +] diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fffafcf071298c69432df7bd72fd785fb230e726 GIT binary patch literal 1579 zcmbVM&5zqe6n|qoaXy{4Y0Dxga-r%fDjO@|iV)j^Xoa+_XnUv;@}~AQv0{77c+%bM z3D6#U-)q%Nwd#!%*Pi(gEOO$+EozWJ%Y}K9FKG)0YAHXzZ@;|voB5?)uYi0uJMV`h z1K>AQmQP_J%zl=_F$55h2x8(Ag0dEAvF_?h)*~Y>xCJE}Q86}MQ^|#>6qnsHfih+k zqe@(Ls|&3h)nm)G2=pPSJtJ;oT>k)Q?Hcep0S&%@z&wJW^g^3=;Z`5OlPmjhpg(|p zawf7OBde=OOGe&UMz*ZqiLB5fxpn*Az1>?ZNvJPEmY`*=?J^cg%Q=kcbk=rhE2C|f zvxBiznrqjcL{R>y=Tm8JU`%)I^B1F2uf;!r*+r=y0|l1^;A(*$Xh(*tk4ekmCF$Dn zvD4+FOQ9iuvkeKyAXdJByjaLTl9sk}7;Xze<5bYV7R(-H)E)?tcI|AGri_aZ?S~^_ zivhLw2Hib7?fJuApW0z&CrsF3l8Igv(V%VLmzkSsm@aLqjL$gzM%DXvvwcGH+Sj8* zgfab`bH-2f+{kG3C^ve%pXJ5jKE5;T+_vr0z!|WZI{oCp`HW^m!BXco6Exbo$=EPV z`p!++3rzy&F-;yjQTWhF$6~;ekJ_EClZAqAEt<_7>;PMFylkBKj^~9*C_Jy7j`Pw| zIovq2g!a-?!!cfx@ICx`;k~Iw&VNc~y56*=px3RLbij$07daIpp6r|$STpIx)bsL+ z=fx}-MJU%i@4HbintS*K%mR)(<>wJ_g$k-tn3)oe;1A6xTk{xnOQuxres$~1yNXW# znHyd}f%lxKSqraB>H0@o*SV*=MEa8(LZqfD%caeaDOy*FQ7E>*Q3 z%)h2vP-?yclURQmg@PovglR#le@{35!*iP1@*_`+U3gyHHsPnnwxPe&38LB9ep`RJ HK_vbInf-`K literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/connection.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/connection.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..85f0649f83a8faeecd3886dca39c1d589105cafd GIT binary patch literal 10493 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 auU)U*{uG}_@wLy6Ih*(cPdG$ussArHFG}qI literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/http11.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/http11.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..15e337418b241e0e81569d339e5aa4e6e3674c8e GIT binary patch literal 15831 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/http2.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/http2.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bedddcbeb12be2de06c2931e1e6e454e3b61ff6c GIT binary patch literal 27866 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 E|8Xl(vH$=8 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/interfaces.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/interfaces.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..776217968a33a87131ae7d3b9e0ddf76811991b3 GIT binary patch literal 5411 zcmdT|-ES1v6~D8yJG0-mcMUclh5@_465|ygZCa8h!G#3W#I(e;%Bs>b-nsS;%*S$P z*7jPaLXj&uRn_{bxKbZ@#DYrv38Alj!3u4y5mh6#56w%p&AJLgedsxNX4dQ26p~bx zI`*D9_hZgE_uSt(_ssk*lSvY|4j(_LXL<b9jCUhJ|3%qJ+fko3gMm)`c0=Vo1MIneERsq<9TC>w9osN+c&nLedko_vX3 zuT#hM;+LpXgK5;uP;1g=6ASdjoGsElCt%_0CXqowP%E7FPzYL#t z1&!8YU~UmgN`gj85l!HKkzb3NNJSc@vB}6Tn4v|XCGob%+c7F@5}y&LQQjj=`Xe$; z&?N7P)6`^IODssGG|l{%C4+3#0rHaz(%oP&rL2~sU9e}V$*7iIh?cq=LLuXI2M)ae z8^$Kp3LS&pl0)Cgd?PK_OTuo38zY(2t=gI=f;&RTjJkDZRRUNLyvy*z zduyM7?sS3r*E~4%UL*w84Z#hcBJ&YV>==o-AFb(u_9ry(ohS@nRLjcIZmni#pVpe5PPe4kE^jvmo-EI7n53>*-{%YMZn; zWz81PQsCO=?9oN>aqhtF!}k-9a;F#Nb&2GU-Z^~V`S7hrxwjYPzvgzW<#(-h z=hu4o;D6tN^^}xPF3Qa;>Fxhx{?Kav(CwLz^2Nn?Ga@Di*3!Mdc;|z6es=L=V8%We zdz9Y0*44Mz^`t41%nt1NeR}Ux2RGnBfA^WwkLa0AzOlPau=q{nN1kE&z@vi{}HF{L9$~mb`zaZ!{`A*d0AnBoFrWAYaUl z3gT}C5qOUON^lm#YywAgkX|tXcc#}c3B50@2xBXPmmjm;3ueurCWVO7&M{^)NGk{6 zF)r0XNs7W9$2~i@e~Z^Pr;zW%11(IUH_7Adfse9-595O^Xp%yY7cZAFgO$r(vRpQ8 ztqz%|?4`@)>vh%eN0JL8A65|$?%L|hmc@QaXVKC z8%7>?oM8swBnOPs1rfDCnrWg8t-bup+DpYxy9Q$YA9wFqqRa0tU0v-y(2PMxQzF^? z(#hr6va|I1YG$x0V`q}&2AV14(j?cnR9~*%uK&uu<38*=x!QB8nZc1P*)gzOTJ1g3 zM8VB&lFYSwkb8|ty_@vzjSXzp3mseaVyCYcx$X7hNQYi*qTZz=tBF0I_CT{GqVfn1 zyeh?F$`_SL+Ipj-n@}iBK$vnn0tI5iJUQOp8$o+AK1#7`}?ZL2Y8LI@xFYqsfZgvFb zu>z14fuq?$0RfINIn3?aM%M^G_kMtnzJvr3^yM|PZ@}XndcCL)Rq0&+c8YQ2y-j*O z$>!zl7me$Um4|WsEi|Jej=fw(waS~LcJ9!2R&Aa4qfIt`QQs5<^$%c3GAs}-@xoBK zGS2GsilR>{Zj~yPHU<>kQ7U!DAT=2cMV&CH;@XN@snD9Ms8(ByU3aH!C?}NQz#mDr zht62a`SJ1Z6_1Y`?-V?v8-`+AAa#N&>a?mGTsl~wyPLJ@P#tX}@5B~qn*=$wV%T7i z>hLA`=ZbRStl~gHLqS8^K+ATO26YuWTUF~0q~Kv#nYN81aH4L(ph7W8D~_wWotFKu zs-u6lIB%iSm+0~6ub=#L<0OvdX zFrtia*6vj8x}kxKH{`*=3u{Eb_;$5@KdemDUB%R=s;&b1bQ9*m_ApgD}_?v&e$oKrSd(N1t9oioZ9t9Ybc4k|n|Y8{awxna z2b$tvc{Lxt@M>^}CMdPGqUJJgJ-`dG7QQp`go`hn018{tD13Ad!#{N_5*zc=EMPr4$m#p|- z=-@q^SJa;5HfnHqbkyCvIG3?>r-tKK=DZZ= z%`1Kyf7O2-c#T~EDOjsHF9T_cf*|~feC^LeM1n2{y~tgzZp3p P3_aX^^q&Ood|m$rv|1cL literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/socks_proxy.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/socks_proxy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..edfd31efbb9a413e51b2d990993c1480bf602256 GIT binary patch literal 15912 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 Iterator[float]: + yield 0 + for n in itertools.count(2): + yield factor * (2 ** (n - 2)) + + +class HTTPConnection(ConnectionInterface): + def __init__( + self, + origin: Origin, + ssl_context: Optional[ssl.SSLContext] = None, + keepalive_expiry: Optional[float] = None, + http1: bool = True, + http2: bool = False, + retries: int = 0, + local_address: Optional[str] = None, + uds: Optional[str] = None, + network_backend: Optional[NetworkBackend] = None, + socket_options: Optional[Iterable[SOCKET_OPTION]] = None, + ) -> None: + self._origin = origin + self._ssl_context = ssl_context + self._keepalive_expiry = keepalive_expiry + self._http1 = http1 + self._http2 = http2 + self._retries = retries + self._local_address = local_address + self._uds = uds + + self._network_backend: NetworkBackend = ( + SyncBackend() if network_backend is None else network_backend + ) + self._connection: Optional[ConnectionInterface] = None + self._connect_failed: bool = False + self._request_lock = Lock() + self._socket_options = socket_options + + def handle_request(self, request: Request) -> Response: + if not self.can_handle_request(request.url.origin): + raise RuntimeError( + f"Attempted to send request to {request.url.origin} on connection to {self._origin}" + ) + + with self._request_lock: + if self._connection is None: + try: + stream = self._connect(request) + + ssl_object = stream.get_extra_info("ssl_object") + http2_negotiated = ( + ssl_object is not None + and ssl_object.selected_alpn_protocol() == "h2" + ) + if http2_negotiated or (self._http2 and not self._http1): + from .http2 import HTTP2Connection + + self._connection = HTTP2Connection( + origin=self._origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + else: + self._connection = HTTP11Connection( + origin=self._origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + except Exception as exc: + self._connect_failed = True + raise exc + elif not self._connection.is_available(): + raise ConnectionNotAvailable() + + return self._connection.handle_request(request) + + def _connect(self, request: Request) -> NetworkStream: + timeouts = request.extensions.get("timeout", {}) + sni_hostname = request.extensions.get("sni_hostname", None) + timeout = timeouts.get("connect", None) + + retries_left = self._retries + delays = exponential_backoff(factor=RETRIES_BACKOFF_FACTOR) + + while True: + try: + if self._uds is None: + kwargs = { + "host": self._origin.host.decode("ascii"), + "port": self._origin.port, + "local_address": self._local_address, + "timeout": timeout, + "socket_options": self._socket_options, + } + with Trace("connect_tcp", logger, request, kwargs) as trace: + stream = self._network_backend.connect_tcp(**kwargs) + trace.return_value = stream + else: + kwargs = { + "path": self._uds, + "timeout": timeout, + "socket_options": self._socket_options, + } + with Trace( + "connect_unix_socket", logger, request, kwargs + ) as trace: + stream = self._network_backend.connect_unix_socket( + **kwargs + ) + trace.return_value = stream + + if self._origin.scheme == b"https": + ssl_context = ( + default_ssl_context() + if self._ssl_context is None + else self._ssl_context + ) + alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"] + ssl_context.set_alpn_protocols(alpn_protocols) + + kwargs = { + "ssl_context": ssl_context, + "server_hostname": sni_hostname + or self._origin.host.decode("ascii"), + "timeout": timeout, + } + with Trace("start_tls", logger, request, kwargs) as trace: + stream = stream.start_tls(**kwargs) + trace.return_value = stream + return stream + except (ConnectError, ConnectTimeout): + if retries_left <= 0: + raise + retries_left -= 1 + delay = next(delays) + with Trace("retry", logger, request, kwargs) as trace: + self._network_backend.sleep(delay) + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._origin + + def close(self) -> None: + if self._connection is not None: + with Trace("close", logger, None, {}): + self._connection.close() + + def is_available(self) -> bool: + if self._connection is None: + # If HTTP/2 support is enabled, and the resulting connection could + # end up as HTTP/2 then we should indicate the connection as being + # available to service multiple requests. + return ( + self._http2 + and (self._origin.scheme == b"https" or not self._http1) + and not self._connect_failed + ) + return self._connection.is_available() + + def has_expired(self) -> bool: + if self._connection is None: + return self._connect_failed + return self._connection.has_expired() + + def is_idle(self) -> bool: + if self._connection is None: + return self._connect_failed + return self._connection.is_idle() + + def is_closed(self) -> bool: + if self._connection is None: + return self._connect_failed + return self._connection.is_closed() + + def info(self) -> str: + if self._connection is None: + return "CONNECTION FAILED" if self._connect_failed else "CONNECTING" + return self._connection.info() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.info()}]>" + + # These context managers are not used in the standard flow, but are + # useful for testing or working with connection instances directly. + + def __enter__(self) -> "HTTPConnection": + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + self.close() diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/connection_pool.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/connection_pool.py new file mode 100644 index 00000000..dbcaff1f --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/connection_pool.py @@ -0,0 +1,356 @@ +import ssl +import sys +from types import TracebackType +from typing import Iterable, Iterator, Iterable, List, Optional, Type + +from .._backends.sync import SyncBackend +from .._backends.base import SOCKET_OPTION, NetworkBackend +from .._exceptions import ConnectionNotAvailable, UnsupportedProtocol +from .._models import Origin, Request, Response +from .._synchronization import Event, Lock, ShieldCancellation +from .connection import HTTPConnection +from .interfaces import ConnectionInterface, RequestInterface + + +class RequestStatus: + def __init__(self, request: Request): + self.request = request + self.connection: Optional[ConnectionInterface] = None + self._connection_acquired = Event() + + def set_connection(self, connection: ConnectionInterface) -> None: + assert self.connection is None + self.connection = connection + self._connection_acquired.set() + + def unset_connection(self) -> None: + assert self.connection is not None + self.connection = None + self._connection_acquired = Event() + + def wait_for_connection( + self, timeout: Optional[float] = None + ) -> ConnectionInterface: + if self.connection is None: + self._connection_acquired.wait(timeout=timeout) + assert self.connection is not None + return self.connection + + +class ConnectionPool(RequestInterface): + """ + A connection pool for making HTTP requests. + """ + + def __init__( + self, + ssl_context: Optional[ssl.SSLContext] = None, + max_connections: Optional[int] = 10, + max_keepalive_connections: Optional[int] = None, + keepalive_expiry: Optional[float] = None, + http1: bool = True, + http2: bool = False, + retries: int = 0, + local_address: Optional[str] = None, + uds: Optional[str] = None, + network_backend: Optional[NetworkBackend] = None, + socket_options: Optional[Iterable[SOCKET_OPTION]] = None, + ) -> None: + """ + A connection pool for making HTTP requests. + + Parameters: + ssl_context: An SSL context to use for verifying connections. + If not specified, the default `httpcore.default_ssl_context()` + will be used. + max_connections: The maximum number of concurrent HTTP connections that + the pool should allow. Any attempt to send a request on a pool that + would exceed this amount will block until a connection is available. + max_keepalive_connections: The maximum number of idle HTTP connections + that will be maintained in the pool. + keepalive_expiry: The duration in seconds that an idle HTTP connection + may be maintained for before being expired from the pool. + http1: A boolean indicating if HTTP/1.1 requests should be supported + by the connection pool. Defaults to True. + http2: A boolean indicating if HTTP/2 requests should be supported by + the connection pool. Defaults to False. + retries: The maximum number of retries when trying to establish a + connection. + local_address: Local address to connect from. Can also be used to connect + using a particular address family. Using `local_address="0.0.0.0"` + will connect using an `AF_INET` address (IPv4), while using + `local_address="::"` will connect using an `AF_INET6` address (IPv6). + uds: Path to a Unix Domain Socket to use instead of TCP sockets. + network_backend: A backend instance to use for handling network I/O. + socket_options: Socket options that have to be included + in the TCP socket when the connection was established. + """ + self._ssl_context = ssl_context + + self._max_connections = ( + sys.maxsize if max_connections is None else max_connections + ) + self._max_keepalive_connections = ( + sys.maxsize + if max_keepalive_connections is None + else max_keepalive_connections + ) + self._max_keepalive_connections = min( + self._max_connections, self._max_keepalive_connections + ) + + self._keepalive_expiry = keepalive_expiry + self._http1 = http1 + self._http2 = http2 + self._retries = retries + self._local_address = local_address + self._uds = uds + + self._pool: List[ConnectionInterface] = [] + self._requests: List[RequestStatus] = [] + self._pool_lock = Lock() + self._network_backend = ( + SyncBackend() if network_backend is None else network_backend + ) + self._socket_options = socket_options + + def create_connection(self, origin: Origin) -> ConnectionInterface: + return HTTPConnection( + origin=origin, + ssl_context=self._ssl_context, + keepalive_expiry=self._keepalive_expiry, + http1=self._http1, + http2=self._http2, + retries=self._retries, + local_address=self._local_address, + uds=self._uds, + network_backend=self._network_backend, + socket_options=self._socket_options, + ) + + @property + def connections(self) -> List[ConnectionInterface]: + """ + Return a list of the connections currently in the pool. + + For example: + + ```python + >>> pool.connections + [ + , + , + , + ] + ``` + """ + return list(self._pool) + + def _attempt_to_acquire_connection(self, status: RequestStatus) -> bool: + """ + Attempt to provide a connection that can handle the given origin. + """ + origin = status.request.url.origin + + # If there are queued requests in front of us, then don't acquire a + # connection. We handle requests strictly in order. + waiting = [s for s in self._requests if s.connection is None] + if waiting and waiting[0] is not status: + return False + + # Reuse an existing connection if one is currently available. + for idx, connection in enumerate(self._pool): + if connection.can_handle_request(origin) and connection.is_available(): + self._pool.pop(idx) + self._pool.insert(0, connection) + status.set_connection(connection) + return True + + # If the pool is currently full, attempt to close one idle connection. + if len(self._pool) >= self._max_connections: + for idx, connection in reversed(list(enumerate(self._pool))): + if connection.is_idle(): + connection.close() + self._pool.pop(idx) + break + + # If the pool is still full, then we cannot acquire a connection. + if len(self._pool) >= self._max_connections: + return False + + # Otherwise create a new connection. + connection = self.create_connection(origin) + self._pool.insert(0, connection) + status.set_connection(connection) + return True + + def _close_expired_connections(self) -> None: + """ + Clean up the connection pool by closing off any connections that have expired. + """ + # Close any connections that have expired their keep-alive time. + for idx, connection in reversed(list(enumerate(self._pool))): + if connection.has_expired(): + connection.close() + self._pool.pop(idx) + + # If the pool size exceeds the maximum number of allowed keep-alive connections, + # then close off idle connections as required. + pool_size = len(self._pool) + for idx, connection in reversed(list(enumerate(self._pool))): + if connection.is_idle() and pool_size > self._max_keepalive_connections: + connection.close() + self._pool.pop(idx) + pool_size -= 1 + + def handle_request(self, request: Request) -> Response: + """ + Send an HTTP request, and return an HTTP response. + + This is the core implementation that is called into by `.request()` or `.stream()`. + """ + scheme = request.url.scheme.decode() + if scheme == "": + raise UnsupportedProtocol( + "Request URL is missing an 'http://' or 'https://' protocol." + ) + if scheme not in ("http", "https", "ws", "wss"): + raise UnsupportedProtocol( + f"Request URL has an unsupported protocol '{scheme}://'." + ) + + status = RequestStatus(request) + + with self._pool_lock: + self._requests.append(status) + self._close_expired_connections() + self._attempt_to_acquire_connection(status) + + while True: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("pool", None) + try: + connection = status.wait_for_connection(timeout=timeout) + except BaseException as exc: + # If we timeout here, or if the task is cancelled, then make + # sure to remove the request from the queue before bubbling + # up the exception. + with self._pool_lock: + # Ensure only remove when task exists. + if status in self._requests: + self._requests.remove(status) + raise exc + + try: + response = connection.handle_request(request) + except ConnectionNotAvailable: + # The ConnectionNotAvailable exception is a special case, that + # indicates we need to retry the request on a new connection. + # + # The most common case where this can occur is when multiple + # requests are queued waiting for a single connection, which + # might end up as an HTTP/2 connection, but which actually ends + # up as HTTP/1.1. + with self._pool_lock: + # Maintain our position in the request queue, but reset the + # status so that the request becomes queued again. + status.unset_connection() + self._attempt_to_acquire_connection(status) + except BaseException as exc: + with ShieldCancellation(): + self.response_closed(status) + raise exc + else: + break + + # When we return the response, we wrap the stream in a special class + # that handles notifying the connection pool once the response + # has been released. + assert isinstance(response.stream, Iterable) + return Response( + status=response.status, + headers=response.headers, + content=ConnectionPoolByteStream(response.stream, self, status), + extensions=response.extensions, + ) + + def response_closed(self, status: RequestStatus) -> None: + """ + This method acts as a callback once the request/response cycle is complete. + + It is called into from the `ConnectionPoolByteStream.close()` method. + """ + assert status.connection is not None + connection = status.connection + + with self._pool_lock: + # Update the state of the connection pool. + if status in self._requests: + self._requests.remove(status) + + if connection.is_closed() and connection in self._pool: + self._pool.remove(connection) + + # Since we've had a response closed, it's possible we'll now be able + # to service one or more requests that are currently pending. + for status in self._requests: + if status.connection is None: + acquired = self._attempt_to_acquire_connection(status) + # If we could not acquire a connection for a queued request + # then we don't need to check anymore requests that are + # queued later behind it. + if not acquired: + break + + # Housekeeping. + self._close_expired_connections() + + def close(self) -> None: + """ + Close any connections in the pool. + """ + with self._pool_lock: + for connection in self._pool: + connection.close() + self._pool = [] + self._requests = [] + + def __enter__(self) -> "ConnectionPool": + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + self.close() + + +class ConnectionPoolByteStream: + """ + A wrapper around the response byte stream, that additionally handles + notifying the connection pool when the response has been closed. + """ + + def __init__( + self, + stream: Iterable[bytes], + pool: ConnectionPool, + status: RequestStatus, + ) -> None: + self._stream = stream + self._pool = pool + self._status = status + + def __iter__(self) -> Iterator[bytes]: + for part in self._stream: + yield part + + def close(self) -> None: + try: + if hasattr(self._stream, "close"): + self._stream.close() + finally: + with ShieldCancellation(): + self._pool.response_closed(self._status) diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/http11.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/http11.py new file mode 100644 index 00000000..edcce72a --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/http11.py @@ -0,0 +1,331 @@ +import enum +import logging +import time +from types import TracebackType +from typing import ( + Iterable, + Iterator, + List, + Optional, + Tuple, + Type, + Union, + cast, +) + +import h11 + +from .._backends.base import NetworkStream +from .._exceptions import ( + ConnectionNotAvailable, + LocalProtocolError, + RemoteProtocolError, + map_exceptions, +) +from .._models import Origin, Request, Response +from .._synchronization import Lock, ShieldCancellation +from .._trace import Trace +from .interfaces import ConnectionInterface + +logger = logging.getLogger("httpcore.http11") + + +# A subset of `h11.Event` types supported by `_send_event` +H11SendEvent = Union[ + h11.Request, + h11.Data, + h11.EndOfMessage, +] + + +class HTTPConnectionState(enum.IntEnum): + NEW = 0 + ACTIVE = 1 + IDLE = 2 + CLOSED = 3 + + +class HTTP11Connection(ConnectionInterface): + READ_NUM_BYTES = 64 * 1024 + MAX_INCOMPLETE_EVENT_SIZE = 100 * 1024 + + def __init__( + self, + origin: Origin, + stream: NetworkStream, + keepalive_expiry: Optional[float] = None, + ) -> None: + self._origin = origin + self._network_stream = stream + self._keepalive_expiry: Optional[float] = keepalive_expiry + self._expire_at: Optional[float] = None + self._state = HTTPConnectionState.NEW + self._state_lock = Lock() + self._request_count = 0 + self._h11_state = h11.Connection( + our_role=h11.CLIENT, + max_incomplete_event_size=self.MAX_INCOMPLETE_EVENT_SIZE, + ) + + def handle_request(self, request: Request) -> Response: + if not self.can_handle_request(request.url.origin): + raise RuntimeError( + f"Attempted to send request to {request.url.origin} on connection " + f"to {self._origin}" + ) + + with self._state_lock: + if self._state in (HTTPConnectionState.NEW, HTTPConnectionState.IDLE): + self._request_count += 1 + self._state = HTTPConnectionState.ACTIVE + self._expire_at = None + else: + raise ConnectionNotAvailable() + + try: + kwargs = {"request": request} + with Trace("send_request_headers", logger, request, kwargs) as trace: + self._send_request_headers(**kwargs) + with Trace("send_request_body", logger, request, kwargs) as trace: + self._send_request_body(**kwargs) + with Trace( + "receive_response_headers", logger, request, kwargs + ) as trace: + ( + http_version, + status, + reason_phrase, + headers, + ) = self._receive_response_headers(**kwargs) + trace.return_value = ( + http_version, + status, + reason_phrase, + headers, + ) + + return Response( + status=status, + headers=headers, + content=HTTP11ConnectionByteStream(self, request), + extensions={ + "http_version": http_version, + "reason_phrase": reason_phrase, + "network_stream": self._network_stream, + }, + ) + except BaseException as exc: + with ShieldCancellation(): + with Trace("response_closed", logger, request) as trace: + self._response_closed() + raise exc + + # Sending the request... + + def _send_request_headers(self, request: Request) -> None: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("write", None) + + with map_exceptions({h11.LocalProtocolError: LocalProtocolError}): + event = h11.Request( + method=request.method, + target=request.url.target, + headers=request.headers, + ) + self._send_event(event, timeout=timeout) + + def _send_request_body(self, request: Request) -> None: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("write", None) + + assert isinstance(request.stream, Iterable) + for chunk in request.stream: + event = h11.Data(data=chunk) + self._send_event(event, timeout=timeout) + + self._send_event(h11.EndOfMessage(), timeout=timeout) + + def _send_event( + self, event: h11.Event, timeout: Optional[float] = None + ) -> None: + bytes_to_send = self._h11_state.send(event) + if bytes_to_send is not None: + self._network_stream.write(bytes_to_send, timeout=timeout) + + # Receiving the response... + + def _receive_response_headers( + self, request: Request + ) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]]]: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("read", None) + + while True: + event = self._receive_event(timeout=timeout) + if isinstance(event, h11.Response): + break + if ( + isinstance(event, h11.InformationalResponse) + and event.status_code == 101 + ): + break + + http_version = b"HTTP/" + event.http_version + + # h11 version 0.11+ supports a `raw_items` interface to get the + # raw header casing, rather than the enforced lowercase headers. + headers = event.headers.raw_items() + + return http_version, event.status_code, event.reason, headers + + def _receive_response_body(self, request: Request) -> Iterator[bytes]: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("read", None) + + while True: + event = self._receive_event(timeout=timeout) + if isinstance(event, h11.Data): + yield bytes(event.data) + elif isinstance(event, (h11.EndOfMessage, h11.PAUSED)): + break + + def _receive_event( + self, timeout: Optional[float] = None + ) -> Union[h11.Event, Type[h11.PAUSED]]: + while True: + with map_exceptions({h11.RemoteProtocolError: RemoteProtocolError}): + event = self._h11_state.next_event() + + if event is h11.NEED_DATA: + data = self._network_stream.read( + self.READ_NUM_BYTES, timeout=timeout + ) + + # If we feed this case through h11 we'll raise an exception like: + # + # httpcore.RemoteProtocolError: can't handle event type + # ConnectionClosed when role=SERVER and state=SEND_RESPONSE + # + # Which is accurate, but not very informative from an end-user + # perspective. Instead we handle this case distinctly and treat + # it as a ConnectError. + if data == b"" and self._h11_state.their_state == h11.SEND_RESPONSE: + msg = "Server disconnected without sending a response." + raise RemoteProtocolError(msg) + + self._h11_state.receive_data(data) + else: + # mypy fails to narrow the type in the above if statement above + return cast(Union[h11.Event, Type[h11.PAUSED]], event) + + def _response_closed(self) -> None: + with self._state_lock: + if ( + self._h11_state.our_state is h11.DONE + and self._h11_state.their_state is h11.DONE + ): + self._state = HTTPConnectionState.IDLE + self._h11_state.start_next_cycle() + if self._keepalive_expiry is not None: + now = time.monotonic() + self._expire_at = now + self._keepalive_expiry + else: + self.close() + + # Once the connection is no longer required... + + def close(self) -> None: + # Note that this method unilaterally closes the connection, and does + # not have any kind of locking in place around it. + self._state = HTTPConnectionState.CLOSED + self._network_stream.close() + + # The ConnectionInterface methods provide information about the state of + # the connection, allowing for a connection pooling implementation to + # determine when to reuse and when to close the connection... + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._origin + + def is_available(self) -> bool: + # Note that HTTP/1.1 connections in the "NEW" state are not treated as + # being "available". The control flow which created the connection will + # be able to send an outgoing request, but the connection will not be + # acquired from the connection pool for any other request. + return self._state == HTTPConnectionState.IDLE + + def has_expired(self) -> bool: + now = time.monotonic() + keepalive_expired = self._expire_at is not None and now > self._expire_at + + # If the HTTP connection is idle but the socket is readable, then the + # only valid state is that the socket is about to return b"", indicating + # a server-initiated disconnect. + server_disconnected = ( + self._state == HTTPConnectionState.IDLE + and self._network_stream.get_extra_info("is_readable") + ) + + return keepalive_expired or server_disconnected + + def is_idle(self) -> bool: + return self._state == HTTPConnectionState.IDLE + + def is_closed(self) -> bool: + return self._state == HTTPConnectionState.CLOSED + + def info(self) -> str: + origin = str(self._origin) + return ( + f"{origin!r}, HTTP/1.1, {self._state.name}, " + f"Request Count: {self._request_count}" + ) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + origin = str(self._origin) + return ( + f"<{class_name} [{origin!r}, {self._state.name}, " + f"Request Count: {self._request_count}]>" + ) + + # These context managers are not used in the standard flow, but are + # useful for testing or working with connection instances directly. + + def __enter__(self) -> "HTTP11Connection": + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + self.close() + + +class HTTP11ConnectionByteStream: + def __init__(self, connection: HTTP11Connection, request: Request) -> None: + self._connection = connection + self._request = request + self._closed = False + + def __iter__(self) -> Iterator[bytes]: + kwargs = {"request": self._request} + try: + with Trace("receive_response_body", logger, self._request, kwargs): + for chunk in self._connection._receive_response_body(**kwargs): + yield chunk + except BaseException as exc: + # If we get an exception while streaming the response, + # we want to close the response (and possibly the connection) + # before raising that exception. + with ShieldCancellation(): + self.close() + raise exc + + def close(self) -> None: + if not self._closed: + self._closed = True + with Trace("response_closed", logger, self._request): + self._connection._response_closed() diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/http2.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/http2.py new file mode 100644 index 00000000..d141d459 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/http2.py @@ -0,0 +1,589 @@ +import enum +import logging +import time +import types +import typing + +import h2.config +import h2.connection +import h2.events +import h2.exceptions +import h2.settings + +from .._backends.base import NetworkStream +from .._exceptions import ( + ConnectionNotAvailable, + LocalProtocolError, + RemoteProtocolError, +) +from .._models import Origin, Request, Response +from .._synchronization import Lock, Semaphore, ShieldCancellation +from .._trace import Trace +from .interfaces import ConnectionInterface + +logger = logging.getLogger("httpcore.http2") + + +def has_body_headers(request: Request) -> bool: + return any( + k.lower() == b"content-length" or k.lower() == b"transfer-encoding" + for k, v in request.headers + ) + + +class HTTPConnectionState(enum.IntEnum): + ACTIVE = 1 + IDLE = 2 + CLOSED = 3 + + +class HTTP2Connection(ConnectionInterface): + READ_NUM_BYTES = 64 * 1024 + CONFIG = h2.config.H2Configuration(validate_inbound_headers=False) + + def __init__( + self, + origin: Origin, + stream: NetworkStream, + keepalive_expiry: typing.Optional[float] = None, + ): + self._origin = origin + self._network_stream = stream + self._keepalive_expiry: typing.Optional[float] = keepalive_expiry + self._h2_state = h2.connection.H2Connection(config=self.CONFIG) + self._state = HTTPConnectionState.IDLE + self._expire_at: typing.Optional[float] = None + self._request_count = 0 + self._init_lock = Lock() + self._state_lock = Lock() + self._read_lock = Lock() + self._write_lock = Lock() + self._sent_connection_init = False + self._used_all_stream_ids = False + self._connection_error = False + + # Mapping from stream ID to response stream events. + self._events: typing.Dict[ + int, + typing.Union[ + h2.events.ResponseReceived, + h2.events.DataReceived, + h2.events.StreamEnded, + h2.events.StreamReset, + ], + ] = {} + + # Connection terminated events are stored as state since + # we need to handle them for all streams. + self._connection_terminated: typing.Optional[ + h2.events.ConnectionTerminated + ] = None + + self._read_exception: typing.Optional[Exception] = None + self._write_exception: typing.Optional[Exception] = None + + def handle_request(self, request: Request) -> Response: + if not self.can_handle_request(request.url.origin): + # This cannot occur in normal operation, since the connection pool + # will only send requests on connections that handle them. + # It's in place simply for resilience as a guard against incorrect + # usage, for anyone working directly with httpcore connections. + raise RuntimeError( + f"Attempted to send request to {request.url.origin} on connection " + f"to {self._origin}" + ) + + with self._state_lock: + if self._state in (HTTPConnectionState.ACTIVE, HTTPConnectionState.IDLE): + self._request_count += 1 + self._expire_at = None + self._state = HTTPConnectionState.ACTIVE + else: + raise ConnectionNotAvailable() + + with self._init_lock: + if not self._sent_connection_init: + try: + kwargs = {"request": request} + with Trace("send_connection_init", logger, request, kwargs): + self._send_connection_init(**kwargs) + except BaseException as exc: + with ShieldCancellation(): + self.close() + raise exc + + self._sent_connection_init = True + + # Initially start with just 1 until the remote server provides + # its max_concurrent_streams value + self._max_streams = 1 + + local_settings_max_streams = ( + self._h2_state.local_settings.max_concurrent_streams + ) + self._max_streams_semaphore = Semaphore(local_settings_max_streams) + + for _ in range(local_settings_max_streams - self._max_streams): + self._max_streams_semaphore.acquire() + + self._max_streams_semaphore.acquire() + + try: + stream_id = self._h2_state.get_next_available_stream_id() + self._events[stream_id] = [] + except h2.exceptions.NoAvailableStreamIDError: # pragma: nocover + self._used_all_stream_ids = True + self._request_count -= 1 + raise ConnectionNotAvailable() + + try: + kwargs = {"request": request, "stream_id": stream_id} + with Trace("send_request_headers", logger, request, kwargs): + self._send_request_headers(request=request, stream_id=stream_id) + with Trace("send_request_body", logger, request, kwargs): + self._send_request_body(request=request, stream_id=stream_id) + with Trace( + "receive_response_headers", logger, request, kwargs + ) as trace: + status, headers = self._receive_response( + request=request, stream_id=stream_id + ) + trace.return_value = (status, headers) + + return Response( + status=status, + headers=headers, + content=HTTP2ConnectionByteStream(self, request, stream_id=stream_id), + extensions={ + "http_version": b"HTTP/2", + "network_stream": self._network_stream, + "stream_id": stream_id, + }, + ) + except BaseException as exc: # noqa: PIE786 + with ShieldCancellation(): + kwargs = {"stream_id": stream_id} + with Trace("response_closed", logger, request, kwargs): + self._response_closed(stream_id=stream_id) + + if isinstance(exc, h2.exceptions.ProtocolError): + # One case where h2 can raise a protocol error is when a + # closed frame has been seen by the state machine. + # + # This happens when one stream is reading, and encounters + # a GOAWAY event. Other flows of control may then raise + # a protocol error at any point they interact with the 'h2_state'. + # + # In this case we'll have stored the event, and should raise + # it as a RemoteProtocolError. + if self._connection_terminated: # pragma: nocover + raise RemoteProtocolError(self._connection_terminated) + # If h2 raises a protocol error in some other state then we + # must somehow have made a protocol violation. + raise LocalProtocolError(exc) # pragma: nocover + + raise exc + + def _send_connection_init(self, request: Request) -> None: + """ + The HTTP/2 connection requires some initial setup before we can start + using individual request/response streams on it. + """ + # Need to set these manually here instead of manipulating via + # __setitem__() otherwise the H2Connection will emit SettingsUpdate + # frames in addition to sending the undesired defaults. + self._h2_state.local_settings = h2.settings.Settings( + client=True, + initial_values={ + # Disable PUSH_PROMISE frames from the server since we don't do anything + # with them for now. Maybe when we support caching? + h2.settings.SettingCodes.ENABLE_PUSH: 0, + # These two are taken from h2 for safe defaults + h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 100, + h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: 65536, + }, + ) + + # Some websites (*cough* Yahoo *cough*) balk at this setting being + # present in the initial handshake since it's not defined in the original + # RFC despite the RFC mandating ignoring settings you don't know about. + del self._h2_state.local_settings[ + h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL + ] + + self._h2_state.initiate_connection() + self._h2_state.increment_flow_control_window(2**24) + self._write_outgoing_data(request) + + # Sending the request... + + def _send_request_headers(self, request: Request, stream_id: int) -> None: + """ + Send the request headers to a given stream ID. + """ + end_stream = not has_body_headers(request) + + # In HTTP/2 the ':authority' pseudo-header is used instead of 'Host'. + # In order to gracefully handle HTTP/1.1 and HTTP/2 we always require + # HTTP/1.1 style headers, and map them appropriately if we end up on + # an HTTP/2 connection. + authority = [v for k, v in request.headers if k.lower() == b"host"][0] + + headers = [ + (b":method", request.method), + (b":authority", authority), + (b":scheme", request.url.scheme), + (b":path", request.url.target), + ] + [ + (k.lower(), v) + for k, v in request.headers + if k.lower() + not in ( + b"host", + b"transfer-encoding", + ) + ] + + self._h2_state.send_headers(stream_id, headers, end_stream=end_stream) + self._h2_state.increment_flow_control_window(2**24, stream_id=stream_id) + self._write_outgoing_data(request) + + def _send_request_body(self, request: Request, stream_id: int) -> None: + """ + Iterate over the request body sending it to a given stream ID. + """ + if not has_body_headers(request): + return + + assert isinstance(request.stream, typing.Iterable) + for data in request.stream: + self._send_stream_data(request, stream_id, data) + self._send_end_stream(request, stream_id) + + def _send_stream_data( + self, request: Request, stream_id: int, data: bytes + ) -> None: + """ + Send a single chunk of data in one or more data frames. + """ + while data: + max_flow = self._wait_for_outgoing_flow(request, stream_id) + chunk_size = min(len(data), max_flow) + chunk, data = data[:chunk_size], data[chunk_size:] + self._h2_state.send_data(stream_id, chunk) + self._write_outgoing_data(request) + + def _send_end_stream(self, request: Request, stream_id: int) -> None: + """ + Send an empty data frame on on a given stream ID with the END_STREAM flag set. + """ + self._h2_state.end_stream(stream_id) + self._write_outgoing_data(request) + + # Receiving the response... + + def _receive_response( + self, request: Request, stream_id: int + ) -> typing.Tuple[int, typing.List[typing.Tuple[bytes, bytes]]]: + """ + Return the response status code and headers for a given stream ID. + """ + while True: + event = self._receive_stream_event(request, stream_id) + if isinstance(event, h2.events.ResponseReceived): + break + + status_code = 200 + headers = [] + for k, v in event.headers: + if k == b":status": + status_code = int(v.decode("ascii", errors="ignore")) + elif not k.startswith(b":"): + headers.append((k, v)) + + return (status_code, headers) + + def _receive_response_body( + self, request: Request, stream_id: int + ) -> typing.Iterator[bytes]: + """ + Iterator that returns the bytes of the response body for a given stream ID. + """ + while True: + event = self._receive_stream_event(request, stream_id) + if isinstance(event, h2.events.DataReceived): + amount = event.flow_controlled_length + self._h2_state.acknowledge_received_data(amount, stream_id) + self._write_outgoing_data(request) + yield event.data + elif isinstance(event, h2.events.StreamEnded): + break + + def _receive_stream_event( + self, request: Request, stream_id: int + ) -> typing.Union[ + h2.events.ResponseReceived, h2.events.DataReceived, h2.events.StreamEnded + ]: + """ + Return the next available event for a given stream ID. + + Will read more data from the network if required. + """ + while not self._events.get(stream_id): + self._receive_events(request, stream_id) + event = self._events[stream_id].pop(0) + if isinstance(event, h2.events.StreamReset): + raise RemoteProtocolError(event) + return event + + def _receive_events( + self, request: Request, stream_id: typing.Optional[int] = None + ) -> None: + """ + Read some data from the network until we see one or more events + for a given stream ID. + """ + with self._read_lock: + if self._connection_terminated is not None: + last_stream_id = self._connection_terminated.last_stream_id + if stream_id and last_stream_id and stream_id > last_stream_id: + self._request_count -= 1 + raise ConnectionNotAvailable() + raise RemoteProtocolError(self._connection_terminated) + + # This conditional is a bit icky. We don't want to block reading if we've + # actually got an event to return for a given stream. We need to do that + # check *within* the atomic read lock. Though it also need to be optional, + # because when we call it from `_wait_for_outgoing_flow` we *do* want to + # block until we've available flow control, event when we have events + # pending for the stream ID we're attempting to send on. + if stream_id is None or not self._events.get(stream_id): + events = self._read_incoming_data(request) + for event in events: + if isinstance(event, h2.events.RemoteSettingsChanged): + with Trace( + "receive_remote_settings", logger, request + ) as trace: + self._receive_remote_settings_change(event) + trace.return_value = event + + elif isinstance( + event, + ( + h2.events.ResponseReceived, + h2.events.DataReceived, + h2.events.StreamEnded, + h2.events.StreamReset, + ), + ): + if event.stream_id in self._events: + self._events[event.stream_id].append(event) + + elif isinstance(event, h2.events.ConnectionTerminated): + self._connection_terminated = event + + self._write_outgoing_data(request) + + def _receive_remote_settings_change(self, event: h2.events.Event) -> None: + max_concurrent_streams = event.changed_settings.get( + h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS + ) + if max_concurrent_streams: + new_max_streams = min( + max_concurrent_streams.new_value, + self._h2_state.local_settings.max_concurrent_streams, + ) + if new_max_streams and new_max_streams != self._max_streams: + while new_max_streams > self._max_streams: + self._max_streams_semaphore.release() + self._max_streams += 1 + while new_max_streams < self._max_streams: + self._max_streams_semaphore.acquire() + self._max_streams -= 1 + + def _response_closed(self, stream_id: int) -> None: + self._max_streams_semaphore.release() + del self._events[stream_id] + with self._state_lock: + if self._connection_terminated and not self._events: + self.close() + + elif self._state == HTTPConnectionState.ACTIVE and not self._events: + self._state = HTTPConnectionState.IDLE + if self._keepalive_expiry is not None: + now = time.monotonic() + self._expire_at = now + self._keepalive_expiry + if self._used_all_stream_ids: # pragma: nocover + self.close() + + def close(self) -> None: + # Note that this method unilaterally closes the connection, and does + # not have any kind of locking in place around it. + self._h2_state.close_connection() + self._state = HTTPConnectionState.CLOSED + self._network_stream.close() + + # Wrappers around network read/write operations... + + def _read_incoming_data( + self, request: Request + ) -> typing.List[h2.events.Event]: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("read", None) + + if self._read_exception is not None: + raise self._read_exception # pragma: nocover + + try: + data = self._network_stream.read(self.READ_NUM_BYTES, timeout) + if data == b"": + raise RemoteProtocolError("Server disconnected") + except Exception as exc: + # If we get a network error we should: + # + # 1. Save the exception and just raise it immediately on any future reads. + # (For example, this means that a single read timeout or disconnect will + # immediately close all pending streams. Without requiring multiple + # sequential timeouts.) + # 2. Mark the connection as errored, so that we don't accept any other + # incoming requests. + self._read_exception = exc + self._connection_error = True + raise exc + + events: typing.List[h2.events.Event] = self._h2_state.receive_data(data) + + return events + + def _write_outgoing_data(self, request: Request) -> None: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("write", None) + + with self._write_lock: + data_to_send = self._h2_state.data_to_send() + + if self._write_exception is not None: + raise self._write_exception # pragma: nocover + + try: + self._network_stream.write(data_to_send, timeout) + except Exception as exc: # pragma: nocover + # If we get a network error we should: + # + # 1. Save the exception and just raise it immediately on any future write. + # (For example, this means that a single write timeout or disconnect will + # immediately close all pending streams. Without requiring multiple + # sequential timeouts.) + # 2. Mark the connection as errored, so that we don't accept any other + # incoming requests. + self._write_exception = exc + self._connection_error = True + raise exc + + # Flow control... + + def _wait_for_outgoing_flow(self, request: Request, stream_id: int) -> int: + """ + Returns the maximum allowable outgoing flow for a given stream. + + If the allowable flow is zero, then waits on the network until + WindowUpdated frames have increased the flow rate. + https://tools.ietf.org/html/rfc7540#section-6.9 + """ + local_flow: int = self._h2_state.local_flow_control_window(stream_id) + max_frame_size: int = self._h2_state.max_outbound_frame_size + flow = min(local_flow, max_frame_size) + while flow == 0: + self._receive_events(request) + local_flow = self._h2_state.local_flow_control_window(stream_id) + max_frame_size = self._h2_state.max_outbound_frame_size + flow = min(local_flow, max_frame_size) + return flow + + # Interface for connection pooling... + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._origin + + def is_available(self) -> bool: + return ( + self._state != HTTPConnectionState.CLOSED + and not self._connection_error + and not self._used_all_stream_ids + and not ( + self._h2_state.state_machine.state + == h2.connection.ConnectionState.CLOSED + ) + ) + + def has_expired(self) -> bool: + now = time.monotonic() + return self._expire_at is not None and now > self._expire_at + + def is_idle(self) -> bool: + return self._state == HTTPConnectionState.IDLE + + def is_closed(self) -> bool: + return self._state == HTTPConnectionState.CLOSED + + def info(self) -> str: + origin = str(self._origin) + return ( + f"{origin!r}, HTTP/2, {self._state.name}, " + f"Request Count: {self._request_count}" + ) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + origin = str(self._origin) + return ( + f"<{class_name} [{origin!r}, {self._state.name}, " + f"Request Count: {self._request_count}]>" + ) + + # These context managers are not used in the standard flow, but are + # useful for testing or working with connection instances directly. + + def __enter__(self) -> "HTTP2Connection": + return self + + def __exit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]] = None, + exc_value: typing.Optional[BaseException] = None, + traceback: typing.Optional[types.TracebackType] = None, + ) -> None: + self.close() + + +class HTTP2ConnectionByteStream: + def __init__( + self, connection: HTTP2Connection, request: Request, stream_id: int + ) -> None: + self._connection = connection + self._request = request + self._stream_id = stream_id + self._closed = False + + def __iter__(self) -> typing.Iterator[bytes]: + kwargs = {"request": self._request, "stream_id": self._stream_id} + try: + with Trace("receive_response_body", logger, self._request, kwargs): + for chunk in self._connection._receive_response_body( + request=self._request, stream_id=self._stream_id + ): + yield chunk + except BaseException as exc: + # If we get an exception while streaming the response, + # we want to close the response (and possibly the connection) + # before raising that exception. + with ShieldCancellation(): + self.close() + raise exc + + def close(self) -> None: + if not self._closed: + self._closed = True + kwargs = {"stream_id": self._stream_id} + with Trace("response_closed", logger, self._request, kwargs): + self._connection._response_closed(stream_id=self._stream_id) diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/http_proxy.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/http_proxy.py new file mode 100644 index 00000000..bb368dd4 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/http_proxy.py @@ -0,0 +1,350 @@ +import logging +import ssl +from base64 import b64encode +from typing import Iterable, List, Mapping, Optional, Sequence, Tuple, Union + +from .._backends.base import SOCKET_OPTION, NetworkBackend +from .._exceptions import ProxyError +from .._models import ( + URL, + Origin, + Request, + Response, + enforce_bytes, + enforce_headers, + enforce_url, +) +from .._ssl import default_ssl_context +from .._synchronization import Lock +from .._trace import Trace +from .connection import HTTPConnection +from .connection_pool import ConnectionPool +from .http11 import HTTP11Connection +from .interfaces import ConnectionInterface + +HeadersAsSequence = Sequence[Tuple[Union[bytes, str], Union[bytes, str]]] +HeadersAsMapping = Mapping[Union[bytes, str], Union[bytes, str]] + + +logger = logging.getLogger("httpcore.proxy") + + +def merge_headers( + default_headers: Optional[Sequence[Tuple[bytes, bytes]]] = None, + override_headers: Optional[Sequence[Tuple[bytes, bytes]]] = None, +) -> List[Tuple[bytes, bytes]]: + """ + Append default_headers and override_headers, de-duplicating if a key exists + in both cases. + """ + default_headers = [] if default_headers is None else list(default_headers) + override_headers = [] if override_headers is None else list(override_headers) + has_override = set(key.lower() for key, value in override_headers) + default_headers = [ + (key, value) + for key, value in default_headers + if key.lower() not in has_override + ] + return default_headers + override_headers + + +def build_auth_header(username: bytes, password: bytes) -> bytes: + userpass = username + b":" + password + return b"Basic " + b64encode(userpass) + + +class HTTPProxy(ConnectionPool): + """ + A connection pool that sends requests via an HTTP proxy. + """ + + def __init__( + self, + proxy_url: Union[URL, bytes, str], + proxy_auth: Optional[Tuple[Union[bytes, str], Union[bytes, str]]] = None, + proxy_headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, + ssl_context: Optional[ssl.SSLContext] = None, + max_connections: Optional[int] = 10, + max_keepalive_connections: Optional[int] = None, + keepalive_expiry: Optional[float] = None, + http1: bool = True, + http2: bool = False, + retries: int = 0, + local_address: Optional[str] = None, + uds: Optional[str] = None, + network_backend: Optional[NetworkBackend] = None, + socket_options: Optional[Iterable[SOCKET_OPTION]] = None, + ) -> None: + """ + A connection pool for making HTTP requests. + + Parameters: + proxy_url: The URL to use when connecting to the proxy server. + For example `"http://127.0.0.1:8080/"`. + proxy_auth: Any proxy authentication as a two-tuple of + (username, password). May be either bytes or ascii-only str. + proxy_headers: Any HTTP headers to use for the proxy requests. + For example `{"Proxy-Authorization": "Basic :"}`. + ssl_context: An SSL context to use for verifying connections. + If not specified, the default `httpcore.default_ssl_context()` + will be used. + max_connections: The maximum number of concurrent HTTP connections that + the pool should allow. Any attempt to send a request on a pool that + would exceed this amount will block until a connection is available. + max_keepalive_connections: The maximum number of idle HTTP connections + that will be maintained in the pool. + keepalive_expiry: The duration in seconds that an idle HTTP connection + may be maintained for before being expired from the pool. + http1: A boolean indicating if HTTP/1.1 requests should be supported + by the connection pool. Defaults to True. + http2: A boolean indicating if HTTP/2 requests should be supported by + the connection pool. Defaults to False. + retries: The maximum number of retries when trying to establish + a connection. + local_address: Local address to connect from. Can also be used to + connect using a particular address family. Using + `local_address="0.0.0.0"` will connect using an `AF_INET` address + (IPv4), while using `local_address="::"` will connect using an + `AF_INET6` address (IPv6). + uds: Path to a Unix Domain Socket to use instead of TCP sockets. + network_backend: A backend instance to use for handling network I/O. + """ + super().__init__( + ssl_context=ssl_context, + max_connections=max_connections, + max_keepalive_connections=max_keepalive_connections, + keepalive_expiry=keepalive_expiry, + http1=http1, + http2=http2, + network_backend=network_backend, + retries=retries, + local_address=local_address, + uds=uds, + socket_options=socket_options, + ) + self._ssl_context = ssl_context + self._proxy_url = enforce_url(proxy_url, name="proxy_url") + self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers") + if proxy_auth is not None: + username = enforce_bytes(proxy_auth[0], name="proxy_auth") + password = enforce_bytes(proxy_auth[1], name="proxy_auth") + authorization = build_auth_header(username, password) + self._proxy_headers = [ + (b"Proxy-Authorization", authorization) + ] + self._proxy_headers + + def create_connection(self, origin: Origin) -> ConnectionInterface: + if origin.scheme == b"http": + return ForwardHTTPConnection( + proxy_origin=self._proxy_url.origin, + proxy_headers=self._proxy_headers, + remote_origin=origin, + keepalive_expiry=self._keepalive_expiry, + network_backend=self._network_backend, + ) + return TunnelHTTPConnection( + proxy_origin=self._proxy_url.origin, + proxy_headers=self._proxy_headers, + remote_origin=origin, + ssl_context=self._ssl_context, + keepalive_expiry=self._keepalive_expiry, + http1=self._http1, + http2=self._http2, + network_backend=self._network_backend, + ) + + +class ForwardHTTPConnection(ConnectionInterface): + def __init__( + self, + proxy_origin: Origin, + remote_origin: Origin, + proxy_headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, + keepalive_expiry: Optional[float] = None, + network_backend: Optional[NetworkBackend] = None, + socket_options: Optional[Iterable[SOCKET_OPTION]] = None, + ) -> None: + self._connection = HTTPConnection( + origin=proxy_origin, + keepalive_expiry=keepalive_expiry, + network_backend=network_backend, + socket_options=socket_options, + ) + self._proxy_origin = proxy_origin + self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers") + self._remote_origin = remote_origin + + def handle_request(self, request: Request) -> Response: + headers = merge_headers(self._proxy_headers, request.headers) + url = URL( + scheme=self._proxy_origin.scheme, + host=self._proxy_origin.host, + port=self._proxy_origin.port, + target=bytes(request.url), + ) + proxy_request = Request( + method=request.method, + url=url, + headers=headers, + content=request.stream, + extensions=request.extensions, + ) + return self._connection.handle_request(proxy_request) + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._remote_origin + + def close(self) -> None: + self._connection.close() + + def info(self) -> str: + return self._connection.info() + + def is_available(self) -> bool: + return self._connection.is_available() + + def has_expired(self) -> bool: + return self._connection.has_expired() + + def is_idle(self) -> bool: + return self._connection.is_idle() + + def is_closed(self) -> bool: + return self._connection.is_closed() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.info()}]>" + + +class TunnelHTTPConnection(ConnectionInterface): + def __init__( + self, + proxy_origin: Origin, + remote_origin: Origin, + ssl_context: Optional[ssl.SSLContext] = None, + proxy_headers: Optional[Sequence[Tuple[bytes, bytes]]] = None, + keepalive_expiry: Optional[float] = None, + http1: bool = True, + http2: bool = False, + network_backend: Optional[NetworkBackend] = None, + socket_options: Optional[Iterable[SOCKET_OPTION]] = None, + ) -> None: + self._connection: ConnectionInterface = HTTPConnection( + origin=proxy_origin, + keepalive_expiry=keepalive_expiry, + network_backend=network_backend, + socket_options=socket_options, + ) + self._proxy_origin = proxy_origin + self._remote_origin = remote_origin + self._ssl_context = ssl_context + self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers") + self._keepalive_expiry = keepalive_expiry + self._http1 = http1 + self._http2 = http2 + self._connect_lock = Lock() + self._connected = False + + def handle_request(self, request: Request) -> Response: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("connect", None) + + with self._connect_lock: + if not self._connected: + target = b"%b:%d" % (self._remote_origin.host, self._remote_origin.port) + + connect_url = URL( + scheme=self._proxy_origin.scheme, + host=self._proxy_origin.host, + port=self._proxy_origin.port, + target=target, + ) + connect_headers = merge_headers( + [(b"Host", target), (b"Accept", b"*/*")], self._proxy_headers + ) + connect_request = Request( + method=b"CONNECT", + url=connect_url, + headers=connect_headers, + extensions=request.extensions, + ) + connect_response = self._connection.handle_request( + connect_request + ) + + if connect_response.status < 200 or connect_response.status > 299: + reason_bytes = connect_response.extensions.get("reason_phrase", b"") + reason_str = reason_bytes.decode("ascii", errors="ignore") + msg = "%d %s" % (connect_response.status, reason_str) + self._connection.close() + raise ProxyError(msg) + + stream = connect_response.extensions["network_stream"] + + # Upgrade the stream to SSL + ssl_context = ( + default_ssl_context() + if self._ssl_context is None + else self._ssl_context + ) + alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"] + ssl_context.set_alpn_protocols(alpn_protocols) + + kwargs = { + "ssl_context": ssl_context, + "server_hostname": self._remote_origin.host.decode("ascii"), + "timeout": timeout, + } + with Trace("start_tls", logger, request, kwargs) as trace: + stream = stream.start_tls(**kwargs) + trace.return_value = stream + + # Determine if we should be using HTTP/1.1 or HTTP/2 + ssl_object = stream.get_extra_info("ssl_object") + http2_negotiated = ( + ssl_object is not None + and ssl_object.selected_alpn_protocol() == "h2" + ) + + # Create the HTTP/1.1 or HTTP/2 connection + if http2_negotiated or (self._http2 and not self._http1): + from .http2 import HTTP2Connection + + self._connection = HTTP2Connection( + origin=self._remote_origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + else: + self._connection = HTTP11Connection( + origin=self._remote_origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + + self._connected = True + return self._connection.handle_request(request) + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._remote_origin + + def close(self) -> None: + self._connection.close() + + def info(self) -> str: + return self._connection.info() + + def is_available(self) -> bool: + return self._connection.is_available() + + def has_expired(self) -> bool: + return self._connection.has_expired() + + def is_idle(self) -> bool: + return self._connection.is_idle() + + def is_closed(self) -> bool: + return self._connection.is_closed() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.info()}]>" diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/interfaces.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/interfaces.py new file mode 100644 index 00000000..5e95be1e --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/interfaces.py @@ -0,0 +1,135 @@ +from contextlib import contextmanager +from typing import Iterator, Optional, Union + +from .._models import ( + URL, + Extensions, + HeaderTypes, + Origin, + Request, + Response, + enforce_bytes, + enforce_headers, + enforce_url, + include_request_headers, +) + + +class RequestInterface: + def request( + self, + method: Union[bytes, str], + url: Union[URL, bytes, str], + *, + headers: HeaderTypes = None, + content: Union[bytes, Iterator[bytes], None] = None, + extensions: Optional[Extensions] = None, + ) -> Response: + # Strict type checking on our parameters. + method = enforce_bytes(method, name="method") + url = enforce_url(url, name="url") + headers = enforce_headers(headers, name="headers") + + # Include Host header, and optionally Content-Length or Transfer-Encoding. + headers = include_request_headers(headers, url=url, content=content) + + request = Request( + method=method, + url=url, + headers=headers, + content=content, + extensions=extensions, + ) + response = self.handle_request(request) + try: + response.read() + finally: + response.close() + return response + + @contextmanager + def stream( + self, + method: Union[bytes, str], + url: Union[URL, bytes, str], + *, + headers: HeaderTypes = None, + content: Union[bytes, Iterator[bytes], None] = None, + extensions: Optional[Extensions] = None, + ) -> Iterator[Response]: + # Strict type checking on our parameters. + method = enforce_bytes(method, name="method") + url = enforce_url(url, name="url") + headers = enforce_headers(headers, name="headers") + + # Include Host header, and optionally Content-Length or Transfer-Encoding. + headers = include_request_headers(headers, url=url, content=content) + + request = Request( + method=method, + url=url, + headers=headers, + content=content, + extensions=extensions, + ) + response = self.handle_request(request) + try: + yield response + finally: + response.close() + + def handle_request(self, request: Request) -> Response: + raise NotImplementedError() # pragma: nocover + + +class ConnectionInterface(RequestInterface): + def close(self) -> None: + raise NotImplementedError() # pragma: nocover + + def info(self) -> str: + raise NotImplementedError() # pragma: nocover + + def can_handle_request(self, origin: Origin) -> bool: + raise NotImplementedError() # pragma: nocover + + def is_available(self) -> bool: + """ + Return `True` if the connection is currently able to accept an + outgoing request. + + An HTTP/1.1 connection will only be available if it is currently idle. + + An HTTP/2 connection will be available so long as the stream ID space is + not yet exhausted, and the connection is not in an error state. + + While the connection is being established we may not yet know if it is going + to result in an HTTP/1.1 or HTTP/2 connection. The connection should be + treated as being available, but might ultimately raise `NewConnectionRequired` + required exceptions if multiple requests are attempted over a connection + that ends up being established as HTTP/1.1. + """ + raise NotImplementedError() # pragma: nocover + + def has_expired(self) -> bool: + """ + Return `True` if the connection is in a state where it should be closed. + + This either means that the connection is idle and it has passed the + expiry time on its keep-alive, or that server has sent an EOF. + """ + raise NotImplementedError() # pragma: nocover + + def is_idle(self) -> bool: + """ + Return `True` if the connection is currently idle. + """ + raise NotImplementedError() # pragma: nocover + + def is_closed(self) -> bool: + """ + Return `True` if the connection has been closed. + + Used when a response is closed to determine if the connection may be + returned to the connection pool or not. + """ + raise NotImplementedError() # pragma: nocover diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/socks_proxy.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/socks_proxy.py new file mode 100644 index 00000000..407351d0 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_sync/socks_proxy.py @@ -0,0 +1,340 @@ +import logging +import ssl +import typing + +from socksio import socks5 + +from .._backends.sync import SyncBackend +from .._backends.base import NetworkBackend, NetworkStream +from .._exceptions import ConnectionNotAvailable, ProxyError +from .._models import URL, Origin, Request, Response, enforce_bytes, enforce_url +from .._ssl import default_ssl_context +from .._synchronization import Lock +from .._trace import Trace +from .connection_pool import ConnectionPool +from .http11 import HTTP11Connection +from .interfaces import ConnectionInterface + +logger = logging.getLogger("httpcore.socks") + + +AUTH_METHODS = { + b"\x00": "NO AUTHENTICATION REQUIRED", + b"\x01": "GSSAPI", + b"\x02": "USERNAME/PASSWORD", + b"\xff": "NO ACCEPTABLE METHODS", +} + +REPLY_CODES = { + b"\x00": "Succeeded", + b"\x01": "General SOCKS server failure", + b"\x02": "Connection not allowed by ruleset", + b"\x03": "Network unreachable", + b"\x04": "Host unreachable", + b"\x05": "Connection refused", + b"\x06": "TTL expired", + b"\x07": "Command not supported", + b"\x08": "Address type not supported", +} + + +def _init_socks5_connection( + stream: NetworkStream, + *, + host: bytes, + port: int, + auth: typing.Optional[typing.Tuple[bytes, bytes]] = None, +) -> None: + conn = socks5.SOCKS5Connection() + + # Auth method request + auth_method = ( + socks5.SOCKS5AuthMethod.NO_AUTH_REQUIRED + if auth is None + else socks5.SOCKS5AuthMethod.USERNAME_PASSWORD + ) + conn.send(socks5.SOCKS5AuthMethodsRequest([auth_method])) + outgoing_bytes = conn.data_to_send() + stream.write(outgoing_bytes) + + # Auth method response + incoming_bytes = stream.read(max_bytes=4096) + response = conn.receive_data(incoming_bytes) + assert isinstance(response, socks5.SOCKS5AuthReply) + if response.method != auth_method: + requested = AUTH_METHODS.get(auth_method, "UNKNOWN") + responded = AUTH_METHODS.get(response.method, "UNKNOWN") + raise ProxyError( + f"Requested {requested} from proxy server, but got {responded}." + ) + + if response.method == socks5.SOCKS5AuthMethod.USERNAME_PASSWORD: + # Username/password request + assert auth is not None + username, password = auth + conn.send(socks5.SOCKS5UsernamePasswordRequest(username, password)) + outgoing_bytes = conn.data_to_send() + stream.write(outgoing_bytes) + + # Username/password response + incoming_bytes = stream.read(max_bytes=4096) + response = conn.receive_data(incoming_bytes) + assert isinstance(response, socks5.SOCKS5UsernamePasswordReply) + if not response.success: + raise ProxyError("Invalid username/password") + + # Connect request + conn.send( + socks5.SOCKS5CommandRequest.from_address( + socks5.SOCKS5Command.CONNECT, (host, port) + ) + ) + outgoing_bytes = conn.data_to_send() + stream.write(outgoing_bytes) + + # Connect response + incoming_bytes = stream.read(max_bytes=4096) + response = conn.receive_data(incoming_bytes) + assert isinstance(response, socks5.SOCKS5Reply) + if response.reply_code != socks5.SOCKS5ReplyCode.SUCCEEDED: + reply_code = REPLY_CODES.get(response.reply_code, "UNKOWN") + raise ProxyError(f"Proxy Server could not connect: {reply_code}.") + + +class SOCKSProxy(ConnectionPool): + """ + A connection pool that sends requests via an HTTP proxy. + """ + + def __init__( + self, + proxy_url: typing.Union[URL, bytes, str], + proxy_auth: typing.Optional[ + typing.Tuple[typing.Union[bytes, str], typing.Union[bytes, str]] + ] = None, + ssl_context: typing.Optional[ssl.SSLContext] = None, + max_connections: typing.Optional[int] = 10, + max_keepalive_connections: typing.Optional[int] = None, + keepalive_expiry: typing.Optional[float] = None, + http1: bool = True, + http2: bool = False, + retries: int = 0, + network_backend: typing.Optional[NetworkBackend] = None, + ) -> None: + """ + A connection pool for making HTTP requests. + + Parameters: + proxy_url: The URL to use when connecting to the proxy server. + For example `"http://127.0.0.1:8080/"`. + ssl_context: An SSL context to use for verifying connections. + If not specified, the default `httpcore.default_ssl_context()` + will be used. + max_connections: The maximum number of concurrent HTTP connections that + the pool should allow. Any attempt to send a request on a pool that + would exceed this amount will block until a connection is available. + max_keepalive_connections: The maximum number of idle HTTP connections + that will be maintained in the pool. + keepalive_expiry: The duration in seconds that an idle HTTP connection + may be maintained for before being expired from the pool. + http1: A boolean indicating if HTTP/1.1 requests should be supported + by the connection pool. Defaults to True. + http2: A boolean indicating if HTTP/2 requests should be supported by + the connection pool. Defaults to False. + retries: The maximum number of retries when trying to establish + a connection. + local_address: Local address to connect from. Can also be used to + connect using a particular address family. Using + `local_address="0.0.0.0"` will connect using an `AF_INET` address + (IPv4), while using `local_address="::"` will connect using an + `AF_INET6` address (IPv6). + uds: Path to a Unix Domain Socket to use instead of TCP sockets. + network_backend: A backend instance to use for handling network I/O. + """ + super().__init__( + ssl_context=ssl_context, + max_connections=max_connections, + max_keepalive_connections=max_keepalive_connections, + keepalive_expiry=keepalive_expiry, + http1=http1, + http2=http2, + network_backend=network_backend, + retries=retries, + ) + self._ssl_context = ssl_context + self._proxy_url = enforce_url(proxy_url, name="proxy_url") + if proxy_auth is not None: + username, password = proxy_auth + username_bytes = enforce_bytes(username, name="proxy_auth") + password_bytes = enforce_bytes(password, name="proxy_auth") + self._proxy_auth: typing.Optional[typing.Tuple[bytes, bytes]] = ( + username_bytes, + password_bytes, + ) + else: + self._proxy_auth = None + + def create_connection(self, origin: Origin) -> ConnectionInterface: + return Socks5Connection( + proxy_origin=self._proxy_url.origin, + remote_origin=origin, + proxy_auth=self._proxy_auth, + ssl_context=self._ssl_context, + keepalive_expiry=self._keepalive_expiry, + http1=self._http1, + http2=self._http2, + network_backend=self._network_backend, + ) + + +class Socks5Connection(ConnectionInterface): + def __init__( + self, + proxy_origin: Origin, + remote_origin: Origin, + proxy_auth: typing.Optional[typing.Tuple[bytes, bytes]] = None, + ssl_context: typing.Optional[ssl.SSLContext] = None, + keepalive_expiry: typing.Optional[float] = None, + http1: bool = True, + http2: bool = False, + network_backend: typing.Optional[NetworkBackend] = None, + ) -> None: + self._proxy_origin = proxy_origin + self._remote_origin = remote_origin + self._proxy_auth = proxy_auth + self._ssl_context = ssl_context + self._keepalive_expiry = keepalive_expiry + self._http1 = http1 + self._http2 = http2 + + self._network_backend: NetworkBackend = ( + SyncBackend() if network_backend is None else network_backend + ) + self._connect_lock = Lock() + self._connection: typing.Optional[ConnectionInterface] = None + self._connect_failed = False + + def handle_request(self, request: Request) -> Response: + timeouts = request.extensions.get("timeout", {}) + timeout = timeouts.get("connect", None) + + with self._connect_lock: + if self._connection is None: + try: + # Connect to the proxy + kwargs = { + "host": self._proxy_origin.host.decode("ascii"), + "port": self._proxy_origin.port, + "timeout": timeout, + } + with Trace("connect_tcp", logger, request, kwargs) as trace: + stream = self._network_backend.connect_tcp(**kwargs) + trace.return_value = stream + + # Connect to the remote host using socks5 + kwargs = { + "stream": stream, + "host": self._remote_origin.host.decode("ascii"), + "port": self._remote_origin.port, + "auth": self._proxy_auth, + } + with Trace( + "setup_socks5_connection", logger, request, kwargs + ) as trace: + _init_socks5_connection(**kwargs) + trace.return_value = stream + + # Upgrade the stream to SSL + if self._remote_origin.scheme == b"https": + ssl_context = ( + default_ssl_context() + if self._ssl_context is None + else self._ssl_context + ) + alpn_protocols = ( + ["http/1.1", "h2"] if self._http2 else ["http/1.1"] + ) + ssl_context.set_alpn_protocols(alpn_protocols) + + kwargs = { + "ssl_context": ssl_context, + "server_hostname": self._remote_origin.host.decode("ascii"), + "timeout": timeout, + } + with Trace("start_tls", logger, request, kwargs) as trace: + stream = stream.start_tls(**kwargs) + trace.return_value = stream + + # Determine if we should be using HTTP/1.1 or HTTP/2 + ssl_object = stream.get_extra_info("ssl_object") + http2_negotiated = ( + ssl_object is not None + and ssl_object.selected_alpn_protocol() == "h2" + ) + + # Create the HTTP/1.1 or HTTP/2 connection + if http2_negotiated or ( + self._http2 and not self._http1 + ): # pragma: nocover + from .http2 import HTTP2Connection + + self._connection = HTTP2Connection( + origin=self._remote_origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + else: + self._connection = HTTP11Connection( + origin=self._remote_origin, + stream=stream, + keepalive_expiry=self._keepalive_expiry, + ) + except Exception as exc: + self._connect_failed = True + raise exc + elif not self._connection.is_available(): # pragma: nocover + raise ConnectionNotAvailable() + + return self._connection.handle_request(request) + + def can_handle_request(self, origin: Origin) -> bool: + return origin == self._remote_origin + + def close(self) -> None: + if self._connection is not None: + self._connection.close() + + def is_available(self) -> bool: + if self._connection is None: # pragma: nocover + # If HTTP/2 support is enabled, and the resulting connection could + # end up as HTTP/2 then we should indicate the connection as being + # available to service multiple requests. + return ( + self._http2 + and (self._remote_origin.scheme == b"https" or not self._http1) + and not self._connect_failed + ) + return self._connection.is_available() + + def has_expired(self) -> bool: + if self._connection is None: # pragma: nocover + return self._connect_failed + return self._connection.has_expired() + + def is_idle(self) -> bool: + if self._connection is None: # pragma: nocover + return self._connect_failed + return self._connection.is_idle() + + def is_closed(self) -> bool: + if self._connection is None: # pragma: nocover + return self._connect_failed + return self._connection.is_closed() + + def info(self) -> str: + if self._connection is None: # pragma: nocover + return "CONNECTION FAILED" if self._connect_failed else "CONNECTING" + return self._connection.info() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} [{self.info()}]>" diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_synchronization.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_synchronization.py new file mode 100644 index 00000000..bae27c1b --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_synchronization.py @@ -0,0 +1,279 @@ +import threading +from types import TracebackType +from typing import Optional, Type + +import sniffio + +from ._exceptions import ExceptionMapping, PoolTimeout, map_exceptions + +# Our async synchronization primatives use either 'anyio' or 'trio' depending +# on if they're running under asyncio or trio. + +try: + import trio +except ImportError: # pragma: nocover + trio = None # type: ignore + +try: + import anyio +except ImportError: # pragma: nocover + anyio = None # type: ignore + + +class AsyncLock: + def __init__(self) -> None: + self._backend = "" + + def setup(self) -> None: + """ + Detect if we're running under 'asyncio' or 'trio' and create + a lock with the correct implementation. + """ + self._backend = sniffio.current_async_library() + if self._backend == "trio": + if trio is None: # pragma: nocover + raise RuntimeError( + "Running under trio, requires the 'trio' package to be installed." + ) + self._trio_lock = trio.Lock() + else: + if anyio is None: # pragma: nocover + raise RuntimeError( + "Running under asyncio requires the 'anyio' package to be installed." + ) + self._anyio_lock = anyio.Lock() + + async def __aenter__(self) -> "AsyncLock": + if not self._backend: + self.setup() + + if self._backend == "trio": + await self._trio_lock.acquire() + else: + await self._anyio_lock.acquire() + + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + if self._backend == "trio": + self._trio_lock.release() + else: + self._anyio_lock.release() + + +class AsyncEvent: + def __init__(self) -> None: + self._backend = "" + + def setup(self) -> None: + """ + Detect if we're running under 'asyncio' or 'trio' and create + a lock with the correct implementation. + """ + self._backend = sniffio.current_async_library() + if self._backend == "trio": + if trio is None: # pragma: nocover + raise RuntimeError( + "Running under trio requires the 'trio' package to be installed." + ) + self._trio_event = trio.Event() + else: + if anyio is None: # pragma: nocover + raise RuntimeError( + "Running under asyncio requires the 'anyio' package to be installed." + ) + self._anyio_event = anyio.Event() + + def set(self) -> None: + if not self._backend: + self.setup() + + if self._backend == "trio": + self._trio_event.set() + else: + self._anyio_event.set() + + async def wait(self, timeout: Optional[float] = None) -> None: + if not self._backend: + self.setup() + + if self._backend == "trio": + if trio is None: # pragma: nocover + raise RuntimeError( + "Running under trio requires the 'trio' package to be installed." + ) + + trio_exc_map: ExceptionMapping = {trio.TooSlowError: PoolTimeout} + timeout_or_inf = float("inf") if timeout is None else timeout + with map_exceptions(trio_exc_map): + with trio.fail_after(timeout_or_inf): + await self._trio_event.wait() + else: + if anyio is None: # pragma: nocover + raise RuntimeError( + "Running under asyncio requires the 'anyio' package to be installed." + ) + + anyio_exc_map: ExceptionMapping = {TimeoutError: PoolTimeout} + with map_exceptions(anyio_exc_map): + with anyio.fail_after(timeout): + await self._anyio_event.wait() + + +class AsyncSemaphore: + def __init__(self, bound: int) -> None: + self._bound = bound + self._backend = "" + + def setup(self) -> None: + """ + Detect if we're running under 'asyncio' or 'trio' and create + a semaphore with the correct implementation. + """ + self._backend = sniffio.current_async_library() + if self._backend == "trio": + if trio is None: # pragma: nocover + raise RuntimeError( + "Running under trio requires the 'trio' package to be installed." + ) + + self._trio_semaphore = trio.Semaphore( + initial_value=self._bound, max_value=self._bound + ) + else: + if anyio is None: # pragma: nocover + raise RuntimeError( + "Running under asyncio requires the 'anyio' package to be installed." + ) + + self._anyio_semaphore = anyio.Semaphore( + initial_value=self._bound, max_value=self._bound + ) + + async def acquire(self) -> None: + if not self._backend: + self.setup() + + if self._backend == "trio": + await self._trio_semaphore.acquire() + else: + await self._anyio_semaphore.acquire() + + async def release(self) -> None: + if self._backend == "trio": + self._trio_semaphore.release() + else: + self._anyio_semaphore.release() + + +class AsyncShieldCancellation: + # For certain portions of our codebase where we're dealing with + # closing connections during exception handling we want to shield + # the operation from being cancelled. + # + # with AsyncShieldCancellation(): + # ... # clean-up operations, shielded from cancellation. + + def __init__(self) -> None: + """ + Detect if we're running under 'asyncio' or 'trio' and create + a shielded scope with the correct implementation. + """ + self._backend = sniffio.current_async_library() + + if self._backend == "trio": + if trio is None: # pragma: nocover + raise RuntimeError( + "Running under trio requires the 'trio' package to be installed." + ) + + self._trio_shield = trio.CancelScope(shield=True) + else: + if anyio is None: # pragma: nocover + raise RuntimeError( + "Running under asyncio requires the 'anyio' package to be installed." + ) + + self._anyio_shield = anyio.CancelScope(shield=True) + + def __enter__(self) -> "AsyncShieldCancellation": + if self._backend == "trio": + self._trio_shield.__enter__() + else: + self._anyio_shield.__enter__() + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + if self._backend == "trio": + self._trio_shield.__exit__(exc_type, exc_value, traceback) + else: + self._anyio_shield.__exit__(exc_type, exc_value, traceback) + + +# Our thread-based synchronization primitives... + + +class Lock: + def __init__(self) -> None: + self._lock = threading.Lock() + + def __enter__(self) -> "Lock": + self._lock.acquire() + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + self._lock.release() + + +class Event: + def __init__(self) -> None: + self._event = threading.Event() + + def set(self) -> None: + self._event.set() + + def wait(self, timeout: Optional[float] = None) -> None: + if not self._event.wait(timeout=timeout): + raise PoolTimeout() # pragma: nocover + + +class Semaphore: + def __init__(self, bound: int) -> None: + self._semaphore = threading.Semaphore(value=bound) + + def acquire(self) -> None: + self._semaphore.acquire() + + def release(self) -> None: + self._semaphore.release() + + +class ShieldCancellation: + # Thread-synchronous codebases don't support cancellation semantics. + # We have this class because we need to mirror the async and sync + # cases within our package, but it's just a no-op. + def __enter__(self) -> "ShieldCancellation": + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + pass diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_trace.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_trace.py new file mode 100644 index 00000000..b122a53e --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_trace.py @@ -0,0 +1,105 @@ +import inspect +import logging +from types import TracebackType +from typing import Any, Dict, Optional, Type + +from ._models import Request + + +class Trace: + def __init__( + self, + name: str, + logger: logging.Logger, + request: Optional[Request] = None, + kwargs: Optional[Dict[str, Any]] = None, + ) -> None: + self.name = name + self.logger = logger + self.trace_extension = ( + None if request is None else request.extensions.get("trace") + ) + self.debug = self.logger.isEnabledFor(logging.DEBUG) + self.kwargs = kwargs or {} + self.return_value: Any = None + self.should_trace = self.debug or self.trace_extension is not None + self.prefix = self.logger.name.split(".")[-1] + + def trace(self, name: str, info: Dict[str, Any]) -> None: + if self.trace_extension is not None: + prefix_and_name = f"{self.prefix}.{name}" + ret = self.trace_extension(prefix_and_name, info) + if inspect.iscoroutine(ret): # pragma: no cover + raise TypeError( + "If you are using a synchronous interface, " + "the callback of the `trace` extension should " + "be a normal function instead of an asynchronous function." + ) + + if self.debug: + if not info or "return_value" in info and info["return_value"] is None: + message = name + else: + args = " ".join([f"{key}={value!r}" for key, value in info.items()]) + message = f"{name} {args}" + self.logger.debug(message) + + def __enter__(self) -> "Trace": + if self.should_trace: + info = self.kwargs + self.trace(f"{self.name}.started", info) + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + if self.should_trace: + if exc_value is None: + info = {"return_value": self.return_value} + self.trace(f"{self.name}.complete", info) + else: + info = {"exception": exc_value} + self.trace(f"{self.name}.failed", info) + + async def atrace(self, name: str, info: Dict[str, Any]) -> None: + if self.trace_extension is not None: + prefix_and_name = f"{self.prefix}.{name}" + coro = self.trace_extension(prefix_and_name, info) + if not inspect.iscoroutine(coro): # pragma: no cover + raise TypeError( + "If you're using an asynchronous interface, " + "the callback of the `trace` extension should " + "be an asynchronous function rather than a normal function." + ) + await coro + + if self.debug: + if not info or "return_value" in info and info["return_value"] is None: + message = name + else: + args = " ".join([f"{key}={value!r}" for key, value in info.items()]) + message = f"{name} {args}" + self.logger.debug(message) + + async def __aenter__(self) -> "Trace": + if self.should_trace: + info = self.kwargs + await self.atrace(f"{self.name}.started", info) + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + if self.should_trace: + if exc_value is None: + info = {"return_value": self.return_value} + await self.atrace(f"{self.name}.complete", info) + else: + info = {"exception": exc_value} + await self.atrace(f"{self.name}.failed", info) diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/_utils.py b/Backend/venv/lib/python3.12/site-packages/httpcore/_utils.py new file mode 100644 index 00000000..df5dea8f --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpcore/_utils.py @@ -0,0 +1,36 @@ +import select +import socket +import sys +import typing + + +def is_socket_readable(sock: typing.Optional[socket.socket]) -> bool: + """ + Return whether a socket, as identifed by its file descriptor, is readable. + "A socket is readable" means that the read buffer isn't empty, i.e. that calling + .recv() on it would immediately return some data. + """ + # NOTE: we want check for readability without actually attempting to read, because + # we don't want to block forever if it's not readable. + + # In the case that the socket no longer exists, or cannot return a file + # descriptor, we treat it as being readable, as if it the next read operation + # on it is ready to return the terminating `b""`. + sock_fd = None if sock is None else sock.fileno() + if sock_fd is None or sock_fd < 0: # pragma: nocover + return True + + # The implementation below was stolen from: + # https://github.com/python-trio/trio/blob/20ee2b1b7376db637435d80e266212a35837ddcc/trio/_socket.py#L471-L478 + # See also: https://github.com/encode/httpcore/pull/193#issuecomment-703129316 + + # Use select.select on Windows, and when poll is unavailable and select.poll + # everywhere else. (E.g. When eventlet is in use. See #327) + if ( + sys.platform == "win32" or getattr(select, "poll", None) is None + ): # pragma: nocover + rready, _, _ = select.select([sock_fd], [], [], 0) + return bool(rready) + p = select.poll() + p.register(sock_fd, select.POLLIN) + return bool(p.poll(0)) diff --git a/Backend/venv/lib/python3.12/site-packages/httpcore/py.typed b/Backend/venv/lib/python3.12/site-packages/httpcore/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/INSTALLER b/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/METADATA b/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/METADATA new file mode 100644 index 00000000..84b89fb8 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/METADATA @@ -0,0 +1,212 @@ +Metadata-Version: 2.1 +Name: httpx +Version: 0.24.1 +Summary: The next generation HTTP client. +Project-URL: Changelog, https://github.com/encode/httpx/blob/master/CHANGELOG.md +Project-URL: Documentation, https://www.python-httpx.org +Project-URL: Homepage, https://github.com/encode/httpx +Project-URL: Source, https://github.com/encode/httpx +Author-email: Tom Christie +License-Expression: BSD-3-Clause +License-File: LICENSE.md +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Web Environment +Classifier: Framework :: AsyncIO +Classifier: Framework :: Trio +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Topic :: Internet :: WWW/HTTP +Requires-Python: >=3.7 +Requires-Dist: certifi +Requires-Dist: httpcore<0.18.0,>=0.15.0 +Requires-Dist: idna +Requires-Dist: sniffio +Provides-Extra: brotli +Requires-Dist: brotli; platform_python_implementation == 'CPython' and extra == 'brotli' +Requires-Dist: brotlicffi; platform_python_implementation != 'CPython' and extra == 'brotli' +Provides-Extra: cli +Requires-Dist: click==8.*; extra == 'cli' +Requires-Dist: pygments==2.*; extra == 'cli' +Requires-Dist: rich<14,>=10; extra == 'cli' +Provides-Extra: http2 +Requires-Dist: h2<5,>=3; extra == 'http2' +Provides-Extra: socks +Requires-Dist: socksio==1.*; extra == 'socks' +Description-Content-Type: text/markdown + +

+ HTTPX +

+ +

HTTPX - A next-generation HTTP client for Python.

+ +

+ + Test Suite + + + Package version + +

+ +HTTPX is a fully featured HTTP client library for Python 3. It includes **an integrated +command line client**, has support for both **HTTP/1.1 and HTTP/2**, and provides both **sync +and async APIs**. + +--- + +Install HTTPX using pip: + +```shell +$ pip install httpx +``` + +Now, let's get started: + +```pycon +>>> import httpx +>>> r = httpx.get('https://www.example.org/') +>>> r + +>>> r.status_code +200 +>>> r.headers['content-type'] +'text/html; charset=UTF-8' +>>> r.text +'\n\n\nExample Domain...' +``` + +Or, using the command-line client. + +```shell +$ pip install 'httpx[cli]' # The command line client is an optional dependency. +``` + +Which now allows us to use HTTPX directly from the command-line... + +

+ httpx --help +

+ +Sending a request... + +

+ httpx http://httpbin.org/json +

+ +## Features + +HTTPX builds on the well-established usability of `requests`, and gives you: + +* A broadly [requests-compatible API](https://www.python-httpx.org/compatibility/). +* An integrated command-line client. +* HTTP/1.1 [and HTTP/2 support](https://www.python-httpx.org/http2/). +* Standard synchronous interface, but with [async support if you need it](https://www.python-httpx.org/async/). +* Ability to make requests directly to [WSGI applications](https://www.python-httpx.org/advanced/#calling-into-python-web-apps) or [ASGI applications](https://www.python-httpx.org/async/#calling-into-python-web-apps). +* Strict timeouts everywhere. +* Fully type annotated. +* 100% test coverage. + +Plus all the standard features of `requests`... + +* International Domains and URLs +* Keep-Alive & Connection Pooling +* Sessions with Cookie Persistence +* Browser-style SSL Verification +* Basic/Digest Authentication +* Elegant Key/Value Cookies +* Automatic Decompression +* Automatic Content Decoding +* Unicode Response Bodies +* Multipart File Uploads +* HTTP(S) Proxy Support +* Connection Timeouts +* Streaming Downloads +* .netrc Support +* Chunked Requests + +## Installation + +Install with pip: + +```shell +$ pip install httpx +``` + +Or, to include the optional HTTP/2 support, use: + +```shell +$ pip install httpx[http2] +``` + +HTTPX requires Python 3.7+. + +## Documentation + +Project documentation is available at [https://www.python-httpx.org/](https://www.python-httpx.org/). + +For a run-through of all the basics, head over to the [QuickStart](https://www.python-httpx.org/quickstart/). + +For more advanced topics, see the [Advanced Usage](https://www.python-httpx.org/advanced/) section, the [async support](https://www.python-httpx.org/async/) section, or the [HTTP/2](https://www.python-httpx.org/http2/) section. + +The [Developer Interface](https://www.python-httpx.org/api/) provides a comprehensive API reference. + +To find out about tools that integrate with HTTPX, see [Third Party Packages](https://www.python-httpx.org/third_party_packages/). + +## Contribute + +If you want to contribute with HTTPX check out the [Contributing Guide](https://www.python-httpx.org/contributing/) to learn how to start. + +## Dependencies + +The HTTPX project relies on these excellent libraries: + +* `httpcore` - The underlying transport implementation for `httpx`. + * `h11` - HTTP/1.1 support. +* `certifi` - SSL certificates. +* `idna` - Internationalized domain name support. +* `sniffio` - Async library autodetection. + +As well as these optional installs: + +* `h2` - HTTP/2 support. *(Optional, with `httpx[http2]`)* +* `socksio` - SOCKS proxy support. *(Optional, with `httpx[socks]`)* +* `rich` - Rich terminal support. *(Optional, with `httpx[cli]`)* +* `click` - Command line client support. *(Optional, with `httpx[cli]`)* +* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx[brotli]`)* + +A huge amount of credit is due to `requests` for the API layout that +much of this work follows, as well as to `urllib3` for plenty of design +inspiration around the lower-level networking details. + +--- + +

HTTPX is BSD licensed code.
Designed & crafted with care.

— 🦋 —

+ +## Release Information + +### Added + +* Provide additional context in some `InvalidURL` exceptions. (#2675) + +### Fixed + +* Fix optional percent-encoding behaviour. (#2671) +* More robust checking for opening upload files in binary mode. (#2630) +* Properly support IP addresses in `NO_PROXY` environment variable. (#2659) +* Set default file for `NetRCAuth()` to `None` to use the stdlib default. (#2667) +* Set logging request lines to INFO level for async requests, in line with sync requests. (#2656) +* Fix which gen-delims need to be escaped for path/query/fragment components in URL. (#2701) + + +--- + +[Full changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md) diff --git a/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/RECORD b/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/RECORD new file mode 100644 index 00000000..1cbe3ad6 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/RECORD @@ -0,0 +1,57 @@ +../../../bin/httpx,sha256=YhM3lroNg2xkNdaFKicQjc2KywBLVV9r6yrAbSYZAio,216 +httpx-0.24.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +httpx-0.24.1.dist-info/METADATA,sha256=ZBqGMGxXnjZ-UpNiE4mXBHZzuC4lRx_mTPc_R4YNoiQ,7428 +httpx-0.24.1.dist-info/RECORD,, +httpx-0.24.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +httpx-0.24.1.dist-info/WHEEL,sha256=y1bSCq4r5i4nMmpXeUJMqs3ipKvkZObrIXSvJHm1qCI,87 +httpx-0.24.1.dist-info/entry_points.txt,sha256=2lVkdQmxLA1pNMgSN2eV89o90HCZezhmNwsy6ryKDSA,37 +httpx-0.24.1.dist-info/licenses/LICENSE.md,sha256=TsWdVE8StfU5o6cW_TIaxYzNgDC0ZSIfLIgCAM3yjY0,1508 +httpx/__init__.py,sha256=oCxVAsePEy5DE9eLhGAAq9H3RBGZUDaUROtGEyzbBRo,3210 +httpx/__pycache__/__init__.cpython-312.pyc,, +httpx/__pycache__/__version__.cpython-312.pyc,, +httpx/__pycache__/_api.cpython-312.pyc,, +httpx/__pycache__/_auth.cpython-312.pyc,, +httpx/__pycache__/_client.cpython-312.pyc,, +httpx/__pycache__/_compat.cpython-312.pyc,, +httpx/__pycache__/_config.cpython-312.pyc,, +httpx/__pycache__/_content.cpython-312.pyc,, +httpx/__pycache__/_decoders.cpython-312.pyc,, +httpx/__pycache__/_exceptions.cpython-312.pyc,, +httpx/__pycache__/_main.cpython-312.pyc,, +httpx/__pycache__/_models.cpython-312.pyc,, +httpx/__pycache__/_multipart.cpython-312.pyc,, +httpx/__pycache__/_status_codes.cpython-312.pyc,, +httpx/__pycache__/_types.cpython-312.pyc,, +httpx/__pycache__/_urlparse.cpython-312.pyc,, +httpx/__pycache__/_urls.cpython-312.pyc,, +httpx/__pycache__/_utils.cpython-312.pyc,, +httpx/__version__.py,sha256=bg4cSle4BdKgSjAPJGqR4kGXZ-nTOXf_1g68lFLU8To,108 +httpx/_api.py,sha256=cVU9ErzaXve5rqoPoSHr9yJbovHtICrcxR7yBoNSeOw,13011 +httpx/_auth.py,sha256=58FA-xqqp-XgLZ7Emd4-et-XXuTRaa5buiBYB2MzyvE,11773 +httpx/_client.py,sha256=A9MPP_d1ZlqcO5CeGLgyzVwdHgCpROYSdjoAUA6rpYE,68131 +httpx/_compat.py,sha256=lQa4SnZhS-kNQ8HKpSwKrmJ00nYQKDVaWwwnOYEvjMI,1602 +httpx/_config.py,sha256=9Tg0-pV93Hl5knjyZhCLcoEXymAMn-OLaDsEn2uPK14,12391 +httpx/_content.py,sha256=olbWqawdWWweXeW6gDYHPiEGjip5lqFZKv9OmVd-zIg,8092 +httpx/_decoders.py,sha256=dd8GSkEAe45BzRUF47zH_lg3-BcwXtxzPBSGP5Y4F90,9739 +httpx/_exceptions.py,sha256=xKw-U6vW7zmdReUAGYHMegYWZuDAuE5039L087SHe4Q,7880 +httpx/_main.py,sha256=m9C4RuqjOB6UqL3FFHMjmC45f4SDSO-iOREFLdw4IdM,15784 +httpx/_models.py,sha256=Ho9YjmVMkS-lEMhCGpecfYsenVZy2jsLJmKCexO50tI,42696 +httpx/_multipart.py,sha256=qzt35jAgapaRPwdq-lTKSA5YY6ayrfDIsZLdr3t4NWc,8972 +httpx/_status_codes.py,sha256=XKArMrSoo8oKBQCHdFGA-wsM2PcSTaHE8svDYOUcwWk,5584 +httpx/_transports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +httpx/_transports/__pycache__/__init__.cpython-312.pyc,, +httpx/_transports/__pycache__/asgi.cpython-312.pyc,, +httpx/_transports/__pycache__/base.cpython-312.pyc,, +httpx/_transports/__pycache__/default.cpython-312.pyc,, +httpx/_transports/__pycache__/mock.cpython-312.pyc,, +httpx/_transports/__pycache__/wsgi.cpython-312.pyc,, +httpx/_transports/asgi.py,sha256=lKAL-6dhxqSnZA2fMWtj-MokSTIzjnwwa3DTkkof5cE,5317 +httpx/_transports/base.py,sha256=0BM8yZZEkdFT4tXXSm0h0dK0cSYA4hLgInj_BljGEGw,2510 +httpx/_transports/default.py,sha256=fla9xvSAM3BuGtaMa4PhbX1gW_9oafl8vzujOhcE-H8,12626 +httpx/_transports/mock.py,sha256=sDt3BDXbz8-W94kC8OXtGzF1PWH0y73h1De7Q-XkVtg,1179 +httpx/_transports/wsgi.py,sha256=72ZMPBLPV-aZB4gfsz_SOrJpgKJb6Z9W5wFxhlMQcqg,4754 +httpx/_types.py,sha256=BnX0adSAxLT9BzkxuX96S4odkC9UdLMgws6waxqEKuI,3333 +httpx/_urlparse.py,sha256=JvFjro7sdHohzXwybwYALTTGy2MakRpfFreBTQu9A4w,16669 +httpx/_urls.py,sha256=JAONd-2reXpB_WuQ7WuvhUcLuebiQeYJQPyszADmCow,21840 +httpx/_utils.py,sha256=jaCEUHN9jpHfoudrtSNxYTmTeRLeOrP-s-MOTvq23rA,15397 +httpx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/REQUESTED b/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/WHEEL b/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/WHEEL new file mode 100644 index 00000000..27627551 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.17.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/entry_points.txt b/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/entry_points.txt new file mode 100644 index 00000000..8ae96007 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +httpx = httpx:main diff --git a/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/licenses/LICENSE.md b/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/licenses/LICENSE.md new file mode 100644 index 00000000..ab79d16a --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx-0.24.1.dist-info/licenses/LICENSE.md @@ -0,0 +1,12 @@ +Copyright © 2019, [Encode OSS Ltd](https://www.encode.io/). +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/__init__.py b/Backend/venv/lib/python3.12/site-packages/httpx/__init__.py new file mode 100644 index 00000000..f61112f8 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/__init__.py @@ -0,0 +1,138 @@ +from .__version__ import __description__, __title__, __version__ +from ._api import delete, get, head, options, patch, post, put, request, stream +from ._auth import Auth, BasicAuth, DigestAuth, NetRCAuth +from ._client import USE_CLIENT_DEFAULT, AsyncClient, Client +from ._config import Limits, Proxy, Timeout, create_ssl_context +from ._content import ByteStream +from ._exceptions import ( + CloseError, + ConnectError, + ConnectTimeout, + CookieConflict, + DecodingError, + HTTPError, + HTTPStatusError, + InvalidURL, + LocalProtocolError, + NetworkError, + PoolTimeout, + ProtocolError, + ProxyError, + ReadError, + ReadTimeout, + RemoteProtocolError, + RequestError, + RequestNotRead, + ResponseNotRead, + StreamClosed, + StreamConsumed, + StreamError, + TimeoutException, + TooManyRedirects, + TransportError, + UnsupportedProtocol, + WriteError, + WriteTimeout, +) +from ._models import Cookies, Headers, Request, Response +from ._status_codes import codes +from ._transports.asgi import ASGITransport +from ._transports.base import AsyncBaseTransport, BaseTransport +from ._transports.default import AsyncHTTPTransport, HTTPTransport +from ._transports.mock import MockTransport +from ._transports.wsgi import WSGITransport +from ._types import AsyncByteStream, SyncByteStream +from ._urls import URL, QueryParams + +try: + from ._main import main +except ImportError: # pragma: no cover + + def main() -> None: # type: ignore + import sys + + print( + "The httpx command line client could not run because the required " + "dependencies were not installed.\nMake sure you've installed " + "everything with: pip install 'httpx[cli]'" + ) + sys.exit(1) + + +__all__ = [ + "__description__", + "__title__", + "__version__", + "ASGITransport", + "AsyncBaseTransport", + "AsyncByteStream", + "AsyncClient", + "AsyncHTTPTransport", + "Auth", + "BaseTransport", + "BasicAuth", + "ByteStream", + "Client", + "CloseError", + "codes", + "ConnectError", + "ConnectTimeout", + "CookieConflict", + "Cookies", + "create_ssl_context", + "DecodingError", + "delete", + "DigestAuth", + "get", + "head", + "Headers", + "HTTPError", + "HTTPStatusError", + "HTTPTransport", + "InvalidURL", + "Limits", + "LocalProtocolError", + "main", + "MockTransport", + "NetRCAuth", + "NetworkError", + "options", + "patch", + "PoolTimeout", + "post", + "ProtocolError", + "Proxy", + "ProxyError", + "put", + "QueryParams", + "ReadError", + "ReadTimeout", + "RemoteProtocolError", + "request", + "Request", + "RequestError", + "RequestNotRead", + "Response", + "ResponseNotRead", + "stream", + "StreamClosed", + "StreamConsumed", + "StreamError", + "SyncByteStream", + "Timeout", + "TimeoutException", + "TooManyRedirects", + "TransportError", + "UnsupportedProtocol", + "URL", + "USE_CLIENT_DEFAULT", + "WriteError", + "WriteTimeout", + "WSGITransport", +] + + +__locals = locals() +for __name in __all__: + if not __name.startswith("__"): + setattr(__locals[__name], "__module__", "httpx") # noqa diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..454f0557b4e75a53f8dbcfcd5f857710542db292 GIT binary patch literal 3221 zcmaJ@OLG*-5w4#1Q*ZGU&`5w7KpMnT0s(sRvXF!rmQ~@6j%K=6>Qz_wxVi>u9Cic; zUu@&>-n>VL4~}qf@x{Nud-KJ_2C?gM5IWeGy=lRYb#QWK^^AlFhbLmb$;^6X<(HW? ze<>Do1U`c!{q8Rcg!}`Q)+f~z-uA}``3-T1V|auc)Zj51WEwkNzU-c7rC5AET-v{&1b z-X6Y}?&bUFKE9vs=LhHkZBKazc^~c5a@sq@57Wc^2tC40YVxD>DDS8Je1Hz{WAqq5 zPLJ~w^aLNIgZv~t$xF1vPtjBSG(FAF&@+694)I|+%*(XQ&(gDegpTlY^c+7=&+`lP z0>4Nv@=Np*zf3RlQ98=U=$Jk?<6Yrb=~X^X$N4pSjbEqNwLR(3kQL(e{LY{s#Rz-!qDdV6n0+)uJ~>JVU1rOl{i#Ll&acz^ z!iY3sJnx0IkJ-m;c|Vw(J9@zZ7*e2%3&UpOvP82REbq) zJ1QOMWuZ#eY*}4ZiCPd!6|dE$N(=U*Is+{gO2KSiN~pwSU9PI^lpVTNE#_w26_^s4 z=b2oZMiIQ~diY@0nx4BeJ72M8W^YbDoU5q(WZ3Yl)1J$G*fjcuO;d9&cO`hRD1wcK zN>^OY0yw{`3a3hDg`sCv17ETYSu#{^sv+5f$e})!oA!c`%?c3+RhSNZpH*d~wKcWY zZlDKiE(7bQo?DfwIK!%eeq)I_ zu7G%ks;weyA5IX>*mggJooc8vr{$;0Jrb^LQK9AL&OmWG3Vx{4x4~b?NR@70L1kLL z1Ghe}`=8XkRcR(o{T zcA7!_AZ_1EyO@e&D&+6inP@E9!sh66g4?b?UyX`5Lab=~zMTPpo^CkABR?gwS?N&s zu~tVg$WO>;$!OX8=)`wsG)O5i|1FOG`1h4nX0A$E+c2vE=eF;dp6fHSs*4M3b8>A2+w+(+oV#bQF*B^gKqIIR zK4s?in8~22G~_B2kGby3)hlMrt+j^DLG9LK@Z^(00dE=hsCd}G4kKJB`~=%@p^(L0 z7z(k6neQH#R|C$p@=Ysa2nwZ!WD!egf@g>gfhZe1WaqO4`Br190EpJ zTtJvb=tj7Ra0y`wK*iu?%2^ff`i6%M?gE`wC#UMLWUIZ0!*;mh>Qvutegd60N`6dq)&2ewhka_-JsB0l z1P<@okwd=~n5MhU>v)h(hm<87HE>3k6{ZwWR~K*roj{nWV8G_8GhXHI@ST?^ZCTK+ zAfHkmzRMx7WHqIfW%)K|DhD?fGQ>uu(jkNEOYsLH=3!rP58z#9Jj%rDVhvR6Na0@r zw&I3iY?6tWWMY#nd`%W!k%dh%_7xf1B;zm1_$ImW6}j=6nBT_^8L{ul|AV)BbetHl zd=!?vCZ}QfCYb_3%jY-Az*eGQ^t?_SeA9Q+$j6?gzgvotLf0=xpJl#j>wQCF#@Mr) z-{d>L&iB2{_kB@$mGA#srv3BG7kgfIn7=*q$Mmbr=yyq&^`EUEMl#)RNY3c_cOsMQ c*-C=+F2vOTb1pR#BVWd{Gi~uNiw59-0oy8pF#rGn literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/__version__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/__version__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b717cd0bb48bd5a13168961c1ba7b03f613f9b48 GIT binary patch literal 317 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 fy>HR;g2M8}1%{#7rG1%(PN|{u?-T>E{k&mQ literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_client.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_client.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..400ed724da73913f3c15e57328bdc9016d1034bb GIT binary patch literal 72309 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_content.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_content.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ecab8a8e74b9ee9669da7867164ee2f7c1591f6e GIT binary patch literal 10553 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 Lw;}))1)l!_BV!=V literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_decoders.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_decoders.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ddaf85256521a250f8594c66cd8123d2d31f3ef6 GIT binary patch literal 15117 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_main.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88e6ba565887121c52f2445ad4c5b73fd85167b6 GIT binary patch literal 21469 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 L`y+)ZvS9xU(_KM1 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_status_codes.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_status_codes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c44b8e1cb95ee73d6405456cf978ea86386a175f GIT binary patch literal 7268 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 aBj>&!8F>+Uo_N{x58;W=fx{%z_J0A!ionDG literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_types.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_types.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..daf260cf50fbd64d4465a6fc410e54363c392058 GIT binary patch literal 4163 zcmb7GO>o=B6$U_nKj5GEXIYk1Q`V0~|B$g|*-m1|6DxA0#FAV~iXCJU4ooaX#stAE zKwEU{lu2`tdty$ZPo;14WICgxa?>$0y#O~3rkS|YOLC*C&bY~`Zvjx2oM|V;3?AOT zzkU1d+qd{@FvwHz96xic$OS3tA7s#b{0_7GgNvfRq9jV7BwAuhv_QkxRbph9;G&6) zEwM5uaI#x)%O1fadj+rT6MQl+@Uma<%K;%E2Zf*<5<+rV2+Ms!pBxb)a#V=QF(D?$ zg*Z(iH%>~Nt%Xvb}PB8Txli4?IlU^9_-TQO4*Git?jW&wVJ zWMO$nEpJD&C)v{zNuIOfoPannNA=7B{R74{*Bq}%-rnEyYq{0o31y@ zH8c38S}W(~HH5`GCA5F2DcJPRh^3OHVBad{G}F6hXmiAdyKT6@@5hzm&j8 zG#q=Y#X$pOzF-9o8YT;0cg>Y@o1tZZ`D#r=%Nj%GRz;<}N@6E}em_%CWRzJeuV>yu>RnB#WabnNl}=wJrBq(a zTorS7QCZ5|L*;v!Qt?ivQiCbWQ|U8jGb*HUx&j8`8d5U_O{=VDvZPAYbfso`ducxC zKL|y-r%)gTl>q+(_3t|Mx6nu@aB|DT2l-zzkSa;55Q1tmR4oH_V|Bd^MEb^I3dK;Ymo(1{j|J*5q;}II0ig zFX3QGtHhSvd!JFeK&ht;9)r(f%0mz=!`aGaO+K5I6{%Vxd?1_sq$-wd4Gw^<$=!iE zQ}IdQafXmIA+HcZ=EtPQEK&wWCH4LV)n!;N+MH}nKhEq@!2T=1ask_inCIZ#dt46p z|DAxa)oETF0c%|cwvxnk!P>fwvyStFftT`wx^z?z!SkiU ze05)8;!~hMSd?S~{|N6c*$?|9*C^}M(u?+Pyj&A*(JqQZFK1!PNifw1#5x-tZ{R1@ zIt~m=<1-*mdGJ~I;3-1R5#l9envhosd7Y4#>fi#A5%PO@Rnq*NI%o~ItH#K*QwTH{ zvSFR&Yn3gA|6K})2niF?M@WQ_C?PQ*CYKfAqOf{0q>)p~`Qn=0t4S4u2#y6Rsdgi` zS~}_B;8w*T-D~%dxrR+wv8k{P^#i!xlX_t0CSCfeu?lK@`0Q^+N( zn&B6>jV`iugNcwIC7?;%{w*K9?=x#^q{x&Desb^OVt zXtN>b&a+}b*5R8@>*)!iJ;Cg{hRQ1WNihA66IqMtzg8@vp0$q*oGBe6B2mqVGkOul zZWdOh9IT$7y_f7B^alO_AR_mN`a9VCu8XGW9fqOVT`xuZzoW*UGZC73P8}e-WVkay zv61iC!SC7Q-?L{8_AD@6Hx(IZCYow%rk&Vi^`qyEfoY@fd_DMgU#!Chp7|1us_q@` z_+pJ!-8;0FF$Qyd#4THVXVZ9rP4YjeLher(GF@qiLu)dA)hO6Ky6*3gRY0nrEs>>9Hl`nCnipHMkFT`nHdmi6ZQb5R z-(+HW? ziJ|6lYqWXWNKEKd^jRp;$Tg=L1tWA6pbW(tR~ywY=bP15PLHRI&x{}t=q=*HJ!g^uy1#RZf>65;e6IY;smi< zkW#Aok&!snE&y!H+o`ROj0@L|^n#vPFv5!fSbVTK(~33cjQF@tMV`C;bYO=9@_fik M2meU{vAXns0QDtSX8-^I literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_urlparse.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpx/__pycache__/_urlparse.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09f3aa578620635a12456d28d75b18264d60e385 GIT binary patch literal 15361 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

G50yx#^*WVh*XvXdaS&#G&+un6{P~QF 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 Response: + """ + Sends an HTTP request. + + **Parameters:** + + * **method** - HTTP method for the new `Request` object: `GET`, `OPTIONS`, + `HEAD`, `POST`, `PUT`, `PATCH`, or `DELETE`. + * **url** - URL for the new `Request` object. + * **params** - *(optional)* Query parameters to include in the URL, as a + string, dictionary, or sequence of two-tuples. + * **content** - *(optional)* Binary content to include in the body of the + request, as bytes or a byte iterator. + * **data** - *(optional)* Form data to include in the body of the request, + as a dictionary. + * **files** - *(optional)* A dictionary of upload files to include in the + body of the request. + * **json** - *(optional)* A JSON serializable object to include in the body + of the request. + * **headers** - *(optional)* Dictionary of HTTP headers to include in the + request. + * **cookies** - *(optional)* Dictionary of Cookie items to include in the + request. + * **auth** - *(optional)* An authentication class to use when sending the + request. + * **proxies** - *(optional)* A dictionary mapping proxy keys to proxy URLs. + * **timeout** - *(optional)* The timeout configuration to use when sending + the request. + * **follow_redirects** - *(optional)* Enables or disables HTTP redirects. + * **verify** - *(optional)* SSL certificates (a.k.a CA bundle) used to + verify the identity of requested hosts. Either `True` (default CA bundle), + a path to an SSL certificate file, an `ssl.SSLContext`, or `False` + (which will disable verification). + * **cert** - *(optional)* An SSL certificate used by the requested host + to authenticate the client. Either a path to an SSL certificate file, or + two-tuple of (certificate file, key file), or a three-tuple of (certificate + file, key file, password). + * **trust_env** - *(optional)* Enables or disables usage of environment + variables for configuration. + + **Returns:** `Response` + + Usage: + + ``` + >>> import httpx + >>> response = httpx.request('GET', 'https://httpbin.org/get') + >>> response + + ``` + """ + with Client( + cookies=cookies, + proxies=proxies, + cert=cert, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) as client: + return client.request( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + auth=auth, + follow_redirects=follow_redirects, + ) + + +@contextmanager +def stream( + method: str, + url: URLTypes, + *, + params: typing.Optional[QueryParamTypes] = None, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, + json: typing.Optional[typing.Any] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Optional[AuthTypes] = None, + proxies: typing.Optional[ProxiesTypes] = None, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + follow_redirects: bool = False, + verify: VerifyTypes = True, + cert: typing.Optional[CertTypes] = None, + trust_env: bool = True, +) -> typing.Iterator[Response]: + """ + Alternative to `httpx.request()` that streams the response body + instead of loading it into memory at once. + + **Parameters**: See `httpx.request`. + + See also: [Streaming Responses][0] + + [0]: /quickstart#streaming-responses + """ + with Client( + cookies=cookies, + proxies=proxies, + cert=cert, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) as client: + with client.stream( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + auth=auth, + follow_redirects=follow_redirects, + ) as response: + yield response + + +def get( + url: URLTypes, + *, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Optional[AuthTypes] = None, + proxies: typing.Optional[ProxiesTypes] = None, + follow_redirects: bool = False, + cert: typing.Optional[CertTypes] = None, + verify: VerifyTypes = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends a `GET` request. + + **Parameters**: See `httpx.request`. + + Note that the `data`, `files`, `json` and `content` parameters are not available + on this function, as `GET` requests should not include a request body. + """ + return request( + "GET", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxies=proxies, + follow_redirects=follow_redirects, + cert=cert, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def options( + url: URLTypes, + *, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Optional[AuthTypes] = None, + proxies: typing.Optional[ProxiesTypes] = None, + follow_redirects: bool = False, + cert: typing.Optional[CertTypes] = None, + verify: VerifyTypes = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends an `OPTIONS` request. + + **Parameters**: See `httpx.request`. + + Note that the `data`, `files`, `json` and `content` parameters are not available + on this function, as `OPTIONS` requests should not include a request body. + """ + return request( + "OPTIONS", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxies=proxies, + follow_redirects=follow_redirects, + cert=cert, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def head( + url: URLTypes, + *, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Optional[AuthTypes] = None, + proxies: typing.Optional[ProxiesTypes] = None, + follow_redirects: bool = False, + cert: typing.Optional[CertTypes] = None, + verify: VerifyTypes = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends a `HEAD` request. + + **Parameters**: See `httpx.request`. + + Note that the `data`, `files`, `json` and `content` parameters are not available + on this function, as `HEAD` requests should not include a request body. + """ + return request( + "HEAD", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxies=proxies, + follow_redirects=follow_redirects, + cert=cert, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def post( + url: URLTypes, + *, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, + json: typing.Optional[typing.Any] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Optional[AuthTypes] = None, + proxies: typing.Optional[ProxiesTypes] = None, + follow_redirects: bool = False, + cert: typing.Optional[CertTypes] = None, + verify: VerifyTypes = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends a `POST` request. + + **Parameters**: See `httpx.request`. + """ + return request( + "POST", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxies=proxies, + follow_redirects=follow_redirects, + cert=cert, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def put( + url: URLTypes, + *, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, + json: typing.Optional[typing.Any] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Optional[AuthTypes] = None, + proxies: typing.Optional[ProxiesTypes] = None, + follow_redirects: bool = False, + cert: typing.Optional[CertTypes] = None, + verify: VerifyTypes = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends a `PUT` request. + + **Parameters**: See `httpx.request`. + """ + return request( + "PUT", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxies=proxies, + follow_redirects=follow_redirects, + cert=cert, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def patch( + url: URLTypes, + *, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, + json: typing.Optional[typing.Any] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Optional[AuthTypes] = None, + proxies: typing.Optional[ProxiesTypes] = None, + follow_redirects: bool = False, + cert: typing.Optional[CertTypes] = None, + verify: VerifyTypes = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends a `PATCH` request. + + **Parameters**: See `httpx.request`. + """ + return request( + "PATCH", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxies=proxies, + follow_redirects=follow_redirects, + cert=cert, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def delete( + url: URLTypes, + *, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Optional[AuthTypes] = None, + proxies: typing.Optional[ProxiesTypes] = None, + follow_redirects: bool = False, + cert: typing.Optional[CertTypes] = None, + verify: VerifyTypes = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends a `DELETE` request. + + **Parameters**: See `httpx.request`. + + Note that the `data`, `files`, `json` and `content` parameters are not available + on this function, as `DELETE` requests should not include a request body. + """ + return request( + "DELETE", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxies=proxies, + follow_redirects=follow_redirects, + cert=cert, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_auth.py b/Backend/venv/lib/python3.12/site-packages/httpx/_auth.py new file mode 100644 index 00000000..1d7385d5 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_auth.py @@ -0,0 +1,347 @@ +import hashlib +import netrc +import os +import re +import time +import typing +from base64 import b64encode +from urllib.request import parse_http_list + +from ._exceptions import ProtocolError +from ._models import Request, Response +from ._utils import to_bytes, to_str, unquote + +if typing.TYPE_CHECKING: # pragma: no cover + from hashlib import _Hash + + +class Auth: + """ + Base class for all authentication schemes. + + To implement a custom authentication scheme, subclass `Auth` and override + the `.auth_flow()` method. + + If the authentication scheme does I/O such as disk access or network calls, or uses + synchronization primitives such as locks, you should override `.sync_auth_flow()` + and/or `.async_auth_flow()` instead of `.auth_flow()` to provide specialized + implementations that will be used by `Client` and `AsyncClient` respectively. + """ + + requires_request_body = False + requires_response_body = False + + def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: + """ + Execute the authentication flow. + + To dispatch a request, `yield` it: + + ``` + yield request + ``` + + The client will `.send()` the response back into the flow generator. You can + access it like so: + + ``` + response = yield request + ``` + + A `return` (or reaching the end of the generator) will result in the + client returning the last response obtained from the server. + + You can dispatch as many requests as is necessary. + """ + yield request + + def sync_auth_flow( + self, request: Request + ) -> typing.Generator[Request, Response, None]: + """ + Execute the authentication flow synchronously. + + By default, this defers to `.auth_flow()`. You should override this method + when the authentication scheme does I/O and/or uses concurrency primitives. + """ + if self.requires_request_body: + request.read() + + flow = self.auth_flow(request) + request = next(flow) + + while True: + response = yield request + if self.requires_response_body: + response.read() + + try: + request = flow.send(response) + except StopIteration: + break + + async def async_auth_flow( + self, request: Request + ) -> typing.AsyncGenerator[Request, Response]: + """ + Execute the authentication flow asynchronously. + + By default, this defers to `.auth_flow()`. You should override this method + when the authentication scheme does I/O and/or uses concurrency primitives. + """ + if self.requires_request_body: + await request.aread() + + flow = self.auth_flow(request) + request = next(flow) + + while True: + response = yield request + if self.requires_response_body: + await response.aread() + + try: + request = flow.send(response) + except StopIteration: + break + + +class FunctionAuth(Auth): + """ + Allows the 'auth' argument to be passed as a simple callable function, + that takes the request, and returns a new, modified request. + """ + + def __init__(self, func: typing.Callable[[Request], Request]) -> None: + self._func = func + + def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: + yield self._func(request) + + +class BasicAuth(Auth): + """ + Allows the 'auth' argument to be passed as a (username, password) pair, + and uses HTTP Basic authentication. + """ + + def __init__( + self, username: typing.Union[str, bytes], password: typing.Union[str, bytes] + ): + self._auth_header = self._build_auth_header(username, password) + + def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: + request.headers["Authorization"] = self._auth_header + yield request + + def _build_auth_header( + self, username: typing.Union[str, bytes], password: typing.Union[str, bytes] + ) -> str: + userpass = b":".join((to_bytes(username), to_bytes(password))) + token = b64encode(userpass).decode() + return f"Basic {token}" + + +class NetRCAuth(Auth): + """ + Use a 'netrc' file to lookup basic auth credentials based on the url host. + """ + + def __init__(self, file: typing.Optional[str] = None): + self._netrc_info = netrc.netrc(file) + + def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: + auth_info = self._netrc_info.authenticators(request.url.host) + if auth_info is None or not auth_info[2]: + # The netrc file did not have authentication credentials for this host. + yield request + else: + # Build a basic auth header with credentials from the netrc file. + request.headers["Authorization"] = self._build_auth_header( + username=auth_info[0], password=auth_info[2] + ) + yield request + + def _build_auth_header( + self, username: typing.Union[str, bytes], password: typing.Union[str, bytes] + ) -> str: + userpass = b":".join((to_bytes(username), to_bytes(password))) + token = b64encode(userpass).decode() + return f"Basic {token}" + + +class DigestAuth(Auth): + _ALGORITHM_TO_HASH_FUNCTION: typing.Dict[str, typing.Callable[[bytes], "_Hash"]] = { + "MD5": hashlib.md5, + "MD5-SESS": hashlib.md5, + "SHA": hashlib.sha1, + "SHA-SESS": hashlib.sha1, + "SHA-256": hashlib.sha256, + "SHA-256-SESS": hashlib.sha256, + "SHA-512": hashlib.sha512, + "SHA-512-SESS": hashlib.sha512, + } + + def __init__( + self, username: typing.Union[str, bytes], password: typing.Union[str, bytes] + ) -> None: + self._username = to_bytes(username) + self._password = to_bytes(password) + self._last_challenge: typing.Optional[_DigestAuthChallenge] = None + self._nonce_count = 1 + + def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: + if self._last_challenge: + request.headers["Authorization"] = self._build_auth_header( + request, self._last_challenge + ) + + response = yield request + + if response.status_code != 401 or "www-authenticate" not in response.headers: + # If the response is not a 401 then we don't + # need to build an authenticated request. + return + + for auth_header in response.headers.get_list("www-authenticate"): + if auth_header.lower().startswith("digest "): + break + else: + # If the response does not include a 'WWW-Authenticate: Digest ...' + # header, then we don't need to build an authenticated request. + return + + self._last_challenge = self._parse_challenge(request, response, auth_header) + self._nonce_count = 1 + + request.headers["Authorization"] = self._build_auth_header( + request, self._last_challenge + ) + yield request + + def _parse_challenge( + self, request: Request, response: Response, auth_header: str + ) -> "_DigestAuthChallenge": + """ + Returns a challenge from a Digest WWW-Authenticate header. + These take the form of: + `Digest realm="realm@host.com",qop="auth,auth-int",nonce="abc",opaque="xyz"` + """ + scheme, _, fields = auth_header.partition(" ") + + # This method should only ever have been called with a Digest auth header. + assert scheme.lower() == "digest" + + header_dict: typing.Dict[str, str] = {} + for field in parse_http_list(fields): + key, value = field.strip().split("=", 1) + header_dict[key] = unquote(value) + + try: + realm = header_dict["realm"].encode() + nonce = header_dict["nonce"].encode() + algorithm = header_dict.get("algorithm", "MD5") + opaque = header_dict["opaque"].encode() if "opaque" in header_dict else None + qop = header_dict["qop"].encode() if "qop" in header_dict else None + return _DigestAuthChallenge( + realm=realm, nonce=nonce, algorithm=algorithm, opaque=opaque, qop=qop + ) + except KeyError as exc: + message = "Malformed Digest WWW-Authenticate header" + raise ProtocolError(message, request=request) from exc + + def _build_auth_header( + self, request: Request, challenge: "_DigestAuthChallenge" + ) -> str: + hash_func = self._ALGORITHM_TO_HASH_FUNCTION[challenge.algorithm.upper()] + + def digest(data: bytes) -> bytes: + return hash_func(data).hexdigest().encode() + + A1 = b":".join((self._username, challenge.realm, self._password)) + + path = request.url.raw_path + A2 = b":".join((request.method.encode(), path)) + # TODO: implement auth-int + HA2 = digest(A2) + + nc_value = b"%08x" % self._nonce_count + cnonce = self._get_client_nonce(self._nonce_count, challenge.nonce) + self._nonce_count += 1 + + HA1 = digest(A1) + if challenge.algorithm.lower().endswith("-sess"): + HA1 = digest(b":".join((HA1, challenge.nonce, cnonce))) + + qop = self._resolve_qop(challenge.qop, request=request) + if qop is None: + digest_data = [HA1, challenge.nonce, HA2] + else: + digest_data = [challenge.nonce, nc_value, cnonce, qop, HA2] + key_digest = b":".join(digest_data) + + format_args = { + "username": self._username, + "realm": challenge.realm, + "nonce": challenge.nonce, + "uri": path, + "response": digest(b":".join((HA1, key_digest))), + "algorithm": challenge.algorithm.encode(), + } + if challenge.opaque: + format_args["opaque"] = challenge.opaque + if qop: + format_args["qop"] = b"auth" + format_args["nc"] = nc_value + format_args["cnonce"] = cnonce + + return "Digest " + self._get_header_value(format_args) + + def _get_client_nonce(self, nonce_count: int, nonce: bytes) -> bytes: + s = str(nonce_count).encode() + s += nonce + s += time.ctime().encode() + s += os.urandom(8) + + return hashlib.sha1(s).hexdigest()[:16].encode() + + def _get_header_value(self, header_fields: typing.Dict[str, bytes]) -> str: + NON_QUOTED_FIELDS = ("algorithm", "qop", "nc") + QUOTED_TEMPLATE = '{}="{}"' + NON_QUOTED_TEMPLATE = "{}={}" + + header_value = "" + for i, (field, value) in enumerate(header_fields.items()): + if i > 0: + header_value += ", " + template = ( + QUOTED_TEMPLATE + if field not in NON_QUOTED_FIELDS + else NON_QUOTED_TEMPLATE + ) + header_value += template.format(field, to_str(value)) + + return header_value + + def _resolve_qop( + self, qop: typing.Optional[bytes], request: Request + ) -> typing.Optional[bytes]: + if qop is None: + return None + qops = re.split(b", ?", qop) + if b"auth" in qops: + return b"auth" + + if qops == [b"auth-int"]: + raise NotImplementedError("Digest auth-int support is not yet implemented") + + message = f'Unexpected qop value "{qop!r}" in digest auth' + raise ProtocolError(message, request=request) + + +class _DigestAuthChallenge(typing.NamedTuple): + realm: bytes + nonce: bytes + algorithm: str + opaque: typing.Optional[bytes] + qop: typing.Optional[bytes] diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_client.py b/Backend/venv/lib/python3.12/site-packages/httpx/_client.py new file mode 100644 index 00000000..cb475e02 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_client.py @@ -0,0 +1,2006 @@ +import datetime +import enum +import logging +import typing +import warnings +from contextlib import asynccontextmanager, contextmanager +from types import TracebackType + +from .__version__ import __version__ +from ._auth import Auth, BasicAuth, FunctionAuth +from ._config import ( + DEFAULT_LIMITS, + DEFAULT_MAX_REDIRECTS, + DEFAULT_TIMEOUT_CONFIG, + Limits, + Proxy, + Timeout, +) +from ._decoders import SUPPORTED_DECODERS +from ._exceptions import ( + InvalidURL, + RemoteProtocolError, + TooManyRedirects, + request_context, +) +from ._models import Cookies, Headers, Request, Response +from ._status_codes import codes +from ._transports.asgi import ASGITransport +from ._transports.base import AsyncBaseTransport, BaseTransport +from ._transports.default import AsyncHTTPTransport, HTTPTransport +from ._transports.wsgi import WSGITransport +from ._types import ( + AsyncByteStream, + AuthTypes, + CertTypes, + CookieTypes, + HeaderTypes, + ProxiesTypes, + QueryParamTypes, + RequestContent, + RequestData, + RequestExtensions, + RequestFiles, + SyncByteStream, + TimeoutTypes, + URLTypes, + VerifyTypes, +) +from ._urls import URL, QueryParams +from ._utils import ( + Timer, + URLPattern, + get_environment_proxies, + is_https_redirect, + same_origin, +) + +# The type annotation for @classmethod and context managers here follows PEP 484 +# https://www.python.org/dev/peps/pep-0484/#annotating-instance-and-class-methods +T = typing.TypeVar("T", bound="Client") +U = typing.TypeVar("U", bound="AsyncClient") + + +class UseClientDefault: + """ + For some parameters such as `auth=...` and `timeout=...` we need to be able + to indicate the default "unset" state, in a way that is distinctly different + to using `None`. + + The default "unset" state indicates that whatever default is set on the + client should be used. This is different to setting `None`, which + explicitly disables the parameter, possibly overriding a client default. + + For example we use `timeout=USE_CLIENT_DEFAULT` in the `request()` signature. + Omitting the `timeout` parameter will send a request using whatever default + timeout has been configured on the client. Including `timeout=None` will + ensure no timeout is used. + + Note that user code shouldn't need to use the `USE_CLIENT_DEFAULT` constant, + but it is used internally when a parameter is not included. + """ + + +USE_CLIENT_DEFAULT = UseClientDefault() + + +logger = logging.getLogger("httpx") + +USER_AGENT = f"python-httpx/{__version__}" +ACCEPT_ENCODING = ", ".join( + [key for key in SUPPORTED_DECODERS.keys() if key != "identity"] +) + + +class ClientState(enum.Enum): + # UNOPENED: + # The client has been instantiated, but has not been used to send a request, + # or been opened by entering the context of a `with` block. + UNOPENED = 1 + # OPENED: + # The client has either sent a request, or is within a `with` block. + OPENED = 2 + # CLOSED: + # The client has either exited the `with` block, or `close()` has + # been called explicitly. + CLOSED = 3 + + +class BoundSyncStream(SyncByteStream): + """ + A byte stream that is bound to a given response instance, and that + ensures the `response.elapsed` is set once the response is closed. + """ + + def __init__( + self, stream: SyncByteStream, response: Response, timer: Timer + ) -> None: + self._stream = stream + self._response = response + self._timer = timer + + def __iter__(self) -> typing.Iterator[bytes]: + for chunk in self._stream: + yield chunk + + def close(self) -> None: + seconds = self._timer.sync_elapsed() + self._response.elapsed = datetime.timedelta(seconds=seconds) + self._stream.close() + + +class BoundAsyncStream(AsyncByteStream): + """ + An async byte stream that is bound to a given response instance, and that + ensures the `response.elapsed` is set once the response is closed. + """ + + def __init__( + self, stream: AsyncByteStream, response: Response, timer: Timer + ) -> None: + self._stream = stream + self._response = response + self._timer = timer + + async def __aiter__(self) -> typing.AsyncIterator[bytes]: + async for chunk in self._stream: + yield chunk + + async def aclose(self) -> None: + seconds = await self._timer.async_elapsed() + self._response.elapsed = datetime.timedelta(seconds=seconds) + await self._stream.aclose() + + +EventHook = typing.Callable[..., typing.Any] + + +class BaseClient: + def __init__( + self, + *, + auth: typing.Optional[AuthTypes] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + follow_redirects: bool = False, + max_redirects: int = DEFAULT_MAX_REDIRECTS, + event_hooks: typing.Optional[ + typing.Mapping[str, typing.List[EventHook]] + ] = None, + base_url: URLTypes = "", + trust_env: bool = True, + default_encoding: typing.Union[str, typing.Callable[[bytes], str]] = "utf-8", + ): + event_hooks = {} if event_hooks is None else event_hooks + + self._base_url = self._enforce_trailing_slash(URL(base_url)) + + self._auth = self._build_auth(auth) + self._params = QueryParams(params) + self.headers = Headers(headers) + self._cookies = Cookies(cookies) + self._timeout = Timeout(timeout) + self.follow_redirects = follow_redirects + self.max_redirects = max_redirects + self._event_hooks = { + "request": list(event_hooks.get("request", [])), + "response": list(event_hooks.get("response", [])), + } + self._trust_env = trust_env + self._default_encoding = default_encoding + self._state = ClientState.UNOPENED + + @property + def is_closed(self) -> bool: + """ + Check if the client being closed + """ + return self._state == ClientState.CLOSED + + @property + def trust_env(self) -> bool: + return self._trust_env + + def _enforce_trailing_slash(self, url: URL) -> URL: + if url.raw_path.endswith(b"/"): + return url + return url.copy_with(raw_path=url.raw_path + b"/") + + def _get_proxy_map( + self, proxies: typing.Optional[ProxiesTypes], allow_env_proxies: bool + ) -> typing.Dict[str, typing.Optional[Proxy]]: + if proxies is None: + if allow_env_proxies: + return { + key: None if url is None else Proxy(url=url) + for key, url in get_environment_proxies().items() + } + return {} + if isinstance(proxies, dict): + new_proxies = {} + for key, value in proxies.items(): + proxy = Proxy(url=value) if isinstance(value, (str, URL)) else value + new_proxies[str(key)] = proxy + return new_proxies + else: + proxy = Proxy(url=proxies) if isinstance(proxies, (str, URL)) else proxies + return {"all://": proxy} + + @property + def timeout(self) -> Timeout: + return self._timeout + + @timeout.setter + def timeout(self, timeout: TimeoutTypes) -> None: + self._timeout = Timeout(timeout) + + @property + def event_hooks(self) -> typing.Dict[str, typing.List[EventHook]]: + return self._event_hooks + + @event_hooks.setter + def event_hooks( + self, event_hooks: typing.Dict[str, typing.List[EventHook]] + ) -> None: + self._event_hooks = { + "request": list(event_hooks.get("request", [])), + "response": list(event_hooks.get("response", [])), + } + + @property + def auth(self) -> typing.Optional[Auth]: + """ + Authentication class used when none is passed at the request-level. + + See also [Authentication][0]. + + [0]: /quickstart/#authentication + """ + return self._auth + + @auth.setter + def auth(self, auth: AuthTypes) -> None: + self._auth = self._build_auth(auth) + + @property + def base_url(self) -> URL: + """ + Base URL to use when sending requests with relative URLs. + """ + return self._base_url + + @base_url.setter + def base_url(self, url: URLTypes) -> None: + self._base_url = self._enforce_trailing_slash(URL(url)) + + @property + def headers(self) -> Headers: + """ + HTTP headers to include when sending requests. + """ + return self._headers + + @headers.setter + def headers(self, headers: HeaderTypes) -> None: + client_headers = Headers( + { + b"Accept": b"*/*", + b"Accept-Encoding": ACCEPT_ENCODING.encode("ascii"), + b"Connection": b"keep-alive", + b"User-Agent": USER_AGENT.encode("ascii"), + } + ) + client_headers.update(headers) + self._headers = client_headers + + @property + def cookies(self) -> Cookies: + """ + Cookie values to include when sending requests. + """ + return self._cookies + + @cookies.setter + def cookies(self, cookies: CookieTypes) -> None: + self._cookies = Cookies(cookies) + + @property + def params(self) -> QueryParams: + """ + Query parameters to include in the URL when sending requests. + """ + return self._params + + @params.setter + def params(self, params: QueryParamTypes) -> None: + self._params = QueryParams(params) + + def build_request( + self, + method: str, + url: URLTypes, + *, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, + json: typing.Optional[typing.Any] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> Request: + """ + Build and return a request instance. + + * The `params`, `headers` and `cookies` arguments + are merged with any values set on the client. + * The `url` argument is merged with any `base_url` set on the client. + + See also: [Request instances][0] + + [0]: /advanced/#request-instances + """ + url = self._merge_url(url) + headers = self._merge_headers(headers) + cookies = self._merge_cookies(cookies) + params = self._merge_queryparams(params) + extensions = {} if extensions is None else extensions + if "timeout" not in extensions: + timeout = ( + self.timeout + if isinstance(timeout, UseClientDefault) + else Timeout(timeout) + ) + extensions = dict(**extensions, timeout=timeout.as_dict()) + return Request( + method, + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + extensions=extensions, + ) + + def _merge_url(self, url: URLTypes) -> URL: + """ + Merge a URL argument together with any 'base_url' on the client, + to create the URL used for the outgoing request. + """ + merge_url = URL(url) + if merge_url.is_relative_url: + # To merge URLs we always append to the base URL. To get this + # behaviour correct we always ensure the base URL ends in a '/' + # separator, and strip any leading '/' from the merge URL. + # + # So, eg... + # + # >>> client = Client(base_url="https://www.example.com/subpath") + # >>> client.base_url + # URL('https://www.example.com/subpath/') + # >>> client.build_request("GET", "/path").url + # URL('https://www.example.com/subpath/path') + merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/") + return self.base_url.copy_with(raw_path=merge_raw_path) + return merge_url + + def _merge_cookies( + self, cookies: typing.Optional[CookieTypes] = None + ) -> typing.Optional[CookieTypes]: + """ + Merge a cookies argument together with any cookies on the client, + to create the cookies used for the outgoing request. + """ + if cookies or self.cookies: + merged_cookies = Cookies(self.cookies) + merged_cookies.update(cookies) + return merged_cookies + return cookies + + def _merge_headers( + self, headers: typing.Optional[HeaderTypes] = None + ) -> typing.Optional[HeaderTypes]: + """ + Merge a headers argument together with any headers on the client, + to create the headers used for the outgoing request. + """ + merged_headers = Headers(self.headers) + merged_headers.update(headers) + return merged_headers + + def _merge_queryparams( + self, params: typing.Optional[QueryParamTypes] = None + ) -> typing.Optional[QueryParamTypes]: + """ + Merge a queryparams argument together with any queryparams on the client, + to create the queryparams used for the outgoing request. + """ + if params or self.params: + merged_queryparams = QueryParams(self.params) + return merged_queryparams.merge(params) + return params + + def _build_auth(self, auth: typing.Optional[AuthTypes]) -> typing.Optional[Auth]: + if auth is None: + return None + elif isinstance(auth, tuple): + return BasicAuth(username=auth[0], password=auth[1]) + elif isinstance(auth, Auth): + return auth + elif callable(auth): + return FunctionAuth(func=auth) + else: + raise TypeError(f'Invalid "auth" argument: {auth!r}') + + def _build_request_auth( + self, + request: Request, + auth: typing.Union[AuthTypes, UseClientDefault, None] = USE_CLIENT_DEFAULT, + ) -> Auth: + auth = ( + self._auth if isinstance(auth, UseClientDefault) else self._build_auth(auth) + ) + + if auth is not None: + return auth + + username, password = request.url.username, request.url.password + if username or password: + return BasicAuth(username=username, password=password) + + return Auth() + + def _build_redirect_request(self, request: Request, response: Response) -> Request: + """ + Given a request and a redirect response, return a new request that + should be used to effect the redirect. + """ + method = self._redirect_method(request, response) + url = self._redirect_url(request, response) + headers = self._redirect_headers(request, url, method) + stream = self._redirect_stream(request, method) + cookies = Cookies(self.cookies) + return Request( + method=method, + url=url, + headers=headers, + cookies=cookies, + stream=stream, + extensions=request.extensions, + ) + + def _redirect_method(self, request: Request, response: Response) -> str: + """ + When being redirected we may want to change the method of the request + based on certain specs or browser behavior. + """ + method = request.method + + # https://tools.ietf.org/html/rfc7231#section-6.4.4 + if response.status_code == codes.SEE_OTHER and method != "HEAD": + method = "GET" + + # Do what the browsers do, despite standards... + # Turn 302s into GETs. + if response.status_code == codes.FOUND and method != "HEAD": + method = "GET" + + # If a POST is responded to with a 301, turn it into a GET. + # This bizarre behaviour is explained in 'requests' issue 1704. + if response.status_code == codes.MOVED_PERMANENTLY and method == "POST": + method = "GET" + + return method + + def _redirect_url(self, request: Request, response: Response) -> URL: + """ + Return the URL for the redirect to follow. + """ + location = response.headers["Location"] + + try: + url = URL(location) + except InvalidURL as exc: + raise RemoteProtocolError( + f"Invalid URL in location header: {exc}.", request=request + ) from None + + # Handle malformed 'Location' headers that are "absolute" form, have no host. + # See: https://github.com/encode/httpx/issues/771 + if url.scheme and not url.host: + url = url.copy_with(host=request.url.host) + + # Facilitate relative 'Location' headers, as allowed by RFC 7231. + # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource') + if url.is_relative_url: + url = request.url.join(url) + + # Attach previous fragment if needed (RFC 7231 7.1.2) + if request.url.fragment and not url.fragment: + url = url.copy_with(fragment=request.url.fragment) + + return url + + def _redirect_headers(self, request: Request, url: URL, method: str) -> Headers: + """ + Return the headers that should be used for the redirect request. + """ + headers = Headers(request.headers) + + if not same_origin(url, request.url): + if not is_https_redirect(request.url, url): + # Strip Authorization headers when responses are redirected + # away from the origin. (Except for direct HTTP to HTTPS redirects.) + headers.pop("Authorization", None) + + # Update the Host header. + headers["Host"] = url.netloc.decode("ascii") + + if method != request.method and method == "GET": + # If we've switch to a 'GET' request, then strip any headers which + # are only relevant to the request body. + headers.pop("Content-Length", None) + headers.pop("Transfer-Encoding", None) + + # We should use the client cookie store to determine any cookie header, + # rather than whatever was on the original outgoing request. + headers.pop("Cookie", None) + + return headers + + def _redirect_stream( + self, request: Request, method: str + ) -> typing.Optional[typing.Union[SyncByteStream, AsyncByteStream]]: + """ + Return the body that should be used for the redirect request. + """ + if method != request.method and method == "GET": + return None + + return request.stream + + +class Client(BaseClient): + """ + An HTTP client, with connection pooling, HTTP/2, redirects, cookie persistence, etc. + + It can be shared between threads. + + Usage: + + ```python + >>> client = httpx.Client() + >>> response = client.get('https://example.org') + ``` + + **Parameters:** + + * **auth** - *(optional)* An authentication class to use when sending + requests. + * **params** - *(optional)* Query parameters to include in request URLs, as + a string, dictionary, or sequence of two-tuples. + * **headers** - *(optional)* Dictionary of HTTP headers to include when + sending requests. + * **cookies** - *(optional)* Dictionary of Cookie items to include when + sending requests. + * **verify** - *(optional)* SSL certificates (a.k.a CA bundle) used to + verify the identity of requested hosts. Either `True` (default CA bundle), + a path to an SSL certificate file, an `ssl.SSLContext`, or `False` + (which will disable verification). + * **cert** - *(optional)* An SSL certificate used by the requested host + to authenticate the client. Either a path to an SSL certificate file, or + two-tuple of (certificate file, key file), or a three-tuple of (certificate + file, key file, password). + * **proxies** - *(optional)* A dictionary mapping proxy keys to proxy + URLs. + * **timeout** - *(optional)* The timeout configuration to use when sending + requests. + * **limits** - *(optional)* The limits configuration to use. + * **max_redirects** - *(optional)* The maximum number of redirect responses + that should be followed. + * **base_url** - *(optional)* A URL to use as the base when building + request URLs. + * **transport** - *(optional)* A transport class to use for sending requests + over the network. + * **app** - *(optional)* An WSGI application to send requests to, + rather than sending actual network requests. + * **trust_env** - *(optional)* Enables or disables usage of environment + variables for configuration. + * **default_encoding** - *(optional)* The default encoding to use for decoding + response text, if no charset information is included in a response Content-Type + header. Set to a callable for automatic character set detection. Default: "utf-8". + """ + + def __init__( + self, + *, + auth: typing.Optional[AuthTypes] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + verify: VerifyTypes = True, + cert: typing.Optional[CertTypes] = None, + http1: bool = True, + http2: bool = False, + proxies: typing.Optional[ProxiesTypes] = None, + mounts: typing.Optional[typing.Mapping[str, BaseTransport]] = None, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + follow_redirects: bool = False, + limits: Limits = DEFAULT_LIMITS, + max_redirects: int = DEFAULT_MAX_REDIRECTS, + event_hooks: typing.Optional[ + typing.Mapping[str, typing.List[EventHook]] + ] = None, + base_url: URLTypes = "", + transport: typing.Optional[BaseTransport] = None, + app: typing.Optional[typing.Callable[..., typing.Any]] = None, + trust_env: bool = True, + default_encoding: typing.Union[str, typing.Callable[[bytes], str]] = "utf-8", + ): + super().__init__( + auth=auth, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + follow_redirects=follow_redirects, + max_redirects=max_redirects, + event_hooks=event_hooks, + base_url=base_url, + trust_env=trust_env, + default_encoding=default_encoding, + ) + + if http2: + try: + import h2 # noqa + except ImportError: # pragma: no cover + raise ImportError( + "Using http2=True, but the 'h2' package is not installed. " + "Make sure to install httpx using `pip install httpx[http2]`." + ) from None + + allow_env_proxies = trust_env and app is None and transport is None + proxy_map = self._get_proxy_map(proxies, allow_env_proxies) + + self._transport = self._init_transport( + verify=verify, + cert=cert, + http1=http1, + http2=http2, + limits=limits, + transport=transport, + app=app, + trust_env=trust_env, + ) + self._mounts: typing.Dict[URLPattern, typing.Optional[BaseTransport]] = { + URLPattern(key): None + if proxy is None + else self._init_proxy_transport( + proxy, + verify=verify, + cert=cert, + http1=http1, + http2=http2, + limits=limits, + trust_env=trust_env, + ) + for key, proxy in proxy_map.items() + } + if mounts is not None: + self._mounts.update( + {URLPattern(key): transport for key, transport in mounts.items()} + ) + + self._mounts = dict(sorted(self._mounts.items())) + + def _init_transport( + self, + verify: VerifyTypes = True, + cert: typing.Optional[CertTypes] = None, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + transport: typing.Optional[BaseTransport] = None, + app: typing.Optional[typing.Callable[..., typing.Any]] = None, + trust_env: bool = True, + ) -> BaseTransport: + if transport is not None: + return transport + + if app is not None: + return WSGITransport(app=app) + + return HTTPTransport( + verify=verify, + cert=cert, + http1=http1, + http2=http2, + limits=limits, + trust_env=trust_env, + ) + + def _init_proxy_transport( + self, + proxy: Proxy, + verify: VerifyTypes = True, + cert: typing.Optional[CertTypes] = None, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + trust_env: bool = True, + ) -> BaseTransport: + return HTTPTransport( + verify=verify, + cert=cert, + http1=http1, + http2=http2, + limits=limits, + trust_env=trust_env, + proxy=proxy, + ) + + def _transport_for_url(self, url: URL) -> BaseTransport: + """ + Returns the transport instance that should be used for a given URL. + This will either be the standard connection pool, or a proxy. + """ + for pattern, transport in self._mounts.items(): + if pattern.matches(url): + return self._transport if transport is None else transport + + return self._transport + + def request( + self, + method: str, + url: URLTypes, + *, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, + json: typing.Optional[typing.Any] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault, None] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> Response: + """ + Build and send a request. + + Equivalent to: + + ```python + request = client.build_request(...) + response = client.send(request, ...) + ``` + + See `Client.build_request()`, `Client.send()` and + [Merging of configuration][0] for how the various parameters + are merged with client-level configuration. + + [0]: /advanced/#merging-of-configuration + """ + if cookies is not None: + message = ( + "Setting per-request cookies=<...> is being deprecated, because " + "the expected behaviour on cookie persistence is ambiguous. Set " + "cookies directly on the client instance instead." + ) + warnings.warn(message, DeprecationWarning) + + request = self.build_request( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + extensions=extensions, + ) + return self.send(request, auth=auth, follow_redirects=follow_redirects) + + @contextmanager + def stream( + self, + method: str, + url: URLTypes, + *, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, + json: typing.Optional[typing.Any] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault, None] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> typing.Iterator[Response]: + """ + Alternative to `httpx.request()` that streams the response body + instead of loading it into memory at once. + + **Parameters**: See `httpx.request`. + + See also: [Streaming Responses][0] + + [0]: /quickstart#streaming-responses + """ + request = self.build_request( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + extensions=extensions, + ) + response = self.send( + request=request, + auth=auth, + follow_redirects=follow_redirects, + stream=True, + ) + try: + yield response + finally: + response.close() + + def send( + self, + request: Request, + *, + stream: bool = False, + auth: typing.Union[AuthTypes, UseClientDefault, None] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + ) -> Response: + """ + Send a request. + + The request is sent as-is, unmodified. + + Typically you'll want to build one with `Client.build_request()` + so that any client-level configuration is merged into the request, + but passing an explicit `httpx.Request()` is supported as well. + + See also: [Request instances][0] + + [0]: /advanced/#request-instances + """ + if self._state == ClientState.CLOSED: + raise RuntimeError("Cannot send a request, as the client has been closed.") + + self._state = ClientState.OPENED + follow_redirects = ( + self.follow_redirects + if isinstance(follow_redirects, UseClientDefault) + else follow_redirects + ) + + auth = self._build_request_auth(request, auth) + + response = self._send_handling_auth( + request, + auth=auth, + follow_redirects=follow_redirects, + history=[], + ) + try: + if not stream: + response.read() + + return response + + except BaseException as exc: + response.close() + raise exc + + def _send_handling_auth( + self, + request: Request, + auth: Auth, + follow_redirects: bool, + history: typing.List[Response], + ) -> Response: + auth_flow = auth.sync_auth_flow(request) + try: + request = next(auth_flow) + + while True: + response = self._send_handling_redirects( + request, + follow_redirects=follow_redirects, + history=history, + ) + try: + try: + next_request = auth_flow.send(response) + except StopIteration: + return response + + response.history = list(history) + response.read() + request = next_request + history.append(response) + + except BaseException as exc: + response.close() + raise exc + finally: + auth_flow.close() + + def _send_handling_redirects( + self, + request: Request, + follow_redirects: bool, + history: typing.List[Response], + ) -> Response: + while True: + if len(history) > self.max_redirects: + raise TooManyRedirects( + "Exceeded maximum allowed redirects.", request=request + ) + + for hook in self._event_hooks["request"]: + hook(request) + + response = self._send_single_request(request) + try: + for hook in self._event_hooks["response"]: + hook(response) + response.history = list(history) + + if not response.has_redirect_location: + return response + + request = self._build_redirect_request(request, response) + history = history + [response] + + if follow_redirects: + response.read() + else: + response.next_request = request + return response + + except BaseException as exc: + response.close() + raise exc + + def _send_single_request(self, request: Request) -> Response: + """ + Sends a single request, without handling any redirections. + """ + transport = self._transport_for_url(request.url) + timer = Timer() + timer.sync_start() + + if not isinstance(request.stream, SyncByteStream): + raise RuntimeError( + "Attempted to send an async request with a sync Client instance." + ) + + with request_context(request=request): + response = transport.handle_request(request) + + assert isinstance(response.stream, SyncByteStream) + + response.request = request + response.stream = BoundSyncStream( + response.stream, response=response, timer=timer + ) + self.cookies.extract_cookies(response) + response.default_encoding = self._default_encoding + + logger.info( + 'HTTP Request: %s %s "%s %d %s"', + request.method, + request.url, + response.http_version, + response.status_code, + response.reason_phrase, + ) + + return response + + def get( + self, + url: URLTypes, + *, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> Response: + """ + Send a `GET` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "GET", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def options( + self, + url: URLTypes, + *, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> Response: + """ + Send an `OPTIONS` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "OPTIONS", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def head( + self, + url: URLTypes, + *, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> Response: + """ + Send a `HEAD` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "HEAD", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def post( + self, + url: URLTypes, + *, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, + json: typing.Optional[typing.Any] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> Response: + """ + Send a `POST` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "POST", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def put( + self, + url: URLTypes, + *, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, + json: typing.Optional[typing.Any] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> Response: + """ + Send a `PUT` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "PUT", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def patch( + self, + url: URLTypes, + *, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, + json: typing.Optional[typing.Any] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> Response: + """ + Send a `PATCH` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "PATCH", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def delete( + self, + url: URLTypes, + *, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> Response: + """ + Send a `DELETE` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "DELETE", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def close(self) -> None: + """ + Close transport and proxies. + """ + if self._state != ClientState.CLOSED: + self._state = ClientState.CLOSED + + self._transport.close() + for transport in self._mounts.values(): + if transport is not None: + transport.close() + + def __enter__(self: T) -> T: + if self._state != ClientState.UNOPENED: + msg = { + ClientState.OPENED: "Cannot open a client instance more than once.", + ClientState.CLOSED: "Cannot reopen a client instance, once it has been closed.", + }[self._state] + raise RuntimeError(msg) + + self._state = ClientState.OPENED + + self._transport.__enter__() + for transport in self._mounts.values(): + if transport is not None: + transport.__enter__() + return self + + def __exit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]] = None, + exc_value: typing.Optional[BaseException] = None, + traceback: typing.Optional[TracebackType] = None, + ) -> None: + self._state = ClientState.CLOSED + + self._transport.__exit__(exc_type, exc_value, traceback) + for transport in self._mounts.values(): + if transport is not None: + transport.__exit__(exc_type, exc_value, traceback) + + +class AsyncClient(BaseClient): + """ + An asynchronous HTTP client, with connection pooling, HTTP/2, redirects, + cookie persistence, etc. + + Usage: + + ```python + >>> async with httpx.AsyncClient() as client: + >>> response = await client.get('https://example.org') + ``` + + **Parameters:** + + * **auth** - *(optional)* An authentication class to use when sending + requests. + * **params** - *(optional)* Query parameters to include in request URLs, as + a string, dictionary, or sequence of two-tuples. + * **headers** - *(optional)* Dictionary of HTTP headers to include when + sending requests. + * **cookies** - *(optional)* Dictionary of Cookie items to include when + sending requests. + * **verify** - *(optional)* SSL certificates (a.k.a CA bundle) used to + verify the identity of requested hosts. Either `True` (default CA bundle), + a path to an SSL certificate file, an `ssl.SSLContext`, or `False` + (which will disable verification). + * **cert** - *(optional)* An SSL certificate used by the requested host + to authenticate the client. Either a path to an SSL certificate file, or + two-tuple of (certificate file, key file), or a three-tuple of (certificate + file, key file, password). + * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be + enabled. Defaults to `False`. + * **proxies** - *(optional)* A dictionary mapping HTTP protocols to proxy + URLs. + * **timeout** - *(optional)* The timeout configuration to use when sending + requests. + * **limits** - *(optional)* The limits configuration to use. + * **max_redirects** - *(optional)* The maximum number of redirect responses + that should be followed. + * **base_url** - *(optional)* A URL to use as the base when building + request URLs. + * **transport** - *(optional)* A transport class to use for sending requests + over the network. + * **app** - *(optional)* An ASGI application to send requests to, + rather than sending actual network requests. + * **trust_env** - *(optional)* Enables or disables usage of environment + variables for configuration. + * **default_encoding** - *(optional)* The default encoding to use for decoding + response text, if no charset information is included in a response Content-Type + header. Set to a callable for automatic character set detection. Default: "utf-8". + """ + + def __init__( + self, + *, + auth: typing.Optional[AuthTypes] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + verify: VerifyTypes = True, + cert: typing.Optional[CertTypes] = None, + http1: bool = True, + http2: bool = False, + proxies: typing.Optional[ProxiesTypes] = None, + mounts: typing.Optional[typing.Mapping[str, AsyncBaseTransport]] = None, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + follow_redirects: bool = False, + limits: Limits = DEFAULT_LIMITS, + max_redirects: int = DEFAULT_MAX_REDIRECTS, + event_hooks: typing.Optional[ + typing.Mapping[str, typing.List[typing.Callable[..., typing.Any]]] + ] = None, + base_url: URLTypes = "", + transport: typing.Optional[AsyncBaseTransport] = None, + app: typing.Optional[typing.Callable[..., typing.Any]] = None, + trust_env: bool = True, + default_encoding: typing.Union[str, typing.Callable[[bytes], str]] = "utf-8", + ): + super().__init__( + auth=auth, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + follow_redirects=follow_redirects, + max_redirects=max_redirects, + event_hooks=event_hooks, + base_url=base_url, + trust_env=trust_env, + default_encoding=default_encoding, + ) + + if http2: + try: + import h2 # noqa + except ImportError: # pragma: no cover + raise ImportError( + "Using http2=True, but the 'h2' package is not installed. " + "Make sure to install httpx using `pip install httpx[http2]`." + ) from None + + allow_env_proxies = trust_env and app is None and transport is None + proxy_map = self._get_proxy_map(proxies, allow_env_proxies) + + self._transport = self._init_transport( + verify=verify, + cert=cert, + http1=http1, + http2=http2, + limits=limits, + transport=transport, + app=app, + trust_env=trust_env, + ) + + self._mounts: typing.Dict[URLPattern, typing.Optional[AsyncBaseTransport]] = { + URLPattern(key): None + if proxy is None + else self._init_proxy_transport( + proxy, + verify=verify, + cert=cert, + http1=http1, + http2=http2, + limits=limits, + trust_env=trust_env, + ) + for key, proxy in proxy_map.items() + } + if mounts is not None: + self._mounts.update( + {URLPattern(key): transport for key, transport in mounts.items()} + ) + self._mounts = dict(sorted(self._mounts.items())) + + def _init_transport( + self, + verify: VerifyTypes = True, + cert: typing.Optional[CertTypes] = None, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + transport: typing.Optional[AsyncBaseTransport] = None, + app: typing.Optional[typing.Callable[..., typing.Any]] = None, + trust_env: bool = True, + ) -> AsyncBaseTransport: + if transport is not None: + return transport + + if app is not None: + return ASGITransport(app=app) + + return AsyncHTTPTransport( + verify=verify, + cert=cert, + http1=http1, + http2=http2, + limits=limits, + trust_env=trust_env, + ) + + def _init_proxy_transport( + self, + proxy: Proxy, + verify: VerifyTypes = True, + cert: typing.Optional[CertTypes] = None, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + trust_env: bool = True, + ) -> AsyncBaseTransport: + return AsyncHTTPTransport( + verify=verify, + cert=cert, + http2=http2, + limits=limits, + trust_env=trust_env, + proxy=proxy, + ) + + def _transport_for_url(self, url: URL) -> AsyncBaseTransport: + """ + Returns the transport instance that should be used for a given URL. + This will either be the standard connection pool, or a proxy. + """ + for pattern, transport in self._mounts.items(): + if pattern.matches(url): + return self._transport if transport is None else transport + + return self._transport + + async def request( + self, + method: str, + url: URLTypes, + *, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, + json: typing.Optional[typing.Any] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault, None] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> Response: + """ + Build and send a request. + + Equivalent to: + + ```python + request = client.build_request(...) + response = await client.send(request, ...) + ``` + + See `AsyncClient.build_request()`, `AsyncClient.send()` + and [Merging of configuration][0] for how the various parameters + are merged with client-level configuration. + + [0]: /advanced/#merging-of-configuration + """ + request = self.build_request( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + extensions=extensions, + ) + return await self.send(request, auth=auth, follow_redirects=follow_redirects) + + @asynccontextmanager + async def stream( + self, + method: str, + url: URLTypes, + *, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, + json: typing.Optional[typing.Any] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> typing.AsyncIterator[Response]: + """ + Alternative to `httpx.request()` that streams the response body + instead of loading it into memory at once. + + **Parameters**: See `httpx.request`. + + See also: [Streaming Responses][0] + + [0]: /quickstart#streaming-responses + """ + request = self.build_request( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + extensions=extensions, + ) + response = await self.send( + request=request, + auth=auth, + follow_redirects=follow_redirects, + stream=True, + ) + try: + yield response + finally: + await response.aclose() + + async def send( + self, + request: Request, + *, + stream: bool = False, + auth: typing.Union[AuthTypes, UseClientDefault, None] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + ) -> Response: + """ + Send a request. + + The request is sent as-is, unmodified. + + Typically you'll want to build one with `AsyncClient.build_request()` + so that any client-level configuration is merged into the request, + but passing an explicit `httpx.Request()` is supported as well. + + See also: [Request instances][0] + + [0]: /advanced/#request-instances + """ + if self._state == ClientState.CLOSED: + raise RuntimeError("Cannot send a request, as the client has been closed.") + + self._state = ClientState.OPENED + follow_redirects = ( + self.follow_redirects + if isinstance(follow_redirects, UseClientDefault) + else follow_redirects + ) + + auth = self._build_request_auth(request, auth) + + response = await self._send_handling_auth( + request, + auth=auth, + follow_redirects=follow_redirects, + history=[], + ) + try: + if not stream: + await response.aread() + + return response + + except BaseException as exc: # pragma: no cover + await response.aclose() + raise exc + + async def _send_handling_auth( + self, + request: Request, + auth: Auth, + follow_redirects: bool, + history: typing.List[Response], + ) -> Response: + auth_flow = auth.async_auth_flow(request) + try: + request = await auth_flow.__anext__() + + while True: + response = await self._send_handling_redirects( + request, + follow_redirects=follow_redirects, + history=history, + ) + try: + try: + next_request = await auth_flow.asend(response) + except StopAsyncIteration: + return response + + response.history = list(history) + await response.aread() + request = next_request + history.append(response) + + except BaseException as exc: + await response.aclose() + raise exc + finally: + await auth_flow.aclose() + + async def _send_handling_redirects( + self, + request: Request, + follow_redirects: bool, + history: typing.List[Response], + ) -> Response: + while True: + if len(history) > self.max_redirects: + raise TooManyRedirects( + "Exceeded maximum allowed redirects.", request=request + ) + + for hook in self._event_hooks["request"]: + await hook(request) + + response = await self._send_single_request(request) + try: + for hook in self._event_hooks["response"]: + await hook(response) + + response.history = list(history) + + if not response.has_redirect_location: + return response + + request = self._build_redirect_request(request, response) + history = history + [response] + + if follow_redirects: + await response.aread() + else: + response.next_request = request + return response + + except BaseException as exc: + await response.aclose() + raise exc + + async def _send_single_request(self, request: Request) -> Response: + """ + Sends a single request, without handling any redirections. + """ + transport = self._transport_for_url(request.url) + timer = Timer() + await timer.async_start() + + if not isinstance(request.stream, AsyncByteStream): + raise RuntimeError( + "Attempted to send an sync request with an AsyncClient instance." + ) + + with request_context(request=request): + response = await transport.handle_async_request(request) + + assert isinstance(response.stream, AsyncByteStream) + response.request = request + response.stream = BoundAsyncStream( + response.stream, response=response, timer=timer + ) + self.cookies.extract_cookies(response) + response.default_encoding = self._default_encoding + + logger.info( + 'HTTP Request: %s %s "%s %d %s"', + request.method, + request.url, + response.http_version, + response.status_code, + response.reason_phrase, + ) + + return response + + async def get( + self, + url: URLTypes, + *, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault, None] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> Response: + """ + Send a `GET` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "GET", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def options( + self, + url: URLTypes, + *, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> Response: + """ + Send an `OPTIONS` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "OPTIONS", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def head( + self, + url: URLTypes, + *, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> Response: + """ + Send a `HEAD` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "HEAD", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def post( + self, + url: URLTypes, + *, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, + json: typing.Optional[typing.Any] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> Response: + """ + Send a `POST` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "POST", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def put( + self, + url: URLTypes, + *, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, + json: typing.Optional[typing.Any] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> Response: + """ + Send a `PUT` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "PUT", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def patch( + self, + url: URLTypes, + *, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, + json: typing.Optional[typing.Any] = None, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> Response: + """ + Send a `PATCH` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "PATCH", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def delete( + self, + url: URLTypes, + *, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT, + timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT, + extensions: typing.Optional[RequestExtensions] = None, + ) -> Response: + """ + Send a `DELETE` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "DELETE", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def aclose(self) -> None: + """ + Close transport and proxies. + """ + if self._state != ClientState.CLOSED: + self._state = ClientState.CLOSED + + await self._transport.aclose() + for proxy in self._mounts.values(): + if proxy is not None: + await proxy.aclose() + + async def __aenter__(self: U) -> U: + if self._state != ClientState.UNOPENED: + msg = { + ClientState.OPENED: "Cannot open a client instance more than once.", + ClientState.CLOSED: "Cannot reopen a client instance, once it has been closed.", + }[self._state] + raise RuntimeError(msg) + + self._state = ClientState.OPENED + + await self._transport.__aenter__() + for proxy in self._mounts.values(): + if proxy is not None: + await proxy.__aenter__() + return self + + async def __aexit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]] = None, + exc_value: typing.Optional[BaseException] = None, + traceback: typing.Optional[TracebackType] = None, + ) -> None: + self._state = ClientState.CLOSED + + await self._transport.__aexit__(exc_type, exc_value, traceback) + for proxy in self._mounts.values(): + if proxy is not None: + await proxy.__aexit__(exc_type, exc_value, traceback) diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_compat.py b/Backend/venv/lib/python3.12/site-packages/httpx/_compat.py new file mode 100644 index 00000000..a9b9c630 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_compat.py @@ -0,0 +1,43 @@ +""" +The _compat module is used for code which requires branching between different +Python environments. It is excluded from the code coverage checks. +""" +import ssl +import sys + +# Brotli support is optional +# The C bindings in `brotli` are recommended for CPython. +# The CFFI bindings in `brotlicffi` are recommended for PyPy and everything else. +try: + import brotlicffi as brotli +except ImportError: # pragma: no cover + try: + import brotli + except ImportError: + brotli = None + +if sys.version_info >= (3, 10) or ( + sys.version_info >= (3, 7) and ssl.OPENSSL_VERSION_INFO >= (1, 1, 0, 7) +): + + def set_minimum_tls_version_1_2(context: ssl.SSLContext) -> None: + # The OP_NO_SSL* and OP_NO_TLS* become deprecated in favor of + # 'SSLContext.minimum_version' from Python 3.7 onwards, however + # this attribute is not available unless the ssl module is compiled + # with OpenSSL 1.1.0g or newer. + # https://docs.python.org/3.10/library/ssl.html#ssl.SSLContext.minimum_version + # https://docs.python.org/3.7/library/ssl.html#ssl.SSLContext.minimum_version + context.minimum_version = ssl.TLSVersion.TLSv1_2 + +else: + + def set_minimum_tls_version_1_2(context: ssl.SSLContext) -> None: + # If 'minimum_version' isn't available, we configure these options with + # the older deprecated variants. + context.options |= ssl.OP_NO_SSLv2 + context.options |= ssl.OP_NO_SSLv3 + context.options |= ssl.OP_NO_TLSv1 + context.options |= ssl.OP_NO_TLSv1_1 + + +__all__ = ["brotli", "set_minimum_tls_version_1_2"] diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_config.py b/Backend/venv/lib/python3.12/site-packages/httpx/_config.py new file mode 100644 index 00000000..f46a5bfe --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_config.py @@ -0,0 +1,369 @@ +import logging +import os +import ssl +import sys +import typing +from pathlib import Path + +import certifi + +from ._compat import set_minimum_tls_version_1_2 +from ._models import Headers +from ._types import CertTypes, HeaderTypes, TimeoutTypes, URLTypes, VerifyTypes +from ._urls import URL +from ._utils import get_ca_bundle_from_env + +DEFAULT_CIPHERS = ":".join( + [ + "ECDHE+AESGCM", + "ECDHE+CHACHA20", + "DHE+AESGCM", + "DHE+CHACHA20", + "ECDH+AESGCM", + "DH+AESGCM", + "ECDH+AES", + "DH+AES", + "RSA+AESGCM", + "RSA+AES", + "!aNULL", + "!eNULL", + "!MD5", + "!DSS", + ] +) + + +logger = logging.getLogger("httpx") + + +class UnsetType: + pass # pragma: no cover + + +UNSET = UnsetType() + + +def create_ssl_context( + cert: typing.Optional[CertTypes] = None, + verify: VerifyTypes = True, + trust_env: bool = True, + http2: bool = False, +) -> ssl.SSLContext: + return SSLConfig( + cert=cert, verify=verify, trust_env=trust_env, http2=http2 + ).ssl_context + + +class SSLConfig: + """ + SSL Configuration. + """ + + DEFAULT_CA_BUNDLE_PATH = Path(certifi.where()) + + def __init__( + self, + *, + cert: typing.Optional[CertTypes] = None, + verify: VerifyTypes = True, + trust_env: bool = True, + http2: bool = False, + ): + self.cert = cert + self.verify = verify + self.trust_env = trust_env + self.http2 = http2 + self.ssl_context = self.load_ssl_context() + + def load_ssl_context(self) -> ssl.SSLContext: + logger.debug( + "load_ssl_context verify=%r cert=%r trust_env=%r http2=%r", + self.verify, + self.cert, + self.trust_env, + self.http2, + ) + + if self.verify: + return self.load_ssl_context_verify() + return self.load_ssl_context_no_verify() + + def load_ssl_context_no_verify(self) -> ssl.SSLContext: + """ + Return an SSL context for unverified connections. + """ + context = self._create_default_ssl_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + self._load_client_certs(context) + return context + + def load_ssl_context_verify(self) -> ssl.SSLContext: + """ + Return an SSL context for verified connections. + """ + if self.trust_env and self.verify is True: + ca_bundle = get_ca_bundle_from_env() + if ca_bundle is not None: + self.verify = ca_bundle + + if isinstance(self.verify, ssl.SSLContext): + # Allow passing in our own SSLContext object that's pre-configured. + context = self.verify + self._load_client_certs(context) + return context + elif isinstance(self.verify, bool): + ca_bundle_path = self.DEFAULT_CA_BUNDLE_PATH + elif Path(self.verify).exists(): + ca_bundle_path = Path(self.verify) + else: + raise IOError( + "Could not find a suitable TLS CA certificate bundle, " + "invalid path: {}".format(self.verify) + ) + + context = self._create_default_ssl_context() + context.verify_mode = ssl.CERT_REQUIRED + context.check_hostname = True + + # Signal to server support for PHA in TLS 1.3. Raises an + # AttributeError if only read-only access is implemented. + if sys.version_info >= (3, 8): # pragma: no cover + try: + context.post_handshake_auth = True + except AttributeError: # pragma: no cover + pass + + # Disable using 'commonName' for SSLContext.check_hostname + # when the 'subjectAltName' extension isn't available. + try: + context.hostname_checks_common_name = False + except AttributeError: # pragma: no cover + pass + + if ca_bundle_path.is_file(): + cafile = str(ca_bundle_path) + logger.debug("load_verify_locations cafile=%r", cafile) + context.load_verify_locations(cafile=cafile) + elif ca_bundle_path.is_dir(): + capath = str(ca_bundle_path) + logger.debug("load_verify_locations capath=%r", capath) + context.load_verify_locations(capath=capath) + + self._load_client_certs(context) + + return context + + def _create_default_ssl_context(self) -> ssl.SSLContext: + """ + Creates the default SSLContext object that's used for both verified + and unverified connections. + """ + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + set_minimum_tls_version_1_2(context) + context.options |= ssl.OP_NO_COMPRESSION + context.set_ciphers(DEFAULT_CIPHERS) + + if ssl.HAS_ALPN: + alpn_idents = ["http/1.1", "h2"] if self.http2 else ["http/1.1"] + context.set_alpn_protocols(alpn_idents) + + if sys.version_info >= (3, 8): # pragma: no cover + keylogfile = os.environ.get("SSLKEYLOGFILE") + if keylogfile and self.trust_env: + context.keylog_filename = keylogfile + + return context + + def _load_client_certs(self, ssl_context: ssl.SSLContext) -> None: + """ + Loads client certificates into our SSLContext object + """ + if self.cert is not None: + if isinstance(self.cert, str): + ssl_context.load_cert_chain(certfile=self.cert) + elif isinstance(self.cert, tuple) and len(self.cert) == 2: + ssl_context.load_cert_chain(certfile=self.cert[0], keyfile=self.cert[1]) + elif isinstance(self.cert, tuple) and len(self.cert) == 3: + ssl_context.load_cert_chain( + certfile=self.cert[0], + keyfile=self.cert[1], + password=self.cert[2], # type: ignore + ) + + +class Timeout: + """ + Timeout configuration. + + **Usage**: + + Timeout(None) # No timeouts. + Timeout(5.0) # 5s timeout on all operations. + Timeout(None, connect=5.0) # 5s timeout on connect, no other timeouts. + Timeout(5.0, connect=10.0) # 10s timeout on connect. 5s timeout elsewhere. + Timeout(5.0, pool=None) # No timeout on acquiring connection from pool. + # 5s timeout elsewhere. + """ + + def __init__( + self, + timeout: typing.Union[TimeoutTypes, UnsetType] = UNSET, + *, + connect: typing.Union[None, float, UnsetType] = UNSET, + read: typing.Union[None, float, UnsetType] = UNSET, + write: typing.Union[None, float, UnsetType] = UNSET, + pool: typing.Union[None, float, UnsetType] = UNSET, + ): + if isinstance(timeout, Timeout): + # Passed as a single explicit Timeout. + assert connect is UNSET + assert read is UNSET + assert write is UNSET + assert pool is UNSET + self.connect = timeout.connect # type: typing.Optional[float] + self.read = timeout.read # type: typing.Optional[float] + self.write = timeout.write # type: typing.Optional[float] + self.pool = timeout.pool # type: typing.Optional[float] + elif isinstance(timeout, tuple): + # Passed as a tuple. + self.connect = timeout[0] + self.read = timeout[1] + self.write = None if len(timeout) < 3 else timeout[2] + self.pool = None if len(timeout) < 4 else timeout[3] + elif not ( + isinstance(connect, UnsetType) + or isinstance(read, UnsetType) + or isinstance(write, UnsetType) + or isinstance(pool, UnsetType) + ): + self.connect = connect + self.read = read + self.write = write + self.pool = pool + else: + if isinstance(timeout, UnsetType): + raise ValueError( + "httpx.Timeout must either include a default, or set all " + "four parameters explicitly." + ) + self.connect = timeout if isinstance(connect, UnsetType) else connect + self.read = timeout if isinstance(read, UnsetType) else read + self.write = timeout if isinstance(write, UnsetType) else write + self.pool = timeout if isinstance(pool, UnsetType) else pool + + def as_dict(self) -> typing.Dict[str, typing.Optional[float]]: + return { + "connect": self.connect, + "read": self.read, + "write": self.write, + "pool": self.pool, + } + + def __eq__(self, other: typing.Any) -> bool: + return ( + isinstance(other, self.__class__) + and self.connect == other.connect + and self.read == other.read + and self.write == other.write + and self.pool == other.pool + ) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + if len({self.connect, self.read, self.write, self.pool}) == 1: + return f"{class_name}(timeout={self.connect})" + return ( + f"{class_name}(connect={self.connect}, " + f"read={self.read}, write={self.write}, pool={self.pool})" + ) + + +class Limits: + """ + Configuration for limits to various client behaviors. + + **Parameters:** + + * **max_connections** - The maximum number of concurrent connections that may be + established. + * **max_keepalive_connections** - Allow the connection pool to maintain + keep-alive connections below this point. Should be less than or equal + to `max_connections`. + * **keepalive_expiry** - Time limit on idle keep-alive connections in seconds. + """ + + def __init__( + self, + *, + max_connections: typing.Optional[int] = None, + max_keepalive_connections: typing.Optional[int] = None, + keepalive_expiry: typing.Optional[float] = 5.0, + ): + self.max_connections = max_connections + self.max_keepalive_connections = max_keepalive_connections + self.keepalive_expiry = keepalive_expiry + + def __eq__(self, other: typing.Any) -> bool: + return ( + isinstance(other, self.__class__) + and self.max_connections == other.max_connections + and self.max_keepalive_connections == other.max_keepalive_connections + and self.keepalive_expiry == other.keepalive_expiry + ) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + return ( + f"{class_name}(max_connections={self.max_connections}, " + f"max_keepalive_connections={self.max_keepalive_connections}, " + f"keepalive_expiry={self.keepalive_expiry})" + ) + + +class Proxy: + def __init__( + self, + url: URLTypes, + *, + auth: typing.Optional[typing.Tuple[str, str]] = None, + headers: typing.Optional[HeaderTypes] = None, + ): + url = URL(url) + headers = Headers(headers) + + if url.scheme not in ("http", "https", "socks5"): + raise ValueError(f"Unknown scheme for proxy URL {url!r}") + + if url.username or url.password: + # Remove any auth credentials from the URL. + auth = (url.username, url.password) + url = url.copy_with(username=None, password=None) + + self.url = url + self.auth = auth + self.headers = headers + + @property + def raw_auth(self) -> typing.Optional[typing.Tuple[bytes, bytes]]: + # The proxy authentication as raw bytes. + return ( + None + if self.auth is None + else (self.auth[0].encode("utf-8"), self.auth[1].encode("utf-8")) + ) + + def __repr__(self) -> str: + # The authentication is represented with the password component masked. + auth = (self.auth[0], "********") if self.auth else None + + # Build a nice concise representation. + url_str = f"{str(self.url)!r}" + auth_str = f", auth={auth!r}" if auth else "" + headers_str = f", headers={dict(self.headers)!r}" if self.headers else "" + return f"Proxy({url_str}{auth_str}{headers_str})" + + +DEFAULT_TIMEOUT_CONFIG = Timeout(timeout=5.0) +DEFAULT_LIMITS = Limits(max_connections=100, max_keepalive_connections=20) +DEFAULT_MAX_REDIRECTS = 20 diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_content.py b/Backend/venv/lib/python3.12/site-packages/httpx/_content.py new file mode 100644 index 00000000..b16e12d9 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_content.py @@ -0,0 +1,238 @@ +import inspect +import warnings +from json import dumps as json_dumps +from typing import ( + Any, + AsyncIterable, + AsyncIterator, + Dict, + Iterable, + Iterator, + Mapping, + Optional, + Tuple, + Union, +) +from urllib.parse import urlencode + +from ._exceptions import StreamClosed, StreamConsumed +from ._multipart import MultipartStream +from ._types import ( + AsyncByteStream, + RequestContent, + RequestData, + RequestFiles, + ResponseContent, + SyncByteStream, +) +from ._utils import peek_filelike_length, primitive_value_to_str + + +class ByteStream(AsyncByteStream, SyncByteStream): + def __init__(self, stream: bytes) -> None: + self._stream = stream + + def __iter__(self) -> Iterator[bytes]: + yield self._stream + + async def __aiter__(self) -> AsyncIterator[bytes]: + yield self._stream + + +class IteratorByteStream(SyncByteStream): + CHUNK_SIZE = 65_536 + + def __init__(self, stream: Iterable[bytes]): + self._stream = stream + self._is_stream_consumed = False + self._is_generator = inspect.isgenerator(stream) + + def __iter__(self) -> Iterator[bytes]: + if self._is_stream_consumed and self._is_generator: + raise StreamConsumed() + + self._is_stream_consumed = True + if hasattr(self._stream, "read"): + # File-like interfaces should use 'read' directly. + chunk = self._stream.read(self.CHUNK_SIZE) + while chunk: + yield chunk + chunk = self._stream.read(self.CHUNK_SIZE) + else: + # Otherwise iterate. + for part in self._stream: + yield part + + +class AsyncIteratorByteStream(AsyncByteStream): + CHUNK_SIZE = 65_536 + + def __init__(self, stream: AsyncIterable[bytes]): + self._stream = stream + self._is_stream_consumed = False + self._is_generator = inspect.isasyncgen(stream) + + async def __aiter__(self) -> AsyncIterator[bytes]: + if self._is_stream_consumed and self._is_generator: + raise StreamConsumed() + + self._is_stream_consumed = True + if hasattr(self._stream, "aread"): + # File-like interfaces should use 'aread' directly. + chunk = await self._stream.aread(self.CHUNK_SIZE) + while chunk: + yield chunk + chunk = await self._stream.aread(self.CHUNK_SIZE) + else: + # Otherwise iterate. + async for part in self._stream: + yield part + + +class UnattachedStream(AsyncByteStream, SyncByteStream): + """ + If a request or response is serialized using pickle, then it is no longer + attached to a stream for I/O purposes. Any stream operations should result + in `httpx.StreamClosed`. + """ + + def __iter__(self) -> Iterator[bytes]: + raise StreamClosed() + + async def __aiter__(self) -> AsyncIterator[bytes]: + raise StreamClosed() + yield b"" # pragma: no cover + + +def encode_content( + content: Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]] +) -> Tuple[Dict[str, str], Union[SyncByteStream, AsyncByteStream]]: + if isinstance(content, (bytes, str)): + body = content.encode("utf-8") if isinstance(content, str) else content + content_length = len(body) + headers = {"Content-Length": str(content_length)} if body else {} + return headers, ByteStream(body) + + elif isinstance(content, Iterable) and not isinstance(content, dict): + # `not isinstance(content, dict)` is a bit oddly specific, but it + # catches a case that's easy for users to make in error, and would + # otherwise pass through here, like any other bytes-iterable, + # because `dict` happens to be iterable. See issue #2491. + content_length_or_none = peek_filelike_length(content) + + if content_length_or_none is None: + headers = {"Transfer-Encoding": "chunked"} + else: + headers = {"Content-Length": str(content_length_or_none)} + return headers, IteratorByteStream(content) # type: ignore + + elif isinstance(content, AsyncIterable): + headers = {"Transfer-Encoding": "chunked"} + return headers, AsyncIteratorByteStream(content) + + raise TypeError(f"Unexpected type for 'content', {type(content)!r}") + + +def encode_urlencoded_data( + data: RequestData, +) -> Tuple[Dict[str, str], ByteStream]: + plain_data = [] + for key, value in data.items(): + if isinstance(value, (list, tuple)): + plain_data.extend([(key, primitive_value_to_str(item)) for item in value]) + else: + plain_data.append((key, primitive_value_to_str(value))) + body = urlencode(plain_data, doseq=True).encode("utf-8") + content_length = str(len(body)) + content_type = "application/x-www-form-urlencoded" + headers = {"Content-Length": content_length, "Content-Type": content_type} + return headers, ByteStream(body) + + +def encode_multipart_data( + data: RequestData, files: RequestFiles, boundary: Optional[bytes] +) -> Tuple[Dict[str, str], MultipartStream]: + multipart = MultipartStream(data=data, files=files, boundary=boundary) + headers = multipart.get_headers() + return headers, multipart + + +def encode_text(text: str) -> Tuple[Dict[str, str], ByteStream]: + body = text.encode("utf-8") + content_length = str(len(body)) + content_type = "text/plain; charset=utf-8" + headers = {"Content-Length": content_length, "Content-Type": content_type} + return headers, ByteStream(body) + + +def encode_html(html: str) -> Tuple[Dict[str, str], ByteStream]: + body = html.encode("utf-8") + content_length = str(len(body)) + content_type = "text/html; charset=utf-8" + headers = {"Content-Length": content_length, "Content-Type": content_type} + return headers, ByteStream(body) + + +def encode_json(json: Any) -> Tuple[Dict[str, str], ByteStream]: + body = json_dumps(json).encode("utf-8") + content_length = str(len(body)) + content_type = "application/json" + headers = {"Content-Length": content_length, "Content-Type": content_type} + return headers, ByteStream(body) + + +def encode_request( + content: Optional[RequestContent] = None, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + json: Optional[Any] = None, + boundary: Optional[bytes] = None, +) -> Tuple[Dict[str, str], Union[SyncByteStream, AsyncByteStream]]: + """ + Handles encoding the given `content`, `data`, `files`, and `json`, + returning a two-tuple of (, ). + """ + if data is not None and not isinstance(data, Mapping): + # We prefer to separate `content=` + # for raw request content, and `data=

` for url encoded or + # multipart form content. + # + # However for compat with requests, we *do* still support + # `data=` usages. We deal with that case here, treating it + # as if `content=<...>` had been supplied instead. + message = "Use 'content=<...>' to upload raw bytes/text content." + warnings.warn(message, DeprecationWarning) + return encode_content(data) + + if content is not None: + return encode_content(content) + elif files: + return encode_multipart_data(data or {}, files, boundary) + elif data: + return encode_urlencoded_data(data) + elif json is not None: + return encode_json(json) + + return {}, ByteStream(b"") + + +def encode_response( + content: Optional[ResponseContent] = None, + text: Optional[str] = None, + html: Optional[str] = None, + json: Optional[Any] = None, +) -> Tuple[Dict[str, str], Union[SyncByteStream, AsyncByteStream]]: + """ + Handles encoding the given `content`, returning a two-tuple of + (, ). + """ + if content is not None: + return encode_content(content) + elif text is not None: + return encode_text(text) + elif html is not None: + return encode_html(html) + elif json is not None: + return encode_json(json) + + return {}, ByteStream(b"") diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_decoders.py b/Backend/venv/lib/python3.12/site-packages/httpx/_decoders.py new file mode 100644 index 00000000..500ce7ff --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_decoders.py @@ -0,0 +1,324 @@ +""" +Handlers for Content-Encoding. + +See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding +""" +import codecs +import io +import typing +import zlib + +from ._compat import brotli +from ._exceptions import DecodingError + + +class ContentDecoder: + def decode(self, data: bytes) -> bytes: + raise NotImplementedError() # pragma: no cover + + def flush(self) -> bytes: + raise NotImplementedError() # pragma: no cover + + +class IdentityDecoder(ContentDecoder): + """ + Handle unencoded data. + """ + + def decode(self, data: bytes) -> bytes: + return data + + def flush(self) -> bytes: + return b"" + + +class DeflateDecoder(ContentDecoder): + """ + Handle 'deflate' decoding. + + See: https://stackoverflow.com/questions/1838699 + """ + + def __init__(self) -> None: + self.first_attempt = True + self.decompressor = zlib.decompressobj() + + def decode(self, data: bytes) -> bytes: + was_first_attempt = self.first_attempt + self.first_attempt = False + try: + return self.decompressor.decompress(data) + except zlib.error as exc: + if was_first_attempt: + self.decompressor = zlib.decompressobj(-zlib.MAX_WBITS) + return self.decode(data) + raise DecodingError(str(exc)) from exc + + def flush(self) -> bytes: + try: + return self.decompressor.flush() + except zlib.error as exc: # pragma: no cover + raise DecodingError(str(exc)) from exc + + +class GZipDecoder(ContentDecoder): + """ + Handle 'gzip' decoding. + + See: https://stackoverflow.com/questions/1838699 + """ + + def __init__(self) -> None: + self.decompressor = zlib.decompressobj(zlib.MAX_WBITS | 16) + + def decode(self, data: bytes) -> bytes: + try: + return self.decompressor.decompress(data) + except zlib.error as exc: + raise DecodingError(str(exc)) from exc + + def flush(self) -> bytes: + try: + return self.decompressor.flush() + except zlib.error as exc: # pragma: no cover + raise DecodingError(str(exc)) from exc + + +class BrotliDecoder(ContentDecoder): + """ + Handle 'brotli' decoding. + + Requires `pip install brotlipy`. See: https://brotlipy.readthedocs.io/ + or `pip install brotli`. See https://github.com/google/brotli + Supports both 'brotlipy' and 'Brotli' packages since they share an import + name. The top branches are for 'brotlipy' and bottom branches for 'Brotli' + """ + + def __init__(self) -> None: + if brotli is None: # pragma: no cover + raise ImportError( + "Using 'BrotliDecoder', but neither of the 'brotlicffi' or 'brotli' " + "packages have been installed. " + "Make sure to install httpx using `pip install httpx[brotli]`." + ) from None + + self.decompressor = brotli.Decompressor() + self.seen_data = False + self._decompress: typing.Callable[[bytes], bytes] + if hasattr(self.decompressor, "decompress"): + # The 'brotlicffi' package. + self._decompress = self.decompressor.decompress # pragma: no cover + else: + # The 'brotli' package. + self._decompress = self.decompressor.process # pragma: no cover + + def decode(self, data: bytes) -> bytes: + if not data: + return b"" + self.seen_data = True + try: + return self._decompress(data) + except brotli.error as exc: + raise DecodingError(str(exc)) from exc + + def flush(self) -> bytes: + if not self.seen_data: + return b"" + try: + if hasattr(self.decompressor, "finish"): + # Only available in the 'brotlicffi' package. + + # As the decompressor decompresses eagerly, this + # will never actually emit any data. However, it will potentially throw + # errors if a truncated or damaged data stream has been used. + self.decompressor.finish() # pragma: no cover + return b"" + except brotli.error as exc: # pragma: no cover + raise DecodingError(str(exc)) from exc + + +class MultiDecoder(ContentDecoder): + """ + Handle the case where multiple encodings have been applied. + """ + + def __init__(self, children: typing.Sequence[ContentDecoder]) -> None: + """ + 'children' should be a sequence of decoders in the order in which + each was applied. + """ + # Note that we reverse the order for decoding. + self.children = list(reversed(children)) + + def decode(self, data: bytes) -> bytes: + for child in self.children: + data = child.decode(data) + return data + + def flush(self) -> bytes: + data = b"" + for child in self.children: + data = child.decode(data) + child.flush() + return data + + +class ByteChunker: + """ + Handles returning byte content in fixed-size chunks. + """ + + def __init__(self, chunk_size: typing.Optional[int] = None) -> None: + self._buffer = io.BytesIO() + self._chunk_size = chunk_size + + def decode(self, content: bytes) -> typing.List[bytes]: + if self._chunk_size is None: + return [content] if content else [] + + self._buffer.write(content) + if self._buffer.tell() >= self._chunk_size: + value = self._buffer.getvalue() + chunks = [ + value[i : i + self._chunk_size] + for i in range(0, len(value), self._chunk_size) + ] + if len(chunks[-1]) == self._chunk_size: + self._buffer.seek(0) + self._buffer.truncate() + return chunks + else: + self._buffer.seek(0) + self._buffer.write(chunks[-1]) + self._buffer.truncate() + return chunks[:-1] + else: + return [] + + def flush(self) -> typing.List[bytes]: + value = self._buffer.getvalue() + self._buffer.seek(0) + self._buffer.truncate() + return [value] if value else [] + + +class TextChunker: + """ + Handles returning text content in fixed-size chunks. + """ + + def __init__(self, chunk_size: typing.Optional[int] = None) -> None: + self._buffer = io.StringIO() + self._chunk_size = chunk_size + + def decode(self, content: str) -> typing.List[str]: + if self._chunk_size is None: + return [content] + + self._buffer.write(content) + if self._buffer.tell() >= self._chunk_size: + value = self._buffer.getvalue() + chunks = [ + value[i : i + self._chunk_size] + for i in range(0, len(value), self._chunk_size) + ] + if len(chunks[-1]) == self._chunk_size: + self._buffer.seek(0) + self._buffer.truncate() + return chunks + else: + self._buffer.seek(0) + self._buffer.write(chunks[-1]) + self._buffer.truncate() + return chunks[:-1] + else: + return [] + + def flush(self) -> typing.List[str]: + value = self._buffer.getvalue() + self._buffer.seek(0) + self._buffer.truncate() + return [value] if value else [] + + +class TextDecoder: + """ + Handles incrementally decoding bytes into text + """ + + def __init__(self, encoding: str = "utf-8"): + self.decoder = codecs.getincrementaldecoder(encoding)(errors="replace") + + def decode(self, data: bytes) -> str: + return self.decoder.decode(data) + + def flush(self) -> str: + return self.decoder.decode(b"", True) + + +class LineDecoder: + """ + Handles incrementally reading lines from text. + + Has the same behaviour as the stdllib splitlines, but handling the input iteratively. + """ + + def __init__(self) -> None: + self.buffer: typing.List[str] = [] + self.trailing_cr: bool = False + + def decode(self, text: str) -> typing.List[str]: + # See https://docs.python.org/3/library/stdtypes.html#str.splitlines + NEWLINE_CHARS = "\n\r\x0b\x0c\x1c\x1d\x1e\x85\u2028\u2029" + + # We always push a trailing `\r` into the next decode iteration. + if self.trailing_cr: + text = "\r" + text + self.trailing_cr = False + if text.endswith("\r"): + self.trailing_cr = True + text = text[:-1] + + if not text: + return [] + + trailing_newline = text[-1] in NEWLINE_CHARS + lines = text.splitlines() + + if len(lines) == 1 and not trailing_newline: + # No new lines, buffer the input and continue. + self.buffer.append(lines[0]) + return [] + + if self.buffer: + # Include any existing buffer in the first portion of the + # splitlines result. + lines = ["".join(self.buffer) + lines[0]] + lines[1:] + self.buffer = [] + + if not trailing_newline: + # If the last segment of splitlines is not newline terminated, + # then drop it from our output and start a new buffer. + self.buffer = [lines.pop()] + + return lines + + def flush(self) -> typing.List[str]: + if not self.buffer and not self.trailing_cr: + return [] + + lines = ["".join(self.buffer)] + self.buffer = [] + self.trailing_cr = False + return lines + + +SUPPORTED_DECODERS = { + "identity": IdentityDecoder, + "gzip": GZipDecoder, + "deflate": DeflateDecoder, + "br": BrotliDecoder, +} + + +if brotli is None: + SUPPORTED_DECODERS.pop("br") # pragma: no cover diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_exceptions.py b/Backend/venv/lib/python3.12/site-packages/httpx/_exceptions.py new file mode 100644 index 00000000..24a4f8ab --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_exceptions.py @@ -0,0 +1,343 @@ +""" +Our exception hierarchy: + +* HTTPError + x RequestError + + TransportError + - TimeoutException + · ConnectTimeout + · ReadTimeout + · WriteTimeout + · PoolTimeout + - NetworkError + · ConnectError + · ReadError + · WriteError + · CloseError + - ProtocolError + · LocalProtocolError + · RemoteProtocolError + - ProxyError + - UnsupportedProtocol + + DecodingError + + TooManyRedirects + x HTTPStatusError +* InvalidURL +* CookieConflict +* StreamError + x StreamConsumed + x StreamClosed + x ResponseNotRead + x RequestNotRead +""" +import contextlib +import typing + +if typing.TYPE_CHECKING: + from ._models import Request, Response # pragma: no cover + + +class HTTPError(Exception): + """ + Base class for `RequestError` and `HTTPStatusError`. + + Useful for `try...except` blocks when issuing a request, + and then calling `.raise_for_status()`. + + For example: + + ``` + try: + response = httpx.get("https://www.example.com") + response.raise_for_status() + except httpx.HTTPError as exc: + print(f"HTTP Exception for {exc.request.url} - {exc}") + ``` + """ + + def __init__(self, message: str) -> None: + super().__init__(message) + self._request: typing.Optional["Request"] = None + + @property + def request(self) -> "Request": + if self._request is None: + raise RuntimeError("The .request property has not been set.") + return self._request + + @request.setter + def request(self, request: "Request") -> None: + self._request = request + + +class RequestError(HTTPError): + """ + Base class for all exceptions that may occur when issuing a `.request()`. + """ + + def __init__( + self, message: str, *, request: typing.Optional["Request"] = None + ) -> None: + super().__init__(message) + # At the point an exception is raised we won't typically have a request + # instance to associate it with. + # + # The 'request_context' context manager is used within the Client and + # Response methods in order to ensure that any raised exceptions + # have a `.request` property set on them. + self._request = request + + +class TransportError(RequestError): + """ + Base class for all exceptions that occur at the level of the Transport API. + """ + + +# Timeout exceptions... + + +class TimeoutException(TransportError): + """ + The base class for timeout errors. + + An operation has timed out. + """ + + +class ConnectTimeout(TimeoutException): + """ + Timed out while connecting to the host. + """ + + +class ReadTimeout(TimeoutException): + """ + Timed out while receiving data from the host. + """ + + +class WriteTimeout(TimeoutException): + """ + Timed out while sending data to the host. + """ + + +class PoolTimeout(TimeoutException): + """ + Timed out waiting to acquire a connection from the pool. + """ + + +# Core networking exceptions... + + +class NetworkError(TransportError): + """ + The base class for network-related errors. + + An error occurred while interacting with the network. + """ + + +class ReadError(NetworkError): + """ + Failed to receive data from the network. + """ + + +class WriteError(NetworkError): + """ + Failed to send data through the network. + """ + + +class ConnectError(NetworkError): + """ + Failed to establish a connection. + """ + + +class CloseError(NetworkError): + """ + Failed to close a connection. + """ + + +# Other transport exceptions... + + +class ProxyError(TransportError): + """ + An error occurred while establishing a proxy connection. + """ + + +class UnsupportedProtocol(TransportError): + """ + Attempted to make a request to an unsupported protocol. + + For example issuing a request to `ftp://www.example.com`. + """ + + +class ProtocolError(TransportError): + """ + The protocol was violated. + """ + + +class LocalProtocolError(ProtocolError): + """ + A protocol was violated by the client. + + For example if the user instantiated a `Request` instance explicitly, + failed to include the mandatory `Host:` header, and then issued it directly + using `client.send()`. + """ + + +class RemoteProtocolError(ProtocolError): + """ + The protocol was violated by the server. + + For example, returning malformed HTTP. + """ + + +# Other request exceptions... + + +class DecodingError(RequestError): + """ + Decoding of the response failed, due to a malformed encoding. + """ + + +class TooManyRedirects(RequestError): + """ + Too many redirects. + """ + + +# Client errors + + +class HTTPStatusError(HTTPError): + """ + The response had an error HTTP status of 4xx or 5xx. + + May be raised when calling `response.raise_for_status()` + """ + + def __init__( + self, message: str, *, request: "Request", response: "Response" + ) -> None: + super().__init__(message) + self.request = request + self.response = response + + +class InvalidURL(Exception): + """ + URL is improperly formed or cannot be parsed. + """ + + def __init__(self, message: str) -> None: + super().__init__(message) + + +class CookieConflict(Exception): + """ + Attempted to lookup a cookie by name, but multiple cookies existed. + + Can occur when calling `response.cookies.get(...)`. + """ + + def __init__(self, message: str) -> None: + super().__init__(message) + + +# Stream exceptions... + +# These may occur as the result of a programming error, by accessing +# the request/response stream in an invalid manner. + + +class StreamError(RuntimeError): + """ + The base class for stream exceptions. + + The developer made an error in accessing the request stream in + an invalid way. + """ + + def __init__(self, message: str) -> None: + super().__init__(message) + + +class StreamConsumed(StreamError): + """ + Attempted to read or stream content, but the content has already + been streamed. + """ + + def __init__(self) -> None: + message = ( + "Attempted to read or stream some content, but the content has " + "already been streamed. For requests, this could be due to passing " + "a generator as request content, and then receiving a redirect " + "response or a secondary request as part of an authentication flow." + "For responses, this could be due to attempting to stream the response " + "content more than once." + ) + super().__init__(message) + + +class StreamClosed(StreamError): + """ + Attempted to read or stream response content, but the request has been + closed. + """ + + def __init__(self) -> None: + message = ( + "Attempted to read or stream content, but the stream has " "been closed." + ) + super().__init__(message) + + +class ResponseNotRead(StreamError): + """ + Attempted to access streaming response content, without having called `read()`. + """ + + def __init__(self) -> None: + message = "Attempted to access streaming response content, without having called `read()`." + super().__init__(message) + + +class RequestNotRead(StreamError): + """ + Attempted to access streaming request content, without having called `read()`. + """ + + def __init__(self) -> None: + message = "Attempted to access streaming request content, without having called `read()`." + super().__init__(message) + + +@contextlib.contextmanager +def request_context( + request: typing.Optional["Request"] = None, +) -> typing.Iterator[None]: + """ + A context manager that can be used to attach the given request context + to any `RequestError` exceptions that are raised within the block. + """ + try: + yield + except RequestError as exc: + if request is not None: + exc.request = request + raise exc diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_main.py b/Backend/venv/lib/python3.12/site-packages/httpx/_main.py new file mode 100644 index 00000000..7c12ce84 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_main.py @@ -0,0 +1,506 @@ +import functools +import json +import sys +import typing + +import click +import httpcore +import pygments.lexers +import pygments.util +import rich.console +import rich.markup +import rich.progress +import rich.syntax +import rich.table + +from ._client import Client +from ._exceptions import RequestError +from ._models import Response +from ._status_codes import codes + + +def print_help() -> None: + console = rich.console.Console() + + console.print("[bold]HTTPX :butterfly:", justify="center") + console.print() + console.print("A next generation HTTP client.", justify="center") + console.print() + console.print( + "Usage: [bold]httpx[/bold] [cyan] [OPTIONS][/cyan] ", justify="left" + ) + console.print() + + table = rich.table.Table.grid(padding=1, pad_edge=True) + table.add_column("Parameter", no_wrap=True, justify="left", style="bold") + table.add_column("Description") + table.add_row( + "-m, --method [cyan]METHOD", + "Request method, such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD.\n" + "[Default: GET, or POST if a request body is included]", + ) + table.add_row( + "-p, --params [cyan] ...", + "Query parameters to include in the request URL.", + ) + table.add_row( + "-c, --content [cyan]TEXT", "Byte content to include in the request body." + ) + table.add_row( + "-d, --data [cyan] ...", "Form data to include in the request body." + ) + table.add_row( + "-f, --files [cyan] ...", + "Form files to include in the request body.", + ) + table.add_row("-j, --json [cyan]TEXT", "JSON data to include in the request body.") + table.add_row( + "-h, --headers [cyan] ...", + "Include additional HTTP headers in the request.", + ) + table.add_row( + "--cookies [cyan] ...", "Cookies to include in the request." + ) + table.add_row( + "--auth [cyan]", + "Username and password to include in the request. Specify '-' for the password to use " + "a password prompt. Note that using --verbose/-v will expose the Authorization " + "header, including the password encoding in a trivially reversible format.", + ) + + table.add_row( + "--proxies [cyan]URL", + "Send the request via a proxy. Should be the URL giving the proxy address.", + ) + + table.add_row( + "--timeout [cyan]FLOAT", + "Timeout value to use for network operations, such as establishing the connection, " + "reading some data, etc... [Default: 5.0]", + ) + + table.add_row("--follow-redirects", "Automatically follow redirects.") + table.add_row("--no-verify", "Disable SSL verification.") + table.add_row( + "--http2", "Send the request using HTTP/2, if the remote server supports it." + ) + + table.add_row( + "--download [cyan]FILE", + "Save the response content as a file, rather than displaying it.", + ) + + table.add_row("-v, --verbose", "Verbose output. Show request as well as response.") + table.add_row("--help", "Show this message and exit.") + console.print(table) + + +def get_lexer_for_response(response: Response) -> str: + content_type = response.headers.get("Content-Type") + if content_type is not None: + mime_type, _, _ = content_type.partition(";") + try: + return typing.cast( + str, pygments.lexers.get_lexer_for_mimetype(mime_type.strip()).name + ) + except pygments.util.ClassNotFound: # pragma: no cover + pass + return "" # pragma: no cover + + +def format_request_headers(request: httpcore.Request, http2: bool = False) -> str: + version = "HTTP/2" if http2 else "HTTP/1.1" + headers = [ + (name.lower() if http2 else name, value) for name, value in request.headers + ] + method = request.method.decode("ascii") + target = request.url.target.decode("ascii") + lines = [f"{method} {target} {version}"] + [ + f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers + ] + return "\n".join(lines) + + +def format_response_headers( + http_version: bytes, + status: int, + reason_phrase: typing.Optional[bytes], + headers: typing.List[typing.Tuple[bytes, bytes]], +) -> str: + version = http_version.decode("ascii") + reason = ( + codes.get_reason_phrase(status) + if reason_phrase is None + else reason_phrase.decode("ascii") + ) + lines = [f"{version} {status} {reason}"] + [ + f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers + ] + return "\n".join(lines) + + +def print_request_headers(request: httpcore.Request, http2: bool = False) -> None: + console = rich.console.Console() + http_text = format_request_headers(request, http2=http2) + syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True) + console.print(syntax) + syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True) + console.print(syntax) + + +def print_response_headers( + http_version: bytes, + status: int, + reason_phrase: typing.Optional[bytes], + headers: typing.List[typing.Tuple[bytes, bytes]], +) -> None: + console = rich.console.Console() + http_text = format_response_headers(http_version, status, reason_phrase, headers) + syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True) + console.print(syntax) + syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True) + console.print(syntax) + + +def print_response(response: Response) -> None: + console = rich.console.Console() + lexer_name = get_lexer_for_response(response) + if lexer_name: + if lexer_name.lower() == "json": + try: + data = response.json() + text = json.dumps(data, indent=4) + except ValueError: # pragma: no cover + text = response.text + else: + text = response.text + + syntax = rich.syntax.Syntax(text, lexer_name, theme="ansi_dark", word_wrap=True) + console.print(syntax) + else: + console.print(f"<{len(response.content)} bytes of binary data>") + + +_PCTRTT = typing.Tuple[typing.Tuple[str, str], ...] +_PCTRTTT = typing.Tuple[_PCTRTT, ...] +_PeerCertRetDictType = typing.Dict[str, typing.Union[str, _PCTRTTT, _PCTRTT]] + + +def format_certificate(cert: _PeerCertRetDictType) -> str: # pragma: no cover + lines = [] + for key, value in cert.items(): + if isinstance(value, (list, tuple)): + lines.append(f"* {key}:") + for item in value: + if key in ("subject", "issuer"): + for sub_item in item: + lines.append(f"* {sub_item[0]}: {sub_item[1]!r}") + elif isinstance(item, tuple) and len(item) == 2: + lines.append(f"* {item[0]}: {item[1]!r}") + else: + lines.append(f"* {item!r}") + else: + lines.append(f"* {key}: {value!r}") + return "\n".join(lines) + + +def trace( + name: str, info: typing.Mapping[str, typing.Any], verbose: bool = False +) -> None: + console = rich.console.Console() + if name == "connection.connect_tcp.started" and verbose: + host = info["host"] + console.print(f"* Connecting to {host!r}") + elif name == "connection.connect_tcp.complete" and verbose: + stream = info["return_value"] + server_addr = stream.get_extra_info("server_addr") + console.print(f"* Connected to {server_addr[0]!r} on port {server_addr[1]}") + elif name == "connection.start_tls.complete" and verbose: # pragma: no cover + stream = info["return_value"] + ssl_object = stream.get_extra_info("ssl_object") + version = ssl_object.version() + cipher = ssl_object.cipher() + server_cert = ssl_object.getpeercert() + alpn = ssl_object.selected_alpn_protocol() + console.print(f"* SSL established using {version!r} / {cipher[0]!r}") + console.print(f"* Selected ALPN protocol: {alpn!r}") + if server_cert: + console.print("* Server certificate:") + console.print(format_certificate(server_cert)) + elif name == "http11.send_request_headers.started" and verbose: + request = info["request"] + print_request_headers(request, http2=False) + elif name == "http2.send_request_headers.started" and verbose: # pragma: no cover + request = info["request"] + print_request_headers(request, http2=True) + elif name == "http11.receive_response_headers.complete": + http_version, status, reason_phrase, headers = info["return_value"] + print_response_headers(http_version, status, reason_phrase, headers) + elif name == "http2.receive_response_headers.complete": # pragma: no cover + status, headers = info["return_value"] + http_version = b"HTTP/2" + reason_phrase = None + print_response_headers(http_version, status, reason_phrase, headers) + + +def download_response(response: Response, download: typing.BinaryIO) -> None: + console = rich.console.Console() + console.print() + content_length = response.headers.get("Content-Length") + with rich.progress.Progress( + "[progress.description]{task.description}", + "[progress.percentage]{task.percentage:>3.0f}%", + rich.progress.BarColumn(bar_width=None), + rich.progress.DownloadColumn(), + rich.progress.TransferSpeedColumn(), + ) as progress: + description = f"Downloading [bold]{rich.markup.escape(download.name)}" + download_task = progress.add_task( + description, + total=int(content_length or 0), + start=content_length is not None, + ) + for chunk in response.iter_bytes(): + download.write(chunk) + progress.update(download_task, completed=response.num_bytes_downloaded) + + +def validate_json( + ctx: click.Context, + param: typing.Union[click.Option, click.Parameter], + value: typing.Any, +) -> typing.Any: + if value is None: + return None + + try: + return json.loads(value) + except json.JSONDecodeError: # pragma: no cover + raise click.BadParameter("Not valid JSON") + + +def validate_auth( + ctx: click.Context, + param: typing.Union[click.Option, click.Parameter], + value: typing.Any, +) -> typing.Any: + if value == (None, None): + return None + + username, password = value + if password == "-": # pragma: no cover + password = click.prompt("Password", hide_input=True) + return (username, password) + + +def handle_help( + ctx: click.Context, + param: typing.Union[click.Option, click.Parameter], + value: typing.Any, +) -> None: + if not value or ctx.resilient_parsing: + return + + print_help() + ctx.exit() + + +@click.command(add_help_option=False) +@click.argument("url", type=str) +@click.option( + "--method", + "-m", + "method", + type=str, + help=( + "Request method, such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD. " + "[Default: GET, or POST if a request body is included]" + ), +) +@click.option( + "--params", + "-p", + "params", + type=(str, str), + multiple=True, + help="Query parameters to include in the request URL.", +) +@click.option( + "--content", + "-c", + "content", + type=str, + help="Byte content to include in the request body.", +) +@click.option( + "--data", + "-d", + "data", + type=(str, str), + multiple=True, + help="Form data to include in the request body.", +) +@click.option( + "--files", + "-f", + "files", + type=(str, click.File(mode="rb")), + multiple=True, + help="Form files to include in the request body.", +) +@click.option( + "--json", + "-j", + "json", + type=str, + callback=validate_json, + help="JSON data to include in the request body.", +) +@click.option( + "--headers", + "-h", + "headers", + type=(str, str), + multiple=True, + help="Include additional HTTP headers in the request.", +) +@click.option( + "--cookies", + "cookies", + type=(str, str), + multiple=True, + help="Cookies to include in the request.", +) +@click.option( + "--auth", + "auth", + type=(str, str), + default=(None, None), + callback=validate_auth, + help=( + "Username and password to include in the request. " + "Specify '-' for the password to use a password prompt. " + "Note that using --verbose/-v will expose the Authorization header, " + "including the password encoding in a trivially reversible format." + ), +) +@click.option( + "--proxies", + "proxies", + type=str, + default=None, + help="Send the request via a proxy. Should be the URL giving the proxy address.", +) +@click.option( + "--timeout", + "timeout", + type=float, + default=5.0, + help=( + "Timeout value to use for network operations, such as establishing the " + "connection, reading some data, etc... [Default: 5.0]" + ), +) +@click.option( + "--follow-redirects", + "follow_redirects", + is_flag=True, + default=False, + help="Automatically follow redirects.", +) +@click.option( + "--no-verify", + "verify", + is_flag=True, + default=True, + help="Disable SSL verification.", +) +@click.option( + "--http2", + "http2", + type=bool, + is_flag=True, + default=False, + help="Send the request using HTTP/2, if the remote server supports it.", +) +@click.option( + "--download", + type=click.File("wb"), + help="Save the response content as a file, rather than displaying it.", +) +@click.option( + "--verbose", + "-v", + type=bool, + is_flag=True, + default=False, + help="Verbose. Show request as well as response.", +) +@click.option( + "--help", + is_flag=True, + is_eager=True, + expose_value=False, + callback=handle_help, + help="Show this message and exit.", +) +def main( + url: str, + method: str, + params: typing.List[typing.Tuple[str, str]], + content: str, + data: typing.List[typing.Tuple[str, str]], + files: typing.List[typing.Tuple[str, click.File]], + json: str, + headers: typing.List[typing.Tuple[str, str]], + cookies: typing.List[typing.Tuple[str, str]], + auth: typing.Optional[typing.Tuple[str, str]], + proxies: str, + timeout: float, + follow_redirects: bool, + verify: bool, + http2: bool, + download: typing.Optional[typing.BinaryIO], + verbose: bool, +) -> None: + """ + An HTTP command line client. + Sends a request and displays the response. + """ + if not method: + method = "POST" if content or data or files or json else "GET" + + try: + with Client( + proxies=proxies, + timeout=timeout, + verify=verify, + http2=http2, + ) as client: + with client.stream( + method, + url, + params=list(params), + content=content, + data=dict(data), + files=files, # type: ignore + json=json, + headers=headers, + cookies=dict(cookies), + auth=auth, + follow_redirects=follow_redirects, + extensions={"trace": functools.partial(trace, verbose=verbose)}, + ) as response: + if download is not None: + download_response(response, download) + else: + response.read() + if response.content: + print_response(response) + + except RequestError as exc: + console = rich.console.Console() + console.print(f"[red]{type(exc).__name__}[/red]: {exc}") + sys.exit(1) + + sys.exit(0 if response.is_success else 1) diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_models.py b/Backend/venv/lib/python3.12/site-packages/httpx/_models.py new file mode 100644 index 00000000..e0e5278c --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_models.py @@ -0,0 +1,1209 @@ +import datetime +import email.message +import json as jsonlib +import typing +import urllib.request +from collections.abc import Mapping +from http.cookiejar import Cookie, CookieJar + +from ._content import ByteStream, UnattachedStream, encode_request, encode_response +from ._decoders import ( + SUPPORTED_DECODERS, + ByteChunker, + ContentDecoder, + IdentityDecoder, + LineDecoder, + MultiDecoder, + TextChunker, + TextDecoder, +) +from ._exceptions import ( + CookieConflict, + HTTPStatusError, + RequestNotRead, + ResponseNotRead, + StreamClosed, + StreamConsumed, + request_context, +) +from ._multipart import get_multipart_boundary_from_content_type +from ._status_codes import codes +from ._types import ( + AsyncByteStream, + CookieTypes, + HeaderTypes, + QueryParamTypes, + RequestContent, + RequestData, + RequestExtensions, + RequestFiles, + ResponseContent, + ResponseExtensions, + SyncByteStream, +) +from ._urls import URL +from ._utils import ( + guess_json_utf, + is_known_encoding, + normalize_header_key, + normalize_header_value, + obfuscate_sensitive_headers, + parse_content_type_charset, + parse_header_links, +) + + +class Headers(typing.MutableMapping[str, str]): + """ + HTTP headers, as a case-insensitive multi-dict. + """ + + def __init__( + self, + headers: typing.Optional[HeaderTypes] = None, + encoding: typing.Optional[str] = None, + ) -> None: + if headers is None: + self._list = [] # type: typing.List[typing.Tuple[bytes, bytes, bytes]] + elif isinstance(headers, Headers): + self._list = list(headers._list) + elif isinstance(headers, Mapping): + self._list = [ + ( + normalize_header_key(k, lower=False, encoding=encoding), + normalize_header_key(k, lower=True, encoding=encoding), + normalize_header_value(v, encoding), + ) + for k, v in headers.items() + ] + else: + self._list = [ + ( + normalize_header_key(k, lower=False, encoding=encoding), + normalize_header_key(k, lower=True, encoding=encoding), + normalize_header_value(v, encoding), + ) + for k, v in headers + ] + + self._encoding = encoding + + @property + def encoding(self) -> str: + """ + Header encoding is mandated as ascii, but we allow fallbacks to utf-8 + or iso-8859-1. + """ + if self._encoding is None: + for encoding in ["ascii", "utf-8"]: + for key, value in self.raw: + try: + key.decode(encoding) + value.decode(encoding) + except UnicodeDecodeError: + break + else: + # The else block runs if 'break' did not occur, meaning + # all values fitted the encoding. + self._encoding = encoding + break + else: + # The ISO-8859-1 encoding covers all 256 code points in a byte, + # so will never raise decode errors. + self._encoding = "iso-8859-1" + return self._encoding + + @encoding.setter + def encoding(self, value: str) -> None: + self._encoding = value + + @property + def raw(self) -> typing.List[typing.Tuple[bytes, bytes]]: + """ + Returns a list of the raw header items, as byte pairs. + """ + return [(raw_key, value) for raw_key, _, value in self._list] + + def keys(self) -> typing.KeysView[str]: + return {key.decode(self.encoding): None for _, key, value in self._list}.keys() + + def values(self) -> typing.ValuesView[str]: + values_dict: typing.Dict[str, str] = {} + for _, key, value in self._list: + str_key = key.decode(self.encoding) + str_value = value.decode(self.encoding) + if str_key in values_dict: + values_dict[str_key] += f", {str_value}" + else: + values_dict[str_key] = str_value + return values_dict.values() + + def items(self) -> typing.ItemsView[str, str]: + """ + Return `(key, value)` items of headers. Concatenate headers + into a single comma separated value when a key occurs multiple times. + """ + values_dict: typing.Dict[str, str] = {} + for _, key, value in self._list: + str_key = key.decode(self.encoding) + str_value = value.decode(self.encoding) + if str_key in values_dict: + values_dict[str_key] += f", {str_value}" + else: + values_dict[str_key] = str_value + return values_dict.items() + + def multi_items(self) -> typing.List[typing.Tuple[str, str]]: + """ + Return a list of `(key, value)` pairs of headers. Allow multiple + occurrences of the same key without concatenating into a single + comma separated value. + """ + return [ + (key.decode(self.encoding), value.decode(self.encoding)) + for _, key, value in self._list + ] + + def get(self, key: str, default: typing.Any = None) -> typing.Any: + """ + Return a header value. If multiple occurrences of the header occur + then concatenate them together with commas. + """ + try: + return self[key] + except KeyError: + return default + + def get_list(self, key: str, split_commas: bool = False) -> typing.List[str]: + """ + Return a list of all header values for a given key. + If `split_commas=True` is passed, then any comma separated header + values are split into multiple return strings. + """ + get_header_key = key.lower().encode(self.encoding) + + values = [ + item_value.decode(self.encoding) + for _, item_key, item_value in self._list + if item_key.lower() == get_header_key + ] + + if not split_commas: + return values + + split_values = [] + for value in values: + split_values.extend([item.strip() for item in value.split(",")]) + return split_values + + def update(self, headers: typing.Optional[HeaderTypes] = None) -> None: # type: ignore + headers = Headers(headers) + for key in headers.keys(): + if key in self: + self.pop(key) + self._list.extend(headers._list) + + def copy(self) -> "Headers": + return Headers(self, encoding=self.encoding) + + def __getitem__(self, key: str) -> str: + """ + Return a single header value. + + If there are multiple headers with the same key, then we concatenate + them with commas. See: https://tools.ietf.org/html/rfc7230#section-3.2.2 + """ + normalized_key = key.lower().encode(self.encoding) + + items = [ + header_value.decode(self.encoding) + for _, header_key, header_value in self._list + if header_key == normalized_key + ] + + if items: + return ", ".join(items) + + raise KeyError(key) + + def __setitem__(self, key: str, value: str) -> None: + """ + Set the header `key` to `value`, removing any duplicate entries. + Retains insertion order. + """ + set_key = key.encode(self._encoding or "utf-8") + set_value = value.encode(self._encoding or "utf-8") + lookup_key = set_key.lower() + + found_indexes = [ + idx + for idx, (_, item_key, _) in enumerate(self._list) + if item_key == lookup_key + ] + + for idx in reversed(found_indexes[1:]): + del self._list[idx] + + if found_indexes: + idx = found_indexes[0] + self._list[idx] = (set_key, lookup_key, set_value) + else: + self._list.append((set_key, lookup_key, set_value)) + + def __delitem__(self, key: str) -> None: + """ + Remove the header `key`. + """ + del_key = key.lower().encode(self.encoding) + + pop_indexes = [ + idx + for idx, (_, item_key, _) in enumerate(self._list) + if item_key.lower() == del_key + ] + + if not pop_indexes: + raise KeyError(key) + + for idx in reversed(pop_indexes): + del self._list[idx] + + def __contains__(self, key: typing.Any) -> bool: + header_key = key.lower().encode(self.encoding) + return header_key in [key for _, key, _ in self._list] + + def __iter__(self) -> typing.Iterator[typing.Any]: + return iter(self.keys()) + + def __len__(self) -> int: + return len(self._list) + + def __eq__(self, other: typing.Any) -> bool: + try: + other_headers = Headers(other) + except ValueError: + return False + + self_list = [(key, value) for _, key, value in self._list] + other_list = [(key, value) for _, key, value in other_headers._list] + return sorted(self_list) == sorted(other_list) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + + encoding_str = "" + if self.encoding != "ascii": + encoding_str = f", encoding={self.encoding!r}" + + as_list = list(obfuscate_sensitive_headers(self.multi_items())) + as_dict = dict(as_list) + + no_duplicate_keys = len(as_dict) == len(as_list) + if no_duplicate_keys: + return f"{class_name}({as_dict!r}{encoding_str})" + return f"{class_name}({as_list!r}{encoding_str})" + + +class Request: + def __init__( + self, + method: typing.Union[str, bytes], + url: typing.Union["URL", str], + *, + params: typing.Optional[QueryParamTypes] = None, + headers: typing.Optional[HeaderTypes] = None, + cookies: typing.Optional[CookieTypes] = None, + content: typing.Optional[RequestContent] = None, + data: typing.Optional[RequestData] = None, + files: typing.Optional[RequestFiles] = None, + json: typing.Optional[typing.Any] = None, + stream: typing.Union[SyncByteStream, AsyncByteStream, None] = None, + extensions: typing.Optional[RequestExtensions] = None, + ): + self.method = ( + method.decode("ascii").upper() + if isinstance(method, bytes) + else method.upper() + ) + self.url = URL(url) + if params is not None: + self.url = self.url.copy_merge_params(params=params) + self.headers = Headers(headers) + self.extensions = {} if extensions is None else extensions + + if cookies: + Cookies(cookies).set_cookie_header(self) + + if stream is None: + content_type: typing.Optional[str] = self.headers.get("content-type") + headers, stream = encode_request( + content=content, + data=data, + files=files, + json=json, + boundary=get_multipart_boundary_from_content_type( + content_type=content_type.encode(self.headers.encoding) + if content_type + else None + ), + ) + self._prepare(headers) + self.stream = stream + # Load the request body, except for streaming content. + if isinstance(stream, ByteStream): + self.read() + else: + # There's an important distinction between `Request(content=...)`, + # and `Request(stream=...)`. + # + # Using `content=...` implies automatically populated `Host` and content + # headers, of either `Content-Length: ...` or `Transfer-Encoding: chunked`. + # + # Using `stream=...` will not automatically include *any* auto-populated headers. + # + # As an end-user you don't really need `stream=...`. It's only + # useful when: + # + # * Preserving the request stream when copying requests, eg for redirects. + # * Creating request instances on the *server-side* of the transport API. + self.stream = stream + + def _prepare(self, default_headers: typing.Dict[str, str]) -> None: + for key, value in default_headers.items(): + # Ignore Transfer-Encoding if the Content-Length has been set explicitly. + if key.lower() == "transfer-encoding" and "Content-Length" in self.headers: + continue + self.headers.setdefault(key, value) + + auto_headers: typing.List[typing.Tuple[bytes, bytes]] = [] + + has_host = "Host" in self.headers + has_content_length = ( + "Content-Length" in self.headers or "Transfer-Encoding" in self.headers + ) + + if not has_host and self.url.host: + auto_headers.append((b"Host", self.url.netloc)) + if not has_content_length and self.method in ("POST", "PUT", "PATCH"): + auto_headers.append((b"Content-Length", b"0")) + + self.headers = Headers(auto_headers + self.headers.raw) + + @property + def content(self) -> bytes: + if not hasattr(self, "_content"): + raise RequestNotRead() + return self._content + + def read(self) -> bytes: + """ + Read and return the request content. + """ + if not hasattr(self, "_content"): + assert isinstance(self.stream, typing.Iterable) + self._content = b"".join(self.stream) + if not isinstance(self.stream, ByteStream): + # If a streaming request has been read entirely into memory, then + # we can replace the stream with a raw bytes implementation, + # to ensure that any non-replayable streams can still be used. + self.stream = ByteStream(self._content) + return self._content + + async def aread(self) -> bytes: + """ + Read and return the request content. + """ + if not hasattr(self, "_content"): + assert isinstance(self.stream, typing.AsyncIterable) + self._content = b"".join([part async for part in self.stream]) + if not isinstance(self.stream, ByteStream): + # If a streaming request has been read entirely into memory, then + # we can replace the stream with a raw bytes implementation, + # to ensure that any non-replayable streams can still be used. + self.stream = ByteStream(self._content) + return self._content + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + url = str(self.url) + return f"<{class_name}({self.method!r}, {url!r})>" + + def __getstate__(self) -> typing.Dict[str, typing.Any]: + return { + name: value + for name, value in self.__dict__.items() + if name not in ["extensions", "stream"] + } + + def __setstate__(self, state: typing.Dict[str, typing.Any]) -> None: + for name, value in state.items(): + setattr(self, name, value) + self.extensions = {} + self.stream = UnattachedStream() + + +class Response: + def __init__( + self, + status_code: int, + *, + headers: typing.Optional[HeaderTypes] = None, + content: typing.Optional[ResponseContent] = None, + text: typing.Optional[str] = None, + html: typing.Optional[str] = None, + json: typing.Any = None, + stream: typing.Union[SyncByteStream, AsyncByteStream, None] = None, + request: typing.Optional[Request] = None, + extensions: typing.Optional[ResponseExtensions] = None, + history: typing.Optional[typing.List["Response"]] = None, + default_encoding: typing.Union[str, typing.Callable[[bytes], str]] = "utf-8", + ): + self.status_code = status_code + self.headers = Headers(headers) + + self._request: typing.Optional[Request] = request + + # When follow_redirects=False and a redirect is received, + # the client will set `response.next_request`. + self.next_request: typing.Optional[Request] = None + + self.extensions = {} if extensions is None else extensions + self.history = [] if history is None else list(history) + + self.is_closed = False + self.is_stream_consumed = False + + self.default_encoding = default_encoding + + if stream is None: + headers, stream = encode_response(content, text, html, json) + self._prepare(headers) + self.stream = stream + if isinstance(stream, ByteStream): + # Load the response body, except for streaming content. + self.read() + else: + # There's an important distinction between `Response(content=...)`, + # and `Response(stream=...)`. + # + # Using `content=...` implies automatically populated content headers, + # of either `Content-Length: ...` or `Transfer-Encoding: chunked`. + # + # Using `stream=...` will not automatically include any content headers. + # + # As an end-user you don't really need `stream=...`. It's only + # useful when creating response instances having received a stream + # from the transport API. + self.stream = stream + + self._num_bytes_downloaded = 0 + + def _prepare(self, default_headers: typing.Dict[str, str]) -> None: + for key, value in default_headers.items(): + # Ignore Transfer-Encoding if the Content-Length has been set explicitly. + if key.lower() == "transfer-encoding" and "content-length" in self.headers: + continue + self.headers.setdefault(key, value) + + @property + def elapsed(self) -> datetime.timedelta: + """ + Returns the time taken for the complete request/response + cycle to complete. + """ + if not hasattr(self, "_elapsed"): + raise RuntimeError( + "'.elapsed' may only be accessed after the response " + "has been read or closed." + ) + return self._elapsed + + @elapsed.setter + def elapsed(self, elapsed: datetime.timedelta) -> None: + self._elapsed = elapsed + + @property + def request(self) -> Request: + """ + Returns the request instance associated to the current response. + """ + if self._request is None: + raise RuntimeError( + "The request instance has not been set on this response." + ) + return self._request + + @request.setter + def request(self, value: Request) -> None: + self._request = value + + @property + def http_version(self) -> str: + try: + http_version: bytes = self.extensions["http_version"] + except KeyError: + return "HTTP/1.1" + else: + return http_version.decode("ascii", errors="ignore") + + @property + def reason_phrase(self) -> str: + try: + reason_phrase: bytes = self.extensions["reason_phrase"] + except KeyError: + return codes.get_reason_phrase(self.status_code) + else: + return reason_phrase.decode("ascii", errors="ignore") + + @property + def url(self) -> URL: + """ + Returns the URL for which the request was made. + """ + return self.request.url + + @property + def content(self) -> bytes: + if not hasattr(self, "_content"): + raise ResponseNotRead() + return self._content + + @property + def text(self) -> str: + if not hasattr(self, "_text"): + content = self.content + if not content: + self._text = "" + else: + decoder = TextDecoder(encoding=self.encoding or "utf-8") + self._text = "".join([decoder.decode(self.content), decoder.flush()]) + return self._text + + @property + def encoding(self) -> typing.Optional[str]: + """ + Return an encoding to use for decoding the byte content into text. + The priority for determining this is given by... + + * `.encoding = <>` has been set explicitly. + * The encoding as specified by the charset parameter in the Content-Type header. + * The encoding as determined by `default_encoding`, which may either be + a string like "utf-8" indicating the encoding to use, or may be a callable + which enables charset autodetection. + """ + if not hasattr(self, "_encoding"): + encoding = self.charset_encoding + if encoding is None or not is_known_encoding(encoding): + if isinstance(self.default_encoding, str): + encoding = self.default_encoding + elif hasattr(self, "_content"): + encoding = self.default_encoding(self._content) + self._encoding = encoding or "utf-8" + return self._encoding + + @encoding.setter + def encoding(self, value: str) -> None: + self._encoding = value + + @property + def charset_encoding(self) -> typing.Optional[str]: + """ + Return the encoding, as specified by the Content-Type header. + """ + content_type = self.headers.get("Content-Type") + if content_type is None: + return None + + return parse_content_type_charset(content_type) + + def _get_content_decoder(self) -> ContentDecoder: + """ + Returns a decoder instance which can be used to decode the raw byte + content, depending on the Content-Encoding used in the response. + """ + if not hasattr(self, "_decoder"): + decoders: typing.List[ContentDecoder] = [] + values = self.headers.get_list("content-encoding", split_commas=True) + for value in values: + value = value.strip().lower() + try: + decoder_cls = SUPPORTED_DECODERS[value] + decoders.append(decoder_cls()) + except KeyError: + continue + + if len(decoders) == 1: + self._decoder = decoders[0] + elif len(decoders) > 1: + self._decoder = MultiDecoder(children=decoders) + else: + self._decoder = IdentityDecoder() + + return self._decoder + + @property + def is_informational(self) -> bool: + """ + A property which is `True` for 1xx status codes, `False` otherwise. + """ + return codes.is_informational(self.status_code) + + @property + def is_success(self) -> bool: + """ + A property which is `True` for 2xx status codes, `False` otherwise. + """ + return codes.is_success(self.status_code) + + @property + def is_redirect(self) -> bool: + """ + A property which is `True` for 3xx status codes, `False` otherwise. + + Note that not all responses with a 3xx status code indicate a URL redirect. + + Use `response.has_redirect_location` to determine responses with a properly + formed URL redirection. + """ + return codes.is_redirect(self.status_code) + + @property + def is_client_error(self) -> bool: + """ + A property which is `True` for 4xx status codes, `False` otherwise. + """ + return codes.is_client_error(self.status_code) + + @property + def is_server_error(self) -> bool: + """ + A property which is `True` for 5xx status codes, `False` otherwise. + """ + return codes.is_server_error(self.status_code) + + @property + def is_error(self) -> bool: + """ + A property which is `True` for 4xx and 5xx status codes, `False` otherwise. + """ + return codes.is_error(self.status_code) + + @property + def has_redirect_location(self) -> bool: + """ + Returns True for 3xx responses with a properly formed URL redirection, + `False` otherwise. + """ + return ( + self.status_code + in ( + # 301 (Cacheable redirect. Method may change to GET.) + codes.MOVED_PERMANENTLY, + # 302 (Uncacheable redirect. Method may change to GET.) + codes.FOUND, + # 303 (Client should make a GET or HEAD request.) + codes.SEE_OTHER, + # 307 (Equiv. 302, but retain method) + codes.TEMPORARY_REDIRECT, + # 308 (Equiv. 301, but retain method) + codes.PERMANENT_REDIRECT, + ) + and "Location" in self.headers + ) + + def raise_for_status(self) -> None: + """ + Raise the `HTTPStatusError` if one occurred. + """ + request = self._request + if request is None: + raise RuntimeError( + "Cannot call `raise_for_status` as the request " + "instance has not been set on this response." + ) + + if self.is_success: + return + + if self.has_redirect_location: + message = ( + "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n" + "Redirect location: '{0.headers[location]}'\n" + "For more information check: https://httpstatuses.com/{0.status_code}" + ) + else: + message = ( + "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n" + "For more information check: https://httpstatuses.com/{0.status_code}" + ) + + status_class = self.status_code // 100 + error_types = { + 1: "Informational response", + 3: "Redirect response", + 4: "Client error", + 5: "Server error", + } + error_type = error_types.get(status_class, "Invalid status code") + message = message.format(self, error_type=error_type) + raise HTTPStatusError(message, request=request, response=self) + + def json(self, **kwargs: typing.Any) -> typing.Any: + if self.charset_encoding is None and self.content and len(self.content) > 3: + encoding = guess_json_utf(self.content) + if encoding is not None: + return jsonlib.loads(self.content.decode(encoding), **kwargs) + return jsonlib.loads(self.text, **kwargs) + + @property + def cookies(self) -> "Cookies": + if not hasattr(self, "_cookies"): + self._cookies = Cookies() + self._cookies.extract_cookies(self) + return self._cookies + + @property + def links(self) -> typing.Dict[typing.Optional[str], typing.Dict[str, str]]: + """ + Returns the parsed header links of the response, if any + """ + header = self.headers.get("link") + ldict = {} + if header: + links = parse_header_links(header) + for link in links: + key = link.get("rel") or link.get("url") + ldict[key] = link + return ldict + + @property + def num_bytes_downloaded(self) -> int: + return self._num_bytes_downloaded + + def __repr__(self) -> str: + return f"" + + def __getstate__(self) -> typing.Dict[str, typing.Any]: + return { + name: value + for name, value in self.__dict__.items() + if name not in ["extensions", "stream", "is_closed", "_decoder"] + } + + def __setstate__(self, state: typing.Dict[str, typing.Any]) -> None: + for name, value in state.items(): + setattr(self, name, value) + self.is_closed = True + self.extensions = {} + self.stream = UnattachedStream() + + def read(self) -> bytes: + """ + Read and return the response content. + """ + if not hasattr(self, "_content"): + self._content = b"".join(self.iter_bytes()) + return self._content + + def iter_bytes( + self, chunk_size: typing.Optional[int] = None + ) -> typing.Iterator[bytes]: + """ + A byte-iterator over the decoded response content. + This allows us to handle gzip, deflate, and brotli encoded responses. + """ + if hasattr(self, "_content"): + chunk_size = len(self._content) if chunk_size is None else chunk_size + for i in range(0, len(self._content), max(chunk_size, 1)): + yield self._content[i : i + chunk_size] + else: + decoder = self._get_content_decoder() + chunker = ByteChunker(chunk_size=chunk_size) + with request_context(request=self._request): + for raw_bytes in self.iter_raw(): + decoded = decoder.decode(raw_bytes) + for chunk in chunker.decode(decoded): + yield chunk + decoded = decoder.flush() + for chunk in chunker.decode(decoded): + yield chunk # pragma: no cover + for chunk in chunker.flush(): + yield chunk + + def iter_text( + self, chunk_size: typing.Optional[int] = None + ) -> typing.Iterator[str]: + """ + A str-iterator over the decoded response content + that handles both gzip, deflate, etc but also detects the content's + string encoding. + """ + decoder = TextDecoder(encoding=self.encoding or "utf-8") + chunker = TextChunker(chunk_size=chunk_size) + with request_context(request=self._request): + for byte_content in self.iter_bytes(): + text_content = decoder.decode(byte_content) + for chunk in chunker.decode(text_content): + yield chunk + text_content = decoder.flush() + for chunk in chunker.decode(text_content): + yield chunk + for chunk in chunker.flush(): + yield chunk + + def iter_lines(self) -> typing.Iterator[str]: + decoder = LineDecoder() + with request_context(request=self._request): + for text in self.iter_text(): + for line in decoder.decode(text): + yield line + for line in decoder.flush(): + yield line + + def iter_raw( + self, chunk_size: typing.Optional[int] = None + ) -> typing.Iterator[bytes]: + """ + A byte-iterator over the raw response content. + """ + if self.is_stream_consumed: + raise StreamConsumed() + if self.is_closed: + raise StreamClosed() + if not isinstance(self.stream, SyncByteStream): + raise RuntimeError("Attempted to call a sync iterator on an async stream.") + + self.is_stream_consumed = True + self._num_bytes_downloaded = 0 + chunker = ByteChunker(chunk_size=chunk_size) + + with request_context(request=self._request): + for raw_stream_bytes in self.stream: + self._num_bytes_downloaded += len(raw_stream_bytes) + for chunk in chunker.decode(raw_stream_bytes): + yield chunk + + for chunk in chunker.flush(): + yield chunk + + self.close() + + def close(self) -> None: + """ + Close the response and release the connection. + Automatically called if the response body is read to completion. + """ + if not isinstance(self.stream, SyncByteStream): + raise RuntimeError("Attempted to call an sync close on an async stream.") + + if not self.is_closed: + self.is_closed = True + with request_context(request=self._request): + self.stream.close() + + async def aread(self) -> bytes: + """ + Read and return the response content. + """ + if not hasattr(self, "_content"): + self._content = b"".join([part async for part in self.aiter_bytes()]) + return self._content + + async def aiter_bytes( + self, chunk_size: typing.Optional[int] = None + ) -> typing.AsyncIterator[bytes]: + """ + A byte-iterator over the decoded response content. + This allows us to handle gzip, deflate, and brotli encoded responses. + """ + if hasattr(self, "_content"): + chunk_size = len(self._content) if chunk_size is None else chunk_size + for i in range(0, len(self._content), max(chunk_size, 1)): + yield self._content[i : i + chunk_size] + else: + decoder = self._get_content_decoder() + chunker = ByteChunker(chunk_size=chunk_size) + with request_context(request=self._request): + async for raw_bytes in self.aiter_raw(): + decoded = decoder.decode(raw_bytes) + for chunk in chunker.decode(decoded): + yield chunk + decoded = decoder.flush() + for chunk in chunker.decode(decoded): + yield chunk # pragma: no cover + for chunk in chunker.flush(): + yield chunk + + async def aiter_text( + self, chunk_size: typing.Optional[int] = None + ) -> typing.AsyncIterator[str]: + """ + A str-iterator over the decoded response content + that handles both gzip, deflate, etc but also detects the content's + string encoding. + """ + decoder = TextDecoder(encoding=self.encoding or "utf-8") + chunker = TextChunker(chunk_size=chunk_size) + with request_context(request=self._request): + async for byte_content in self.aiter_bytes(): + text_content = decoder.decode(byte_content) + for chunk in chunker.decode(text_content): + yield chunk + text_content = decoder.flush() + for chunk in chunker.decode(text_content): + yield chunk + for chunk in chunker.flush(): + yield chunk + + async def aiter_lines(self) -> typing.AsyncIterator[str]: + decoder = LineDecoder() + with request_context(request=self._request): + async for text in self.aiter_text(): + for line in decoder.decode(text): + yield line + for line in decoder.flush(): + yield line + + async def aiter_raw( + self, chunk_size: typing.Optional[int] = None + ) -> typing.AsyncIterator[bytes]: + """ + A byte-iterator over the raw response content. + """ + if self.is_stream_consumed: + raise StreamConsumed() + if self.is_closed: + raise StreamClosed() + if not isinstance(self.stream, AsyncByteStream): + raise RuntimeError("Attempted to call an async iterator on an sync stream.") + + self.is_stream_consumed = True + self._num_bytes_downloaded = 0 + chunker = ByteChunker(chunk_size=chunk_size) + + with request_context(request=self._request): + async for raw_stream_bytes in self.stream: + self._num_bytes_downloaded += len(raw_stream_bytes) + for chunk in chunker.decode(raw_stream_bytes): + yield chunk + + for chunk in chunker.flush(): + yield chunk + + await self.aclose() + + async def aclose(self) -> None: + """ + Close the response and release the connection. + Automatically called if the response body is read to completion. + """ + if not isinstance(self.stream, AsyncByteStream): + raise RuntimeError("Attempted to call an async close on an sync stream.") + + if not self.is_closed: + self.is_closed = True + with request_context(request=self._request): + await self.stream.aclose() + + +class Cookies(typing.MutableMapping[str, str]): + """ + HTTP Cookies, as a mutable mapping. + """ + + def __init__(self, cookies: typing.Optional[CookieTypes] = None) -> None: + if cookies is None or isinstance(cookies, dict): + self.jar = CookieJar() + if isinstance(cookies, dict): + for key, value in cookies.items(): + self.set(key, value) + elif isinstance(cookies, list): + self.jar = CookieJar() + for key, value in cookies: + self.set(key, value) + elif isinstance(cookies, Cookies): + self.jar = CookieJar() + for cookie in cookies.jar: + self.jar.set_cookie(cookie) + else: + self.jar = cookies + + def extract_cookies(self, response: Response) -> None: + """ + Loads any cookies based on the response `Set-Cookie` headers. + """ + urllib_response = self._CookieCompatResponse(response) + urllib_request = self._CookieCompatRequest(response.request) + + self.jar.extract_cookies(urllib_response, urllib_request) # type: ignore + + def set_cookie_header(self, request: Request) -> None: + """ + Sets an appropriate 'Cookie:' HTTP header on the `Request`. + """ + urllib_request = self._CookieCompatRequest(request) + self.jar.add_cookie_header(urllib_request) + + def set(self, name: str, value: str, domain: str = "", path: str = "/") -> None: + """ + Set a cookie value by name. May optionally include domain and path. + """ + kwargs = { + "version": 0, + "name": name, + "value": value, + "port": None, + "port_specified": False, + "domain": domain, + "domain_specified": bool(domain), + "domain_initial_dot": domain.startswith("."), + "path": path, + "path_specified": bool(path), + "secure": False, + "expires": None, + "discard": True, + "comment": None, + "comment_url": None, + "rest": {"HttpOnly": None}, + "rfc2109": False, + } + cookie = Cookie(**kwargs) # type: ignore + self.jar.set_cookie(cookie) + + def get( # type: ignore + self, + name: str, + default: typing.Optional[str] = None, + domain: typing.Optional[str] = None, + path: typing.Optional[str] = None, + ) -> typing.Optional[str]: + """ + Get a cookie by name. May optionally include domain and path + in order to specify exactly which cookie to retrieve. + """ + value = None + for cookie in self.jar: + if cookie.name == name: + if domain is None or cookie.domain == domain: + if path is None or cookie.path == path: + if value is not None: + message = f"Multiple cookies exist with name={name}" + raise CookieConflict(message) + value = cookie.value + + if value is None: + return default + return value + + def delete( + self, + name: str, + domain: typing.Optional[str] = None, + path: typing.Optional[str] = None, + ) -> None: + """ + Delete a cookie by name. May optionally include domain and path + in order to specify exactly which cookie to delete. + """ + if domain is not None and path is not None: + return self.jar.clear(domain, path, name) + + remove = [ + cookie + for cookie in self.jar + if cookie.name == name + and (domain is None or cookie.domain == domain) + and (path is None or cookie.path == path) + ] + + for cookie in remove: + self.jar.clear(cookie.domain, cookie.path, cookie.name) + + def clear( + self, domain: typing.Optional[str] = None, path: typing.Optional[str] = None + ) -> None: + """ + Delete all cookies. Optionally include a domain and path in + order to only delete a subset of all the cookies. + """ + args = [] + if domain is not None: + args.append(domain) + if path is not None: + assert domain is not None + args.append(path) + self.jar.clear(*args) + + def update(self, cookies: typing.Optional[CookieTypes] = None) -> None: # type: ignore + cookies = Cookies(cookies) + for cookie in cookies.jar: + self.jar.set_cookie(cookie) + + def __setitem__(self, name: str, value: str) -> None: + return self.set(name, value) + + def __getitem__(self, name: str) -> str: + value = self.get(name) + if value is None: + raise KeyError(name) + return value + + def __delitem__(self, name: str) -> None: + return self.delete(name) + + def __len__(self) -> int: + return len(self.jar) + + def __iter__(self) -> typing.Iterator[str]: + return (cookie.name for cookie in self.jar) + + def __bool__(self) -> bool: + for _ in self.jar: + return True + return False + + def __repr__(self) -> str: + cookies_repr = ", ".join( + [ + f"" + for cookie in self.jar + ] + ) + + return f"" + + class _CookieCompatRequest(urllib.request.Request): + """ + Wraps a `Request` instance up in a compatibility interface suitable + for use with `CookieJar` operations. + """ + + def __init__(self, request: Request) -> None: + super().__init__( + url=str(request.url), + headers=dict(request.headers), + method=request.method, + ) + self.request = request + + def add_unredirected_header(self, key: str, value: str) -> None: + super().add_unredirected_header(key, value) + self.request.headers[key] = value + + class _CookieCompatResponse: + """ + Wraps a `Request` instance up in a compatibility interface suitable + for use with `CookieJar` operations. + """ + + def __init__(self, response: Response): + self.response = response + + def info(self) -> email.message.Message: + info = email.message.Message() + for key, value in self.response.headers.multi_items(): + # Note that setting `info[key]` here is an "append" operation, + # not a "replace" operation. + # https://docs.python.org/3/library/email.compat32-message.html#email.message.Message.__setitem__ + info[key] = value + return info diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_multipart.py b/Backend/venv/lib/python3.12/site-packages/httpx/_multipart.py new file mode 100644 index 00000000..446f4ad2 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_multipart.py @@ -0,0 +1,267 @@ +import binascii +import io +import os +import typing +from pathlib import Path + +from ._types import ( + AsyncByteStream, + FileContent, + FileTypes, + RequestData, + RequestFiles, + SyncByteStream, +) +from ._utils import ( + format_form_param, + guess_content_type, + peek_filelike_length, + primitive_value_to_str, + to_bytes, +) + + +def get_multipart_boundary_from_content_type( + content_type: typing.Optional[bytes], +) -> typing.Optional[bytes]: + if not content_type or not content_type.startswith(b"multipart/form-data"): + return None + # parse boundary according to + # https://www.rfc-editor.org/rfc/rfc2046#section-5.1.1 + if b";" in content_type: + for section in content_type.split(b";"): + if section.strip().lower().startswith(b"boundary="): + return section.strip()[len(b"boundary=") :].strip(b'"') + return None + + +class DataField: + """ + A single form field item, within a multipart form field. + """ + + def __init__( + self, name: str, value: typing.Union[str, bytes, int, float, None] + ) -> None: + if not isinstance(name, str): + raise TypeError( + f"Invalid type for name. Expected str, got {type(name)}: {name!r}" + ) + if value is not None and not isinstance(value, (str, bytes, int, float)): + raise TypeError( + f"Invalid type for value. Expected primitive type, got {type(value)}: {value!r}" + ) + self.name = name + self.value: typing.Union[str, bytes] = ( + value if isinstance(value, bytes) else primitive_value_to_str(value) + ) + + def render_headers(self) -> bytes: + if not hasattr(self, "_headers"): + name = format_form_param("name", self.name) + self._headers = b"".join( + [b"Content-Disposition: form-data; ", name, b"\r\n\r\n"] + ) + + return self._headers + + def render_data(self) -> bytes: + if not hasattr(self, "_data"): + self._data = to_bytes(self.value) + + return self._data + + def get_length(self) -> int: + headers = self.render_headers() + data = self.render_data() + return len(headers) + len(data) + + def render(self) -> typing.Iterator[bytes]: + yield self.render_headers() + yield self.render_data() + + +class FileField: + """ + A single file field item, within a multipart form field. + """ + + CHUNK_SIZE = 64 * 1024 + + def __init__(self, name: str, value: FileTypes) -> None: + self.name = name + + fileobj: FileContent + + headers: typing.Dict[str, str] = {} + content_type: typing.Optional[str] = None + + # This large tuple based API largely mirror's requests' API + # It would be good to think of better APIs for this that we could include in httpx 2.0 + # since variable length tuples (especially of 4 elements) are quite unwieldly + if isinstance(value, tuple): + if len(value) == 2: + # neither the 3rd parameter (content_type) nor the 4th (headers) was included + filename, fileobj = value # type: ignore + elif len(value) == 3: + filename, fileobj, content_type = value # type: ignore + else: + # all 4 parameters included + filename, fileobj, content_type, headers = value # type: ignore + else: + filename = Path(str(getattr(value, "name", "upload"))).name + fileobj = value + + if content_type is None: + content_type = guess_content_type(filename) + + has_content_type_header = any("content-type" in key.lower() for key in headers) + if content_type is not None and not has_content_type_header: + # note that unlike requests, we ignore the content_type + # provided in the 3rd tuple element if it is also included in the headers + # requests does the opposite (it overwrites the header with the 3rd tuple element) + headers["Content-Type"] = content_type + + if isinstance(fileobj, io.StringIO): + raise TypeError( + "Multipart file uploads require 'io.BytesIO', not 'io.StringIO'." + ) + if isinstance(fileobj, io.TextIOBase): + raise TypeError( + "Multipart file uploads must be opened in binary mode, not text mode." + ) + + self.filename = filename + self.file = fileobj + self.headers = headers + + def get_length(self) -> typing.Optional[int]: + headers = self.render_headers() + + if isinstance(self.file, (str, bytes)): + return len(headers) + len(to_bytes(self.file)) + + file_length = peek_filelike_length(self.file) + + # If we can't determine the filesize without reading it into memory, + # then return `None` here, to indicate an unknown file length. + if file_length is None: + return None + + return len(headers) + file_length + + def render_headers(self) -> bytes: + if not hasattr(self, "_headers"): + parts = [ + b"Content-Disposition: form-data; ", + format_form_param("name", self.name), + ] + if self.filename: + filename = format_form_param("filename", self.filename) + parts.extend([b"; ", filename]) + for header_name, header_value in self.headers.items(): + key, val = f"\r\n{header_name}: ".encode(), header_value.encode() + parts.extend([key, val]) + parts.append(b"\r\n\r\n") + self._headers = b"".join(parts) + + return self._headers + + def render_data(self) -> typing.Iterator[bytes]: + if isinstance(self.file, (str, bytes)): + yield to_bytes(self.file) + return + + if hasattr(self.file, "seek"): + try: + self.file.seek(0) + except io.UnsupportedOperation: + pass + + chunk = self.file.read(self.CHUNK_SIZE) + while chunk: + yield to_bytes(chunk) + chunk = self.file.read(self.CHUNK_SIZE) + + def render(self) -> typing.Iterator[bytes]: + yield self.render_headers() + yield from self.render_data() + + +class MultipartStream(SyncByteStream, AsyncByteStream): + """ + Request content as streaming multipart encoded form data. + """ + + def __init__( + self, + data: RequestData, + files: RequestFiles, + boundary: typing.Optional[bytes] = None, + ) -> None: + if boundary is None: + boundary = binascii.hexlify(os.urandom(16)) + + self.boundary = boundary + self.content_type = "multipart/form-data; boundary=%s" % boundary.decode( + "ascii" + ) + self.fields = list(self._iter_fields(data, files)) + + def _iter_fields( + self, data: RequestData, files: RequestFiles + ) -> typing.Iterator[typing.Union[FileField, DataField]]: + for name, value in data.items(): + if isinstance(value, (tuple, list)): + for item in value: + yield DataField(name=name, value=item) + else: + yield DataField(name=name, value=value) + + file_items = files.items() if isinstance(files, typing.Mapping) else files + for name, value in file_items: + yield FileField(name=name, value=value) + + def iter_chunks(self) -> typing.Iterator[bytes]: + for field in self.fields: + yield b"--%s\r\n" % self.boundary + yield from field.render() + yield b"\r\n" + yield b"--%s--\r\n" % self.boundary + + def get_content_length(self) -> typing.Optional[int]: + """ + Return the length of the multipart encoded content, or `None` if + any of the files have a length that cannot be determined upfront. + """ + boundary_length = len(self.boundary) + length = 0 + + for field in self.fields: + field_length = field.get_length() + if field_length is None: + return None + + length += 2 + boundary_length + 2 # b"--{boundary}\r\n" + length += field_length + length += 2 # b"\r\n" + + length += 2 + boundary_length + 4 # b"--{boundary}--\r\n" + return length + + # Content stream interface. + + def get_headers(self) -> typing.Dict[str, str]: + content_length = self.get_content_length() + content_type = self.content_type + if content_length is None: + return {"Transfer-Encoding": "chunked", "Content-Type": content_type} + return {"Content-Length": str(content_length), "Content-Type": content_type} + + def __iter__(self) -> typing.Iterator[bytes]: + for chunk in self.iter_chunks(): + yield chunk + + async def __aiter__(self) -> typing.AsyncIterator[bytes]: + for chunk in self.iter_chunks(): + yield chunk diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_status_codes.py b/Backend/venv/lib/python3.12/site-packages/httpx/_status_codes.py new file mode 100644 index 00000000..671c30e1 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_status_codes.py @@ -0,0 +1,158 @@ +from enum import IntEnum + + +class codes(IntEnum): + """HTTP status codes and reason phrases + + Status codes from the following RFCs are all observed: + + * RFC 7231: Hypertext Transfer Protocol (HTTP/1.1), obsoletes 2616 + * RFC 6585: Additional HTTP Status Codes + * RFC 3229: Delta encoding in HTTP + * RFC 4918: HTTP Extensions for WebDAV, obsoletes 2518 + * RFC 5842: Binding Extensions to WebDAV + * RFC 7238: Permanent Redirect + * RFC 2295: Transparent Content Negotiation in HTTP + * RFC 2774: An HTTP Extension Framework + * RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2) + * RFC 2324: Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0) + * RFC 7725: An HTTP Status Code to Report Legal Obstacles + * RFC 8297: An HTTP Status Code for Indicating Hints + * RFC 8470: Using Early Data in HTTP + """ + + def __new__(cls, value: int, phrase: str = "") -> "codes": + obj = int.__new__(cls, value) + obj._value_ = value + + obj.phrase = phrase # type: ignore[attr-defined] + return obj + + def __str__(self) -> str: + return str(self.value) + + @classmethod + def get_reason_phrase(cls, value: int) -> str: + try: + return codes(value).phrase # type: ignore + except ValueError: + return "" + + @classmethod + def is_informational(cls, value: int) -> bool: + """ + Returns `True` for 1xx status codes, `False` otherwise. + """ + return 100 <= value <= 199 + + @classmethod + def is_success(cls, value: int) -> bool: + """ + Returns `True` for 2xx status codes, `False` otherwise. + """ + return 200 <= value <= 299 + + @classmethod + def is_redirect(cls, value: int) -> bool: + """ + Returns `True` for 3xx status codes, `False` otherwise. + """ + return 300 <= value <= 399 + + @classmethod + def is_client_error(cls, value: int) -> bool: + """ + Returns `True` for 4xx status codes, `False` otherwise. + """ + return 400 <= value <= 499 + + @classmethod + def is_server_error(cls, value: int) -> bool: + """ + Returns `True` for 5xx status codes, `False` otherwise. + """ + return 500 <= value <= 599 + + @classmethod + def is_error(cls, value: int) -> bool: + """ + Returns `True` for 4xx or 5xx status codes, `False` otherwise. + """ + return 400 <= value <= 599 + + # informational + CONTINUE = 100, "Continue" + SWITCHING_PROTOCOLS = 101, "Switching Protocols" + PROCESSING = 102, "Processing" + EARLY_HINTS = 103, "Early Hints" + + # success + OK = 200, "OK" + CREATED = 201, "Created" + ACCEPTED = 202, "Accepted" + NON_AUTHORITATIVE_INFORMATION = 203, "Non-Authoritative Information" + NO_CONTENT = 204, "No Content" + RESET_CONTENT = 205, "Reset Content" + PARTIAL_CONTENT = 206, "Partial Content" + MULTI_STATUS = 207, "Multi-Status" + ALREADY_REPORTED = 208, "Already Reported" + IM_USED = 226, "IM Used" + + # redirection + MULTIPLE_CHOICES = 300, "Multiple Choices" + MOVED_PERMANENTLY = 301, "Moved Permanently" + FOUND = 302, "Found" + SEE_OTHER = 303, "See Other" + NOT_MODIFIED = 304, "Not Modified" + USE_PROXY = 305, "Use Proxy" + TEMPORARY_REDIRECT = 307, "Temporary Redirect" + PERMANENT_REDIRECT = 308, "Permanent Redirect" + + # client error + BAD_REQUEST = 400, "Bad Request" + UNAUTHORIZED = 401, "Unauthorized" + PAYMENT_REQUIRED = 402, "Payment Required" + FORBIDDEN = 403, "Forbidden" + NOT_FOUND = 404, "Not Found" + METHOD_NOT_ALLOWED = 405, "Method Not Allowed" + NOT_ACCEPTABLE = 406, "Not Acceptable" + PROXY_AUTHENTICATION_REQUIRED = 407, "Proxy Authentication Required" + REQUEST_TIMEOUT = 408, "Request Timeout" + CONFLICT = 409, "Conflict" + GONE = 410, "Gone" + LENGTH_REQUIRED = 411, "Length Required" + PRECONDITION_FAILED = 412, "Precondition Failed" + REQUEST_ENTITY_TOO_LARGE = 413, "Request Entity Too Large" + REQUEST_URI_TOO_LONG = 414, "Request-URI Too Long" + UNSUPPORTED_MEDIA_TYPE = 415, "Unsupported Media Type" + REQUESTED_RANGE_NOT_SATISFIABLE = 416, "Requested Range Not Satisfiable" + EXPECTATION_FAILED = 417, "Expectation Failed" + IM_A_TEAPOT = 418, "I'm a teapot" + MISDIRECTED_REQUEST = 421, "Misdirected Request" + UNPROCESSABLE_ENTITY = 422, "Unprocessable Entity" + LOCKED = 423, "Locked" + FAILED_DEPENDENCY = 424, "Failed Dependency" + TOO_EARLY = 425, "Too Early" + UPGRADE_REQUIRED = 426, "Upgrade Required" + PRECONDITION_REQUIRED = 428, "Precondition Required" + TOO_MANY_REQUESTS = 429, "Too Many Requests" + REQUEST_HEADER_FIELDS_TOO_LARGE = 431, "Request Header Fields Too Large" + UNAVAILABLE_FOR_LEGAL_REASONS = 451, "Unavailable For Legal Reasons" + + # server errors + INTERNAL_SERVER_ERROR = 500, "Internal Server Error" + NOT_IMPLEMENTED = 501, "Not Implemented" + BAD_GATEWAY = 502, "Bad Gateway" + SERVICE_UNAVAILABLE = 503, "Service Unavailable" + GATEWAY_TIMEOUT = 504, "Gateway Timeout" + HTTP_VERSION_NOT_SUPPORTED = 505, "HTTP Version Not Supported" + VARIANT_ALSO_NEGOTIATES = 506, "Variant Also Negotiates" + INSUFFICIENT_STORAGE = 507, "Insufficient Storage" + LOOP_DETECTED = 508, "Loop Detected" + NOT_EXTENDED = 510, "Not Extended" + NETWORK_AUTHENTICATION_REQUIRED = 511, "Network Authentication Required" + + +# Include lower-case styles for `requests` compatibility. +for code in codes: + setattr(codes, code._name_.lower(), int(code)) diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_transports/__init__.py b/Backend/venv/lib/python3.12/site-packages/httpx/_transports/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_transports/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpx/_transports/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96bc25727280cea5846d3a797d21df8adfc195ea GIT binary patch literal 200 zcmX@j%ge<81nP!LnIQTxh(HIQS%4zb87dhx8U0o=6fpsLpFwJVrR!(p=cekX=T+#t zq!wqFx8=sM-+XJ_W6>pLYTXQ$?+=$EDDmFeeXCg~ScmSp7T8S5Du=@(~~ zr0Ny`6(pvo7VBq}loV9x$Cnf(<`oy@7nKz2$H!;pWtPOp>lIY~;;_lhPbtkwwJTx; U+RF&U#UREt<8 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_transports/__pycache__/asgi.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpx/_transports/__pycache__/asgi.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..238c52106423c5a76f8193f19f6e0603042430e9 GIT binary patch literal 7728 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 McJIFlZBg<6FDKaj?f?J) literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_transports/__pycache__/base.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/httpx/_transports/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b75c754f78c4929f58cca0cc4c99bb19917f1553 GIT binary patch literal 4132 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 "Event": + if sniffio.current_async_library() == "trio": + import trio + + return trio.Event() + else: + import asyncio + + return asyncio.Event() + + +class ASGIResponseStream(AsyncByteStream): + def __init__(self, body: typing.List[bytes]) -> None: + self._body = body + + async def __aiter__(self) -> typing.AsyncIterator[bytes]: + yield b"".join(self._body) + + +class ASGITransport(AsyncBaseTransport): + """ + A custom AsyncTransport that handles sending requests directly to an ASGI app. + The simplest way to use this functionality is to use the `app` argument. + + ``` + client = httpx.AsyncClient(app=app) + ``` + + Alternatively, you can setup the transport instance explicitly. + This allows you to include any additional configuration arguments specific + to the ASGITransport class: + + ``` + transport = httpx.ASGITransport( + app=app, + root_path="/submount", + client=("1.2.3.4", 123) + ) + client = httpx.AsyncClient(transport=transport) + ``` + + Arguments: + + * `app` - The ASGI application. + * `raise_app_exceptions` - Boolean indicating if exceptions in the application + should be raised. Default to `True`. Can be set to `False` for use cases + such as testing the content of a client 500 response. + * `root_path` - The root path on which the ASGI application should be mounted. + * `client` - A two-tuple indicating the client IP and port of incoming requests. + ``` + """ + + def __init__( + self, + app: _ASGIApp, + raise_app_exceptions: bool = True, + root_path: str = "", + client: typing.Tuple[str, int] = ("127.0.0.1", 123), + ) -> None: + self.app = app + self.raise_app_exceptions = raise_app_exceptions + self.root_path = root_path + self.client = client + + async def handle_async_request( + self, + request: Request, + ) -> Response: + assert isinstance(request.stream, AsyncByteStream) + + # ASGI scope. + scope = { + "type": "http", + "asgi": {"version": "3.0"}, + "http_version": "1.1", + "method": request.method, + "headers": [(k.lower(), v) for (k, v) in request.headers.raw], + "scheme": request.url.scheme, + "path": request.url.path, + "raw_path": request.url.raw_path, + "query_string": request.url.query, + "server": (request.url.host, request.url.port), + "client": self.client, + "root_path": self.root_path, + } + + # Request. + request_body_chunks = request.stream.__aiter__() + request_complete = False + + # Response. + status_code = None + response_headers = None + body_parts = [] + response_started = False + response_complete = create_event() + + # ASGI callables. + + async def receive() -> typing.Dict[str, typing.Any]: + nonlocal request_complete + + if request_complete: + await response_complete.wait() + return {"type": "http.disconnect"} + + try: + body = await request_body_chunks.__anext__() + except StopAsyncIteration: + request_complete = True + return {"type": "http.request", "body": b"", "more_body": False} + return {"type": "http.request", "body": body, "more_body": True} + + async def send(message: typing.Dict[str, typing.Any]) -> None: + nonlocal status_code, response_headers, response_started + + if message["type"] == "http.response.start": + assert not response_started + + status_code = message["status"] + response_headers = message.get("headers", []) + response_started = True + + elif message["type"] == "http.response.body": + assert not response_complete.is_set() + body = message.get("body", b"") + more_body = message.get("more_body", False) + + if body and request.method != "HEAD": + body_parts.append(body) + + if not more_body: + response_complete.set() + + try: + await self.app(scope, receive, send) + except Exception: # noqa: PIE-786 + if self.raise_app_exceptions or not response_complete.is_set(): + raise + + assert response_complete.is_set() + assert status_code is not None + assert response_headers is not None + + stream = ASGIResponseStream(body_parts) + + return Response(status_code, headers=response_headers, stream=stream) diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_transports/base.py b/Backend/venv/lib/python3.12/site-packages/httpx/_transports/base.py new file mode 100644 index 00000000..f6fdfe69 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_transports/base.py @@ -0,0 +1,82 @@ +import typing +from types import TracebackType + +from .._models import Request, Response + +T = typing.TypeVar("T", bound="BaseTransport") +A = typing.TypeVar("A", bound="AsyncBaseTransport") + + +class BaseTransport: + def __enter__(self: T) -> T: + return self + + def __exit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]] = None, + exc_value: typing.Optional[BaseException] = None, + traceback: typing.Optional[TracebackType] = None, + ) -> None: + self.close() + + def handle_request(self, request: Request) -> Response: + """ + Send a single HTTP request and return a response. + + Developers shouldn't typically ever need to call into this API directly, + since the Client class provides all the higher level user-facing API + niceties. + + In order to properly release any network resources, the response + stream should *either* be consumed immediately, with a call to + `response.stream.read()`, or else the `handle_request` call should + be followed with a try/finally block to ensuring the stream is + always closed. + + Example usage: + + with httpx.HTTPTransport() as transport: + req = httpx.Request( + method=b"GET", + url=(b"https", b"www.example.com", 443, b"/"), + headers=[(b"Host", b"www.example.com")], + ) + resp = transport.handle_request(req) + body = resp.stream.read() + print(resp.status_code, resp.headers, body) + + + Takes a `Request` instance as the only argument. + + Returns a `Response` instance. + """ + raise NotImplementedError( + "The 'handle_request' method must be implemented." + ) # pragma: no cover + + def close(self) -> None: + pass + + +class AsyncBaseTransport: + async def __aenter__(self: A) -> A: + return self + + async def __aexit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]] = None, + exc_value: typing.Optional[BaseException] = None, + traceback: typing.Optional[TracebackType] = None, + ) -> None: + await self.aclose() + + async def handle_async_request( + self, + request: Request, + ) -> Response: + raise NotImplementedError( + "The 'handle_async_request' method must be implemented." + ) # pragma: no cover + + async def aclose(self) -> None: + pass diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_transports/default.py b/Backend/venv/lib/python3.12/site-packages/httpx/_transports/default.py new file mode 100644 index 00000000..fca7de98 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_transports/default.py @@ -0,0 +1,365 @@ +""" +Custom transports, with nicely configured defaults. + +The following additional keyword arguments are currently supported by httpcore... + +* uds: str +* local_address: str +* retries: int + +Example usages... + +# Disable HTTP/2 on a single specific domain. +mounts = { + "all://": httpx.HTTPTransport(http2=True), + "all://*example.org": httpx.HTTPTransport() +} + +# Using advanced httpcore configuration, with connection retries. +transport = httpx.HTTPTransport(retries=1) +client = httpx.Client(transport=transport) + +# Using advanced httpcore configuration, with unix domain sockets. +transport = httpx.HTTPTransport(uds="socket.uds") +client = httpx.Client(transport=transport) +""" +import contextlib +import typing +from types import TracebackType + +import httpcore + +from .._config import DEFAULT_LIMITS, Limits, Proxy, create_ssl_context +from .._exceptions import ( + ConnectError, + ConnectTimeout, + LocalProtocolError, + NetworkError, + PoolTimeout, + ProtocolError, + ProxyError, + ReadError, + ReadTimeout, + RemoteProtocolError, + TimeoutException, + UnsupportedProtocol, + WriteError, + WriteTimeout, +) +from .._models import Request, Response +from .._types import AsyncByteStream, CertTypes, SyncByteStream, VerifyTypes +from .base import AsyncBaseTransport, BaseTransport + +T = typing.TypeVar("T", bound="HTTPTransport") +A = typing.TypeVar("A", bound="AsyncHTTPTransport") + + +@contextlib.contextmanager +def map_httpcore_exceptions() -> typing.Iterator[None]: + try: + yield + except Exception as exc: # noqa: PIE-786 + mapped_exc = None + + for from_exc, to_exc in HTTPCORE_EXC_MAP.items(): + if not isinstance(exc, from_exc): + continue + # We want to map to the most specific exception we can find. + # Eg if `exc` is an `httpcore.ReadTimeout`, we want to map to + # `httpx.ReadTimeout`, not just `httpx.TimeoutException`. + if mapped_exc is None or issubclass(to_exc, mapped_exc): + mapped_exc = to_exc + + if mapped_exc is None: # pragma: no cover + raise + + message = str(exc) + raise mapped_exc(message) from exc + + +HTTPCORE_EXC_MAP = { + httpcore.TimeoutException: TimeoutException, + httpcore.ConnectTimeout: ConnectTimeout, + httpcore.ReadTimeout: ReadTimeout, + httpcore.WriteTimeout: WriteTimeout, + httpcore.PoolTimeout: PoolTimeout, + httpcore.NetworkError: NetworkError, + httpcore.ConnectError: ConnectError, + httpcore.ReadError: ReadError, + httpcore.WriteError: WriteError, + httpcore.ProxyError: ProxyError, + httpcore.UnsupportedProtocol: UnsupportedProtocol, + httpcore.ProtocolError: ProtocolError, + httpcore.LocalProtocolError: LocalProtocolError, + httpcore.RemoteProtocolError: RemoteProtocolError, +} + + +class ResponseStream(SyncByteStream): + def __init__(self, httpcore_stream: typing.Iterable[bytes]): + self._httpcore_stream = httpcore_stream + + def __iter__(self) -> typing.Iterator[bytes]: + with map_httpcore_exceptions(): + for part in self._httpcore_stream: + yield part + + def close(self) -> None: + if hasattr(self._httpcore_stream, "close"): + self._httpcore_stream.close() + + +class HTTPTransport(BaseTransport): + def __init__( + self, + verify: VerifyTypes = True, + cert: typing.Optional[CertTypes] = None, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + trust_env: bool = True, + proxy: typing.Optional[Proxy] = None, + uds: typing.Optional[str] = None, + local_address: typing.Optional[str] = None, + retries: int = 0, + ) -> None: + ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env) + + if proxy is None: + self._pool = httpcore.ConnectionPool( + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + uds=uds, + local_address=local_address, + retries=retries, + ) + elif proxy.url.scheme in ("http", "https"): + self._pool = httpcore.HTTPProxy( + proxy_url=httpcore.URL( + scheme=proxy.url.raw_scheme, + host=proxy.url.raw_host, + port=proxy.url.port, + target=proxy.url.raw_path, + ), + proxy_auth=proxy.raw_auth, + proxy_headers=proxy.headers.raw, + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + ) + elif proxy.url.scheme == "socks5": + try: + import socksio # noqa + except ImportError: # pragma: no cover + raise ImportError( + "Using SOCKS proxy, but the 'socksio' package is not installed. " + "Make sure to install httpx using `pip install httpx[socks]`." + ) from None + + self._pool = httpcore.SOCKSProxy( + proxy_url=httpcore.URL( + scheme=proxy.url.raw_scheme, + host=proxy.url.raw_host, + port=proxy.url.port, + target=proxy.url.raw_path, + ), + proxy_auth=proxy.raw_auth, + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + ) + else: # pragma: no cover + raise ValueError( + f"Proxy protocol must be either 'http', 'https', or 'socks5', but got {proxy.url.scheme!r}." + ) + + def __enter__(self: T) -> T: # Use generics for subclass support. + self._pool.__enter__() + return self + + def __exit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]] = None, + exc_value: typing.Optional[BaseException] = None, + traceback: typing.Optional[TracebackType] = None, + ) -> None: + with map_httpcore_exceptions(): + self._pool.__exit__(exc_type, exc_value, traceback) + + def handle_request( + self, + request: Request, + ) -> Response: + assert isinstance(request.stream, SyncByteStream) + + req = httpcore.Request( + method=request.method, + url=httpcore.URL( + scheme=request.url.raw_scheme, + host=request.url.raw_host, + port=request.url.port, + target=request.url.raw_path, + ), + headers=request.headers.raw, + content=request.stream, + extensions=request.extensions, + ) + with map_httpcore_exceptions(): + resp = self._pool.handle_request(req) + + assert isinstance(resp.stream, typing.Iterable) + + return Response( + status_code=resp.status, + headers=resp.headers, + stream=ResponseStream(resp.stream), + extensions=resp.extensions, + ) + + def close(self) -> None: + self._pool.close() + + +class AsyncResponseStream(AsyncByteStream): + def __init__(self, httpcore_stream: typing.AsyncIterable[bytes]): + self._httpcore_stream = httpcore_stream + + async def __aiter__(self) -> typing.AsyncIterator[bytes]: + with map_httpcore_exceptions(): + async for part in self._httpcore_stream: + yield part + + async def aclose(self) -> None: + if hasattr(self._httpcore_stream, "aclose"): + await self._httpcore_stream.aclose() + + +class AsyncHTTPTransport(AsyncBaseTransport): + def __init__( + self, + verify: VerifyTypes = True, + cert: typing.Optional[CertTypes] = None, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + trust_env: bool = True, + proxy: typing.Optional[Proxy] = None, + uds: typing.Optional[str] = None, + local_address: typing.Optional[str] = None, + retries: int = 0, + ) -> None: + ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env) + + if proxy is None: + self._pool = httpcore.AsyncConnectionPool( + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + uds=uds, + local_address=local_address, + retries=retries, + ) + elif proxy.url.scheme in ("http", "https"): + self._pool = httpcore.AsyncHTTPProxy( + proxy_url=httpcore.URL( + scheme=proxy.url.raw_scheme, + host=proxy.url.raw_host, + port=proxy.url.port, + target=proxy.url.raw_path, + ), + proxy_auth=proxy.raw_auth, + proxy_headers=proxy.headers.raw, + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + ) + elif proxy.url.scheme == "socks5": + try: + import socksio # noqa + except ImportError: # pragma: no cover + raise ImportError( + "Using SOCKS proxy, but the 'socksio' package is not installed. " + "Make sure to install httpx using `pip install httpx[socks]`." + ) from None + + self._pool = httpcore.AsyncSOCKSProxy( + proxy_url=httpcore.URL( + scheme=proxy.url.raw_scheme, + host=proxy.url.raw_host, + port=proxy.url.port, + target=proxy.url.raw_path, + ), + proxy_auth=proxy.raw_auth, + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + ) + else: # pragma: no cover + raise ValueError( + f"Proxy protocol must be either 'http', 'https', or 'socks5', but got {proxy.url.scheme!r}." + ) + + async def __aenter__(self: A) -> A: # Use generics for subclass support. + await self._pool.__aenter__() + return self + + async def __aexit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]] = None, + exc_value: typing.Optional[BaseException] = None, + traceback: typing.Optional[TracebackType] = None, + ) -> None: + with map_httpcore_exceptions(): + await self._pool.__aexit__(exc_type, exc_value, traceback) + + async def handle_async_request( + self, + request: Request, + ) -> Response: + assert isinstance(request.stream, AsyncByteStream) + + req = httpcore.Request( + method=request.method, + url=httpcore.URL( + scheme=request.url.raw_scheme, + host=request.url.raw_host, + port=request.url.port, + target=request.url.raw_path, + ), + headers=request.headers.raw, + content=request.stream, + extensions=request.extensions, + ) + with map_httpcore_exceptions(): + resp = await self._pool.handle_async_request(req) + + assert isinstance(resp.stream, typing.AsyncIterable) + + return Response( + status_code=resp.status, + headers=resp.headers, + stream=AsyncResponseStream(resp.stream), + extensions=resp.extensions, + ) + + async def aclose(self) -> None: + await self._pool.aclose() diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_transports/mock.py b/Backend/venv/lib/python3.12/site-packages/httpx/_transports/mock.py new file mode 100644 index 00000000..82043da2 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_transports/mock.py @@ -0,0 +1,38 @@ +import typing + +from .._models import Request, Response +from .base import AsyncBaseTransport, BaseTransport + +SyncHandler = typing.Callable[[Request], Response] +AsyncHandler = typing.Callable[[Request], typing.Coroutine[None, None, Response]] + + +class MockTransport(AsyncBaseTransport, BaseTransport): + def __init__(self, handler: typing.Union[SyncHandler, AsyncHandler]) -> None: + self.handler = handler + + def handle_request( + self, + request: Request, + ) -> Response: + request.read() + response = self.handler(request) + if not isinstance(response, Response): # pragma: no cover + raise TypeError("Cannot use an async handler in a sync Client") + return response + + async def handle_async_request( + self, + request: Request, + ) -> Response: + await request.aread() + response = self.handler(request) + + # Allow handler to *optionally* be an `async` function. + # If it is, then the `response` variable need to be awaited to actually + # return the result. + + if not isinstance(response, Response): + response = await response + + return response diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_transports/wsgi.py b/Backend/venv/lib/python3.12/site-packages/httpx/_transports/wsgi.py new file mode 100644 index 00000000..33035ce5 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_transports/wsgi.py @@ -0,0 +1,143 @@ +import io +import itertools +import sys +import typing + +from .._models import Request, Response +from .._types import SyncByteStream +from .base import BaseTransport + +if typing.TYPE_CHECKING: + from _typeshed import OptExcInfo # pragma: no cover + from _typeshed.wsgi import WSGIApplication # pragma: no cover + +_T = typing.TypeVar("_T") + + +def _skip_leading_empty_chunks(body: typing.Iterable[_T]) -> typing.Iterable[_T]: + body = iter(body) + for chunk in body: + if chunk: + return itertools.chain([chunk], body) + return [] + + +class WSGIByteStream(SyncByteStream): + def __init__(self, result: typing.Iterable[bytes]) -> None: + self._close = getattr(result, "close", None) + self._result = _skip_leading_empty_chunks(result) + + def __iter__(self) -> typing.Iterator[bytes]: + for part in self._result: + yield part + + def close(self) -> None: + if self._close is not None: + self._close() + + +class WSGITransport(BaseTransport): + """ + A custom transport that handles sending requests directly to an WSGI app. + The simplest way to use this functionality is to use the `app` argument. + + ``` + client = httpx.Client(app=app) + ``` + + Alternatively, you can setup the transport instance explicitly. + This allows you to include any additional configuration arguments specific + to the WSGITransport class: + + ``` + transport = httpx.WSGITransport( + app=app, + script_name="/submount", + remote_addr="1.2.3.4" + ) + client = httpx.Client(transport=transport) + ``` + + Arguments: + + * `app` - The WSGI application. + * `raise_app_exceptions` - Boolean indicating if exceptions in the application + should be raised. Default to `True`. Can be set to `False` for use cases + such as testing the content of a client 500 response. + * `script_name` - The root path on which the WSGI application should be mounted. + * `remote_addr` - A string indicating the client IP of incoming requests. + ``` + """ + + def __init__( + self, + app: "WSGIApplication", + raise_app_exceptions: bool = True, + script_name: str = "", + remote_addr: str = "127.0.0.1", + wsgi_errors: typing.Optional[typing.TextIO] = None, + ) -> None: + self.app = app + self.raise_app_exceptions = raise_app_exceptions + self.script_name = script_name + self.remote_addr = remote_addr + self.wsgi_errors = wsgi_errors + + def handle_request(self, request: Request) -> Response: + request.read() + wsgi_input = io.BytesIO(request.content) + + port = request.url.port or {"http": 80, "https": 443}[request.url.scheme] + environ = { + "wsgi.version": (1, 0), + "wsgi.url_scheme": request.url.scheme, + "wsgi.input": wsgi_input, + "wsgi.errors": self.wsgi_errors or sys.stderr, + "wsgi.multithread": True, + "wsgi.multiprocess": False, + "wsgi.run_once": False, + "REQUEST_METHOD": request.method, + "SCRIPT_NAME": self.script_name, + "PATH_INFO": request.url.path, + "QUERY_STRING": request.url.query.decode("ascii"), + "SERVER_NAME": request.url.host, + "SERVER_PORT": str(port), + "REMOTE_ADDR": self.remote_addr, + } + for header_key, header_value in request.headers.raw: + key = header_key.decode("ascii").upper().replace("-", "_") + if key not in ("CONTENT_TYPE", "CONTENT_LENGTH"): + key = "HTTP_" + key + environ[key] = header_value.decode("ascii") + + seen_status = None + seen_response_headers = None + seen_exc_info = None + + def start_response( + status: str, + response_headers: typing.List[typing.Tuple[str, str]], + exc_info: typing.Optional["OptExcInfo"] = None, + ) -> typing.Callable[[bytes], typing.Any]: + nonlocal seen_status, seen_response_headers, seen_exc_info + seen_status = status + seen_response_headers = response_headers + seen_exc_info = exc_info + return lambda _: None + + result = self.app(environ, start_response) + + stream = WSGIByteStream(result) + + assert seen_status is not None + assert seen_response_headers is not None + if seen_exc_info and seen_exc_info[0] and self.raise_app_exceptions: + raise seen_exc_info[1] + + status_code = int(seen_status.split()[0]) + headers = [ + (key.encode("ascii"), value.encode("ascii")) + for key, value in seen_response_headers + ] + + return Response(status_code, headers=headers, stream=stream) diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_types.py b/Backend/venv/lib/python3.12/site-packages/httpx/_types.py new file mode 100644 index 00000000..6b610e14 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_types.py @@ -0,0 +1,132 @@ +""" +Type definitions for type checking purposes. +""" + +import ssl +from http.cookiejar import CookieJar +from typing import ( + IO, + TYPE_CHECKING, + Any, + AsyncIterable, + AsyncIterator, + Callable, + Dict, + Iterable, + Iterator, + List, + Mapping, + NamedTuple, + Optional, + Sequence, + Tuple, + Union, +) + +if TYPE_CHECKING: # pragma: no cover + from ._auth import Auth # noqa: F401 + from ._config import Proxy, Timeout # noqa: F401 + from ._models import Cookies, Headers, Request # noqa: F401 + from ._urls import URL, QueryParams # noqa: F401 + + +PrimitiveData = Optional[Union[str, int, float, bool]] + +RawURL = NamedTuple( + "RawURL", + [ + ("raw_scheme", bytes), + ("raw_host", bytes), + ("port", Optional[int]), + ("raw_path", bytes), + ], +) + +URLTypes = Union["URL", str] + +QueryParamTypes = Union[ + "QueryParams", + Mapping[str, Union[PrimitiveData, Sequence[PrimitiveData]]], + List[Tuple[str, PrimitiveData]], + Tuple[Tuple[str, PrimitiveData], ...], + str, + bytes, +] + +HeaderTypes = Union[ + "Headers", + Mapping[str, str], + Mapping[bytes, bytes], + Sequence[Tuple[str, str]], + Sequence[Tuple[bytes, bytes]], +] + +CookieTypes = Union["Cookies", CookieJar, Dict[str, str], List[Tuple[str, str]]] + +CertTypes = Union[ + # certfile + str, + # (certfile, keyfile) + Tuple[str, Optional[str]], + # (certfile, keyfile, password) + Tuple[str, Optional[str], Optional[str]], +] +VerifyTypes = Union[str, bool, ssl.SSLContext] +TimeoutTypes = Union[ + Optional[float], + Tuple[Optional[float], Optional[float], Optional[float], Optional[float]], + "Timeout", +] +ProxiesTypes = Union[URLTypes, "Proxy", Dict[URLTypes, Union[None, URLTypes, "Proxy"]]] + +AuthTypes = Union[ + Tuple[Union[str, bytes], Union[str, bytes]], + Callable[["Request"], "Request"], + "Auth", +] + +RequestContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]] +ResponseContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]] +ResponseExtensions = Mapping[str, Any] + +RequestData = Mapping[str, Any] + +FileContent = Union[IO[bytes], bytes, str] +FileTypes = Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], FileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], +] +RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]] + +RequestExtensions = Mapping[str, Any] + + +class SyncByteStream: + def __iter__(self) -> Iterator[bytes]: + raise NotImplementedError( + "The '__iter__' method must be implemented." + ) # pragma: no cover + yield b"" # pragma: no cover + + def close(self) -> None: + """ + Subclasses can override this method to release any network resources + after a request/response cycle is complete. + """ + + +class AsyncByteStream: + async def __aiter__(self) -> AsyncIterator[bytes]: + raise NotImplementedError( + "The '__aiter__' method must be implemented." + ) # pragma: no cover + yield b"" # pragma: no cover + + async def aclose(self) -> None: + pass diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_urlparse.py b/Backend/venv/lib/python3.12/site-packages/httpx/_urlparse.py new file mode 100644 index 00000000..69ff0b4b --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_urlparse.py @@ -0,0 +1,462 @@ +""" +An implementation of `urlparse` that provides URL validation and normalization +as described by RFC3986. + +We rely on this implementation rather than the one in Python's stdlib, because: + +* It provides more complete URL validation. +* It properly differentiates between an empty querystring and an absent querystring, + to distinguish URLs with a trailing '?'. +* It handles scheme, hostname, port, and path normalization. +* It supports IDNA hostnames, normalizing them to their encoded form. +* The API supports passing individual components, as well as the complete URL string. + +Previously we relied on the excellent `rfc3986` package to handle URL parsing and +validation, but this module provides a simpler alternative, with less indirection +required. +""" +import ipaddress +import re +import typing + +import idna + +from ._exceptions import InvalidURL + +MAX_URL_LENGTH = 65536 + +# https://datatracker.ietf.org/doc/html/rfc3986.html#section-2.3 +UNRESERVED_CHARACTERS = ( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" +) +SUB_DELIMS = "!$&'()*+,;=" + +PERCENT_ENCODED_REGEX = re.compile("%[A-Fa-f0-9]{2}") + + +# {scheme}: (optional) +# //{authority} (optional) +# {path} +# ?{query} (optional) +# #{fragment} (optional) +URL_REGEX = re.compile( + ( + r"(?:(?P{scheme}):)?" + r"(?://(?P{authority}))?" + r"(?P{path})" + r"(?:\?(?P{query}))?" + r"(?:#(?P{fragment}))?" + ).format( + scheme="([a-zA-Z][a-zA-Z0-9+.-]*)?", + authority="[^/?#]*", + path="[^?#]*", + query="[^#]*", + fragment=".*", + ) +) + +# {userinfo}@ (optional) +# {host} +# :{port} (optional) +AUTHORITY_REGEX = re.compile( + ( + r"(?:(?P{userinfo})@)?" r"(?P{host})" r":?(?P{port})?" + ).format( + userinfo="[^@]*", # Any character sequence not including '@'. + host="(\\[.*\\]|[^:]*)", # Either any character sequence not including ':', + # or an IPv6 address enclosed within square brackets. + port=".*", # Any character sequence. + ) +) + + +# If we call urlparse with an individual component, then we need to regex +# validate that component individually. +# Note that we're duplicating the same strings as above. Shock! Horror!! +COMPONENT_REGEX = { + "scheme": re.compile("([a-zA-Z][a-zA-Z0-9+.-]*)?"), + "authority": re.compile("[^/?#]*"), + "path": re.compile("[^?#]*"), + "query": re.compile("[^#]*"), + "fragment": re.compile(".*"), + "userinfo": re.compile("[^@]*"), + "host": re.compile("(\\[.*\\]|[^:]*)"), + "port": re.compile(".*"), +} + + +# We use these simple regexs as a first pass before handing off to +# the stdlib 'ipaddress' module for IP address validation. +IPv4_STYLE_HOSTNAME = re.compile(r"^[0-9]+.[0-9]+.[0-9]+.[0-9]+$") +IPv6_STYLE_HOSTNAME = re.compile(r"^\[.*\]$") + + +class ParseResult(typing.NamedTuple): + scheme: str + userinfo: str + host: str + port: typing.Optional[int] + path: str + query: typing.Optional[str] + fragment: typing.Optional[str] + + @property + def authority(self) -> str: + return "".join( + [ + f"{self.userinfo}@" if self.userinfo else "", + f"[{self.host}]" if ":" in self.host else self.host, + f":{self.port}" if self.port is not None else "", + ] + ) + + @property + def netloc(self) -> str: + return "".join( + [ + f"[{self.host}]" if ":" in self.host else self.host, + f":{self.port}" if self.port is not None else "", + ] + ) + + def copy_with(self, **kwargs: typing.Optional[str]) -> "ParseResult": + if not kwargs: + return self + + defaults = { + "scheme": self.scheme, + "authority": self.authority, + "path": self.path, + "query": self.query, + "fragment": self.fragment, + } + defaults.update(kwargs) + return urlparse("", **defaults) + + def __str__(self) -> str: + authority = self.authority + return "".join( + [ + f"{self.scheme}:" if self.scheme else "", + f"//{authority}" if authority else "", + self.path, + f"?{self.query}" if self.query is not None else "", + f"#{self.fragment}" if self.fragment is not None else "", + ] + ) + + +def urlparse(url: str = "", **kwargs: typing.Optional[str]) -> ParseResult: + # Initial basic checks on allowable URLs. + # --------------------------------------- + + # Hard limit the maximum allowable URL length. + if len(url) > MAX_URL_LENGTH: + raise InvalidURL("URL too long") + + # If a URL includes any ASCII control characters including \t, \r, \n, + # then treat it as invalid. + if any(char.isascii() and not char.isprintable() for char in url): + raise InvalidURL("Invalid non-printable ASCII character in URL") + + # Some keyword arguments require special handling. + # ------------------------------------------------ + + # Coerce "port" to a string, if it is provided as an integer. + if "port" in kwargs: + port = kwargs["port"] + kwargs["port"] = str(port) if isinstance(port, int) else port + + # Replace "netloc" with "host and "port". + if "netloc" in kwargs: + netloc = kwargs.pop("netloc") or "" + kwargs["host"], _, kwargs["port"] = netloc.partition(":") + + # Replace "username" and/or "password" with "userinfo". + if "username" in kwargs or "password" in kwargs: + username = quote(kwargs.pop("username", "") or "") + password = quote(kwargs.pop("password", "") or "") + kwargs["userinfo"] = f"{username}:{password}" if password else username + + # Replace "raw_path" with "path" and "query". + if "raw_path" in kwargs: + raw_path = kwargs.pop("raw_path") or "" + kwargs["path"], seperator, kwargs["query"] = raw_path.partition("?") + if not seperator: + kwargs["query"] = None + + # Ensure that IPv6 "host" addresses are always escaped with "[...]". + if "host" in kwargs: + host = kwargs.get("host") or "" + if ":" in host and not (host.startswith("[") and host.endswith("]")): + kwargs["host"] = f"[{host}]" + + # If any keyword arguments are provided, ensure they are valid. + # ------------------------------------------------------------- + + for key, value in kwargs.items(): + if value is not None: + if len(value) > MAX_URL_LENGTH: + raise InvalidURL(f"URL component '{key}' too long") + + # If a component includes any ASCII control characters including \t, \r, \n, + # then treat it as invalid. + if any(char.isascii() and not char.isprintable() for char in value): + raise InvalidURL( + f"Invalid non-printable ASCII character in URL component '{key}'" + ) + + # Ensure that keyword arguments match as a valid regex. + if not COMPONENT_REGEX[key].fullmatch(value): + raise InvalidURL(f"Invalid URL component '{key}'") + + # The URL_REGEX will always match, but may have empty components. + url_match = URL_REGEX.match(url) + assert url_match is not None + url_dict = url_match.groupdict() + + # * 'scheme', 'authority', and 'path' may be empty strings. + # * 'query' may be 'None', indicating no trailing "?" portion. + # Any string including the empty string, indicates a trailing "?". + # * 'fragment' may be 'None', indicating no trailing "#" portion. + # Any string including the empty string, indicates a trailing "#". + scheme = kwargs.get("scheme", url_dict["scheme"]) or "" + authority = kwargs.get("authority", url_dict["authority"]) or "" + path = kwargs.get("path", url_dict["path"]) or "" + query = kwargs.get("query", url_dict["query"]) + fragment = kwargs.get("fragment", url_dict["fragment"]) + + # The AUTHORITY_REGEX will always match, but may have empty components. + authority_match = AUTHORITY_REGEX.match(authority) + assert authority_match is not None + authority_dict = authority_match.groupdict() + + # * 'userinfo' and 'host' may be empty strings. + # * 'port' may be 'None'. + userinfo = kwargs.get("userinfo", authority_dict["userinfo"]) or "" + host = kwargs.get("host", authority_dict["host"]) or "" + port = kwargs.get("port", authority_dict["port"]) + + # Normalize and validate each component. + # We end up with a parsed representation of the URL, + # with components that are plain ASCII bytestrings. + parsed_scheme: str = scheme.lower() + parsed_userinfo: str = quote(userinfo, safe=SUB_DELIMS + ":") + parsed_host: str = encode_host(host) + parsed_port: typing.Optional[int] = normalize_port(port, scheme) + + has_scheme = parsed_scheme != "" + has_authority = ( + parsed_userinfo != "" or parsed_host != "" or parsed_port is not None + ) + validate_path(path, has_scheme=has_scheme, has_authority=has_authority) + if has_authority: + path = normalize_path(path) + + # The GEN_DELIMS set is... : / ? # [ ] @ + # These do not need to be percent-quoted unless they serve as delimiters for the + # specific component. + + # For 'path' we need to drop ? and # from the GEN_DELIMS set. + parsed_path: str = quote(path, safe=SUB_DELIMS + ":/[]@") + # For 'query' we need to drop '#' from the GEN_DELIMS set. + parsed_query: typing.Optional[str] = ( + None if query is None else quote(query, safe=SUB_DELIMS + ":/?[]@") + ) + # For 'fragment' we can include all of the GEN_DELIMS set. + parsed_fragment: typing.Optional[str] = ( + None if fragment is None else quote(fragment, safe=SUB_DELIMS + ":/?#[]@") + ) + + # The parsed ASCII bytestrings are our canonical form. + # All properties of the URL are derived from these. + return ParseResult( + parsed_scheme, + parsed_userinfo, + parsed_host, + parsed_port, + parsed_path, + parsed_query, + parsed_fragment, + ) + + +def encode_host(host: str) -> str: + if not host: + return "" + + elif IPv4_STYLE_HOSTNAME.match(host): + # Validate IPv4 hostnames like #.#.#.# + # + # From https://datatracker.ietf.org/doc/html/rfc3986/#section-3.2.2 + # + # IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet + try: + ipaddress.IPv4Address(host) + except ipaddress.AddressValueError: + raise InvalidURL(f"Invalid IPv4 address: {host!r}") + return host + + elif IPv6_STYLE_HOSTNAME.match(host): + # Validate IPv6 hostnames like [...] + # + # From https://datatracker.ietf.org/doc/html/rfc3986/#section-3.2.2 + # + # "A host identified by an Internet Protocol literal address, version 6 + # [RFC3513] or later, is distinguished by enclosing the IP literal + # within square brackets ("[" and "]"). This is the only place where + # square bracket characters are allowed in the URI syntax." + try: + ipaddress.IPv6Address(host[1:-1]) + except ipaddress.AddressValueError: + raise InvalidURL(f"Invalid IPv6 address: {host!r}") + return host[1:-1] + + elif host.isascii(): + # Regular ASCII hostnames + # + # From https://datatracker.ietf.org/doc/html/rfc3986/#section-3.2.2 + # + # reg-name = *( unreserved / pct-encoded / sub-delims ) + return quote(host.lower(), safe=SUB_DELIMS) + + # IDNA hostnames + try: + return idna.encode(host.lower()).decode("ascii") + except idna.IDNAError: + raise InvalidURL(f"Invalid IDNA hostname: {host!r}") + + +def normalize_port( + port: typing.Optional[typing.Union[str, int]], scheme: str +) -> typing.Optional[int]: + # From https://tools.ietf.org/html/rfc3986#section-3.2.3 + # + # "A scheme may define a default port. For example, the "http" scheme + # defines a default port of "80", corresponding to its reserved TCP + # port number. The type of port designated by the port number (e.g., + # TCP, UDP, SCTP) is defined by the URI scheme. URI producers and + # normalizers should omit the port component and its ":" delimiter if + # port is empty or if its value would be the same as that of the + # scheme's default." + if port is None or port == "": + return None + + try: + port_as_int = int(port) + except ValueError: + raise InvalidURL(f"Invalid port: {port!r}") + + # See https://url.spec.whatwg.org/#url-miscellaneous + default_port = {"ftp": 21, "http": 80, "https": 443, "ws": 80, "wss": 443}.get( + scheme + ) + if port_as_int == default_port: + return None + return port_as_int + + +def validate_path(path: str, has_scheme: bool, has_authority: bool) -> None: + """ + Path validation rules that depend on if the URL contains a scheme or authority component. + + See https://datatracker.ietf.org/doc/html/rfc3986.html#section-3.3 + """ + if has_authority: + # > If a URI contains an authority component, then the path component + # > must either be empty or begin with a slash ("/") character." + if path and not path.startswith("/"): + raise InvalidURL("For absolute URLs, path must be empty or begin with '/'") + else: + # > If a URI does not contain an authority component, then the path cannot begin + # > with two slash characters ("//"). + if path.startswith("//"): + raise InvalidURL( + "URLs with no authority component cannot have a path starting with '//'" + ) + # > In addition, a URI reference (Section 4.1) may be a relative-path reference, in which + # > case the first path segment cannot contain a colon (":") character. + if path.startswith(":") and not has_scheme: + raise InvalidURL( + "URLs with no scheme component cannot have a path starting with ':'" + ) + + +def normalize_path(path: str) -> str: + """ + Drop "." and ".." segments from a URL path. + + For example: + + normalize_path("/path/./to/somewhere/..") == "/path/to" + """ + # https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4 + components = path.split("/") + output: typing.List[str] = [] + for component in components: + if component == ".": + pass + elif component == "..": + if output and output != [""]: + output.pop() + else: + output.append(component) + return "/".join(output) + + +def percent_encode(char: str) -> str: + """ + Replace a single character with the percent-encoded representation. + + Characters outside the ASCII range are represented with their a percent-encoded + representation of their UTF-8 byte sequence. + + For example: + + percent_encode(" ") == "%20" + """ + return "".join([f"%{byte:02x}" for byte in char.encode("utf-8")]).upper() + + +def is_safe(string: str, safe: str = "/") -> bool: + """ + Determine if a given string is already quote-safe. + """ + NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe + "%" + + # All characters must already be non-escaping or '%' + for char in string: + if char not in NON_ESCAPED_CHARS: + return False + + # Any '%' characters must be valid '%xx' escape sequences. + return string.count("%") == len(PERCENT_ENCODED_REGEX.findall(string)) + + +def quote(string: str, safe: str = "/") -> str: + """ + Use percent-encoding to quote a string if required. + """ + if is_safe(string, safe=safe): + return string + + NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe + return "".join( + [char if char in NON_ESCAPED_CHARS else percent_encode(char) for char in string] + ) + + +def urlencode(items: typing.List[typing.Tuple[str, str]]) -> str: + # We can use a much simpler version of the stdlib urlencode here because + # we don't need to handle a bunch of different typing cases, such as bytes vs str. + # + # https://github.com/python/cpython/blob/b2f7b2ef0b5421e01efb8c7bee2ef95d3bab77eb/Lib/urllib/parse.py#L926 + # + # Note that we use '%20' encoding for spaces, and treat '/' as a safe + # character. This means our query params have the same escaping as other + # characters in the URL path. This is slightly different to `requests`, + # but is the behaviour that browsers use. + # + # See https://github.com/encode/httpx/issues/2536 and + # https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlencode + return "&".join([quote(k) + "=" + quote(v) for k, v in items]) diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_urls.py b/Backend/venv/lib/python3.12/site-packages/httpx/_urls.py new file mode 100644 index 00000000..b023941b --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_urls.py @@ -0,0 +1,642 @@ +import typing +from urllib.parse import parse_qs, unquote + +import idna + +from ._types import QueryParamTypes, RawURL, URLTypes +from ._urlparse import urlencode, urlparse +from ._utils import primitive_value_to_str + + +class URL: + """ + url = httpx.URL("HTTPS://jo%40email.com:a%20secret@müller.de:1234/pa%20th?search=ab#anchorlink") + + assert url.scheme == "https" + assert url.username == "jo@email.com" + assert url.password == "a secret" + assert url.userinfo == b"jo%40email.com:a%20secret" + assert url.host == "müller.de" + assert url.raw_host == b"xn--mller-kva.de" + assert url.port == 1234 + assert url.netloc == b"xn--mller-kva.de:1234" + assert url.path == "/pa th" + assert url.query == b"?search=ab" + assert url.raw_path == b"/pa%20th?search=ab" + assert url.fragment == "anchorlink" + + The components of a URL are broken down like this: + + https://jo%40email.com:a%20secret@müller.de:1234/pa%20th?search=ab#anchorlink + [scheme] [ username ] [password] [ host ][port][ path ] [ query ] [fragment] + [ userinfo ] [ netloc ][ raw_path ] + + Note that: + + * `url.scheme` is normalized to always be lowercased. + + * `url.host` is normalized to always be lowercased. Internationalized domain + names are represented in unicode, without IDNA encoding applied. For instance: + + url = httpx.URL("http://中国.icom.museum") + assert url.host == "中国.icom.museum" + url = httpx.URL("http://xn--fiqs8s.icom.museum") + assert url.host == "中国.icom.museum" + + * `url.raw_host` is normalized to always be lowercased, and is IDNA encoded. + + url = httpx.URL("http://中国.icom.museum") + assert url.raw_host == b"xn--fiqs8s.icom.museum" + url = httpx.URL("http://xn--fiqs8s.icom.museum") + assert url.raw_host == b"xn--fiqs8s.icom.museum" + + * `url.port` is either None or an integer. URLs that include the default port for + "http", "https", "ws", "wss", and "ftp" schemes have their port normalized to `None`. + + assert httpx.URL("http://example.com") == httpx.URL("http://example.com:80") + assert httpx.URL("http://example.com").port is None + assert httpx.URL("http://example.com:80").port is None + + * `url.userinfo` is raw bytes, without URL escaping. Usually you'll want to work with + `url.username` and `url.password` instead, which handle the URL escaping. + + * `url.raw_path` is raw bytes of both the path and query, without URL escaping. + This portion is used as the target when constructing HTTP requests. Usually you'll + want to work with `url.path` instead. + + * `url.query` is raw bytes, without URL escaping. A URL query string portion can only + be properly URL escaped when decoding the parameter names and values themselves. + """ + + def __init__( + self, url: typing.Union["URL", str] = "", **kwargs: typing.Any + ) -> None: + if kwargs: + allowed = { + "scheme": str, + "username": str, + "password": str, + "userinfo": bytes, + "host": str, + "port": int, + "netloc": bytes, + "path": str, + "query": bytes, + "raw_path": bytes, + "fragment": str, + "params": object, + } + + # Perform type checking for all supported keyword arguments. + for key, value in kwargs.items(): + if key not in allowed: + message = f"{key!r} is an invalid keyword argument for URL()" + raise TypeError(message) + if value is not None and not isinstance(value, allowed[key]): + expected = allowed[key].__name__ + seen = type(value).__name__ + message = f"Argument {key!r} must be {expected} but got {seen}" + raise TypeError(message) + if isinstance(value, bytes): + kwargs[key] = value.decode("ascii") + + if "params" in kwargs: + # Replace any "params" keyword with the raw "query" instead. + # + # Ensure that empty params use `kwargs["query"] = None` rather + # than `kwargs["query"] = ""`, so that generated URLs do not + # include an empty trailing "?". + params = kwargs.pop("params") + kwargs["query"] = None if not params else str(QueryParams(params)) + + if isinstance(url, str): + self._uri_reference = urlparse(url, **kwargs) + elif isinstance(url, URL): + self._uri_reference = url._uri_reference.copy_with(**kwargs) + else: + raise TypeError( + f"Invalid type for url. Expected str or httpx.URL, got {type(url)}: {url!r}" + ) + + @property + def scheme(self) -> str: + """ + The URL scheme, such as "http", "https". + Always normalised to lowercase. + """ + return self._uri_reference.scheme + + @property + def raw_scheme(self) -> bytes: + """ + The raw bytes representation of the URL scheme, such as b"http", b"https". + Always normalised to lowercase. + """ + return self._uri_reference.scheme.encode("ascii") + + @property + def userinfo(self) -> bytes: + """ + The URL userinfo as a raw bytestring. + For example: b"jo%40email.com:a%20secret". + """ + return self._uri_reference.userinfo.encode("ascii") + + @property + def username(self) -> str: + """ + The URL username as a string, with URL decoding applied. + For example: "jo@email.com" + """ + userinfo = self._uri_reference.userinfo + return unquote(userinfo.partition(":")[0]) + + @property + def password(self) -> str: + """ + The URL password as a string, with URL decoding applied. + For example: "a secret" + """ + userinfo = self._uri_reference.userinfo + return unquote(userinfo.partition(":")[2]) + + @property + def host(self) -> str: + """ + The URL host as a string. + Always normalized to lowercase, with IDNA hosts decoded into unicode. + + Examples: + + url = httpx.URL("http://www.EXAMPLE.org") + assert url.host == "www.example.org" + + url = httpx.URL("http://中国.icom.museum") + assert url.host == "中国.icom.museum" + + url = httpx.URL("http://xn--fiqs8s.icom.museum") + assert url.host == "中国.icom.museum" + + url = httpx.URL("https://[::ffff:192.168.0.1]") + assert url.host == "::ffff:192.168.0.1" + """ + host: str = self._uri_reference.host + + if host.startswith("xn--"): + host = idna.decode(host) + + return host + + @property + def raw_host(self) -> bytes: + """ + The raw bytes representation of the URL host. + Always normalized to lowercase, and IDNA encoded. + + Examples: + + url = httpx.URL("http://www.EXAMPLE.org") + assert url.raw_host == b"www.example.org" + + url = httpx.URL("http://中国.icom.museum") + assert url.raw_host == b"xn--fiqs8s.icom.museum" + + url = httpx.URL("http://xn--fiqs8s.icom.museum") + assert url.raw_host == b"xn--fiqs8s.icom.museum" + + url = httpx.URL("https://[::ffff:192.168.0.1]") + assert url.raw_host == b"::ffff:192.168.0.1" + """ + return self._uri_reference.host.encode("ascii") + + @property + def port(self) -> typing.Optional[int]: + """ + The URL port as an integer. + + Note that the URL class performs port normalization as per the WHATWG spec. + Default ports for "http", "https", "ws", "wss", and "ftp" schemes are always + treated as `None`. + + For example: + + assert httpx.URL("http://www.example.com") == httpx.URL("http://www.example.com:80") + assert httpx.URL("http://www.example.com:80").port is None + """ + return self._uri_reference.port + + @property + def netloc(self) -> bytes: + """ + Either `` or `:` as bytes. + Always normalized to lowercase, and IDNA encoded. + + This property may be used for generating the value of a request + "Host" header. + """ + return self._uri_reference.netloc.encode("ascii") + + @property + def path(self) -> str: + """ + The URL path as a string. Excluding the query string, and URL decoded. + + For example: + + url = httpx.URL("https://example.com/pa%20th") + assert url.path == "/pa th" + """ + path = self._uri_reference.path or "/" + return unquote(path) + + @property + def query(self) -> bytes: + """ + The URL query string, as raw bytes, excluding the leading b"?". + + This is necessarily a bytewise interface, because we cannot + perform URL decoding of this representation until we've parsed + the keys and values into a QueryParams instance. + + For example: + + url = httpx.URL("https://example.com/?filter=some%20search%20terms") + assert url.query == b"filter=some%20search%20terms" + """ + query = self._uri_reference.query or "" + return query.encode("ascii") + + @property + def params(self) -> "QueryParams": + """ + The URL query parameters, neatly parsed and packaged into an immutable + multidict representation. + """ + return QueryParams(self._uri_reference.query) + + @property + def raw_path(self) -> bytes: + """ + The complete URL path and query string as raw bytes. + Used as the target when constructing HTTP requests. + + For example: + + GET /users?search=some%20text HTTP/1.1 + Host: www.example.org + Connection: close + """ + path = self._uri_reference.path or "/" + if self._uri_reference.query is not None: + path += "?" + self._uri_reference.query + return path.encode("ascii") + + @property + def fragment(self) -> str: + """ + The URL fragments, as used in HTML anchors. + As a string, without the leading '#'. + """ + return unquote(self._uri_reference.fragment or "") + + @property + def raw(self) -> RawURL: + """ + Provides the (scheme, host, port, target) for the outgoing request. + + In older versions of `httpx` this was used in the low-level transport API. + We no longer use `RawURL`, and this property will be deprecated in a future release. + """ + return RawURL( + self.raw_scheme, + self.raw_host, + self.port, + self.raw_path, + ) + + @property + def is_absolute_url(self) -> bool: + """ + Return `True` for absolute URLs such as 'http://example.com/path', + and `False` for relative URLs such as '/path'. + """ + # We don't use `.is_absolute` from `rfc3986` because it treats + # URLs with a fragment portion as not absolute. + # What we actually care about is if the URL provides + # a scheme and hostname to which connections should be made. + return bool(self._uri_reference.scheme and self._uri_reference.host) + + @property + def is_relative_url(self) -> bool: + """ + Return `False` for absolute URLs such as 'http://example.com/path', + and `True` for relative URLs such as '/path'. + """ + return not self.is_absolute_url + + def copy_with(self, **kwargs: typing.Any) -> "URL": + """ + Copy this URL, returning a new URL with some components altered. + Accepts the same set of parameters as the components that are made + available via properties on the `URL` class. + + For example: + + url = httpx.URL("https://www.example.com").copy_with(username="jo@gmail.com", password="a secret") + assert url == "https://jo%40email.com:a%20secret@www.example.com" + """ + return URL(self, **kwargs) + + def copy_set_param(self, key: str, value: typing.Any = None) -> "URL": + return self.copy_with(params=self.params.set(key, value)) + + def copy_add_param(self, key: str, value: typing.Any = None) -> "URL": + return self.copy_with(params=self.params.add(key, value)) + + def copy_remove_param(self, key: str) -> "URL": + return self.copy_with(params=self.params.remove(key)) + + def copy_merge_params(self, params: QueryParamTypes) -> "URL": + return self.copy_with(params=self.params.merge(params)) + + def join(self, url: URLTypes) -> "URL": + """ + Return an absolute URL, using this URL as the base. + + Eg. + + url = httpx.URL("https://www.example.com/test") + url = url.join("/new/path") + assert url == "https://www.example.com/new/path" + """ + from urllib.parse import urljoin + + return URL(urljoin(str(self), str(URL(url)))) + + def __hash__(self) -> int: + return hash(str(self)) + + def __eq__(self, other: typing.Any) -> bool: + return isinstance(other, (URL, str)) and str(self) == str(URL(other)) + + def __str__(self) -> str: + return str(self._uri_reference) + + def __repr__(self) -> str: + scheme, userinfo, host, port, path, query, fragment = self._uri_reference + + if ":" in userinfo: + # Mask any password component. + userinfo = f'{userinfo.split(":")[0]}:[secure]' + + authority = "".join( + [ + f"{userinfo}@" if userinfo else "", + f"[{host}]" if ":" in host else host, + f":{port}" if port is not None else "", + ] + ) + url = "".join( + [ + f"{self.scheme}:" if scheme else "", + f"//{authority}" if authority else "", + path, + f"?{query}" if query is not None else "", + f"#{fragment}" if fragment is not None else "", + ] + ) + + return f"{self.__class__.__name__}({url!r})" + + +class QueryParams(typing.Mapping[str, str]): + """ + URL query parameters, as a multi-dict. + """ + + def __init__( + self, *args: typing.Optional[QueryParamTypes], **kwargs: typing.Any + ) -> None: + assert len(args) < 2, "Too many arguments." + assert not (args and kwargs), "Cannot mix named and unnamed arguments." + + value = args[0] if args else kwargs + + if value is None or isinstance(value, (str, bytes)): + value = value.decode("ascii") if isinstance(value, bytes) else value + self._dict = parse_qs(value, keep_blank_values=True) + elif isinstance(value, QueryParams): + self._dict = {k: list(v) for k, v in value._dict.items()} + else: + dict_value: typing.Dict[typing.Any, typing.List[typing.Any]] = {} + if isinstance(value, (list, tuple)): + # Convert list inputs like: + # [("a", "123"), ("a", "456"), ("b", "789")] + # To a dict representation, like: + # {"a": ["123", "456"], "b": ["789"]} + for item in value: + dict_value.setdefault(item[0], []).append(item[1]) + else: + # Convert dict inputs like: + # {"a": "123", "b": ["456", "789"]} + # To dict inputs where values are always lists, like: + # {"a": ["123"], "b": ["456", "789"]} + dict_value = { + k: list(v) if isinstance(v, (list, tuple)) else [v] + for k, v in value.items() + } + + # Ensure that keys and values are neatly coerced to strings. + # We coerce values `True` and `False` to JSON-like "true" and "false" + # representations, and coerce `None` values to the empty string. + self._dict = { + str(k): [primitive_value_to_str(item) for item in v] + for k, v in dict_value.items() + } + + def keys(self) -> typing.KeysView[str]: + """ + Return all the keys in the query params. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert list(q.keys()) == ["a", "b"] + """ + return self._dict.keys() + + def values(self) -> typing.ValuesView[str]: + """ + Return all the values in the query params. If a key occurs more than once + only the first item for that key is returned. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert list(q.values()) == ["123", "789"] + """ + return {k: v[0] for k, v in self._dict.items()}.values() + + def items(self) -> typing.ItemsView[str, str]: + """ + Return all items in the query params. If a key occurs more than once + only the first item for that key is returned. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert list(q.items()) == [("a", "123"), ("b", "789")] + """ + return {k: v[0] for k, v in self._dict.items()}.items() + + def multi_items(self) -> typing.List[typing.Tuple[str, str]]: + """ + Return all items in the query params. Allow duplicate keys to occur. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert list(q.multi_items()) == [("a", "123"), ("a", "456"), ("b", "789")] + """ + multi_items: typing.List[typing.Tuple[str, str]] = [] + for k, v in self._dict.items(): + multi_items.extend([(k, i) for i in v]) + return multi_items + + def get(self, key: typing.Any, default: typing.Any = None) -> typing.Any: + """ + Get a value from the query param for a given key. If the key occurs + more than once, then only the first value is returned. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert q.get("a") == "123" + """ + if key in self._dict: + return self._dict[str(key)][0] + return default + + def get_list(self, key: str) -> typing.List[str]: + """ + Get all values from the query param for a given key. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert q.get_list("a") == ["123", "456"] + """ + return list(self._dict.get(str(key), [])) + + def set(self, key: str, value: typing.Any = None) -> "QueryParams": + """ + Return a new QueryParams instance, setting the value of a key. + + Usage: + + q = httpx.QueryParams("a=123") + q = q.set("a", "456") + assert q == httpx.QueryParams("a=456") + """ + q = QueryParams() + q._dict = dict(self._dict) + q._dict[str(key)] = [primitive_value_to_str(value)] + return q + + def add(self, key: str, value: typing.Any = None) -> "QueryParams": + """ + Return a new QueryParams instance, setting or appending the value of a key. + + Usage: + + q = httpx.QueryParams("a=123") + q = q.add("a", "456") + assert q == httpx.QueryParams("a=123&a=456") + """ + q = QueryParams() + q._dict = dict(self._dict) + q._dict[str(key)] = q.get_list(key) + [primitive_value_to_str(value)] + return q + + def remove(self, key: str) -> "QueryParams": + """ + Return a new QueryParams instance, removing the value of a key. + + Usage: + + q = httpx.QueryParams("a=123") + q = q.remove("a") + assert q == httpx.QueryParams("") + """ + q = QueryParams() + q._dict = dict(self._dict) + q._dict.pop(str(key), None) + return q + + def merge(self, params: typing.Optional[QueryParamTypes] = None) -> "QueryParams": + """ + Return a new QueryParams instance, updated with. + + Usage: + + q = httpx.QueryParams("a=123") + q = q.merge({"b": "456"}) + assert q == httpx.QueryParams("a=123&b=456") + + q = httpx.QueryParams("a=123") + q = q.merge({"a": "456", "b": "789"}) + assert q == httpx.QueryParams("a=456&b=789") + """ + q = QueryParams(params) + q._dict = {**self._dict, **q._dict} + return q + + def __getitem__(self, key: typing.Any) -> str: + return self._dict[key][0] + + def __contains__(self, key: typing.Any) -> bool: + return key in self._dict + + def __iter__(self) -> typing.Iterator[typing.Any]: + return iter(self.keys()) + + def __len__(self) -> int: + return len(self._dict) + + def __bool__(self) -> bool: + return bool(self._dict) + + def __hash__(self) -> int: + return hash(str(self)) + + def __eq__(self, other: typing.Any) -> bool: + if not isinstance(other, self.__class__): + return False + return sorted(self.multi_items()) == sorted(other.multi_items()) + + def __str__(self) -> str: + """ + Note that we use '%20' encoding for spaces, and treat '/' as a safe + character. + + See https://github.com/encode/httpx/issues/2536 and + https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlencode + """ + return urlencode(self.multi_items()) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + query_string = str(self) + return f"{class_name}({query_string!r})" + + def update(self, params: typing.Optional[QueryParamTypes] = None) -> None: + raise RuntimeError( + "QueryParams are immutable since 0.18.0. " + "Use `q = q.merge(...)` to create an updated copy." + ) + + def __setitem__(self, key: str, value: str) -> None: + raise RuntimeError( + "QueryParams are immutable since 0.18.0. " + "Use `q = q.set(key, value)` to create an updated copy." + ) diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/_utils.py b/Backend/venv/lib/python3.12/site-packages/httpx/_utils.py new file mode 100644 index 00000000..a3a045da --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/httpx/_utils.py @@ -0,0 +1,477 @@ +import codecs +import email.message +import ipaddress +import mimetypes +import os +import re +import time +import typing +from pathlib import Path +from urllib.request import getproxies + +import sniffio + +from ._types import PrimitiveData + +if typing.TYPE_CHECKING: # pragma: no cover + from ._urls import URL + + +_HTML5_FORM_ENCODING_REPLACEMENTS = {'"': "%22", "\\": "\\\\"} +_HTML5_FORM_ENCODING_REPLACEMENTS.update( + {chr(c): "%{:02X}".format(c) for c in range(0x1F + 1) if c != 0x1B} +) +_HTML5_FORM_ENCODING_RE = re.compile( + r"|".join([re.escape(c) for c in _HTML5_FORM_ENCODING_REPLACEMENTS.keys()]) +) + + +def normalize_header_key( + value: typing.Union[str, bytes], + lower: bool, + encoding: typing.Optional[str] = None, +) -> bytes: + """ + Coerce str/bytes into a strictly byte-wise HTTP header key. + """ + if isinstance(value, bytes): + bytes_value = value + else: + bytes_value = value.encode(encoding or "ascii") + + return bytes_value.lower() if lower else bytes_value + + +def normalize_header_value( + value: typing.Union[str, bytes], encoding: typing.Optional[str] = None +) -> bytes: + """ + Coerce str/bytes into a strictly byte-wise HTTP header value. + """ + if isinstance(value, bytes): + return value + return value.encode(encoding or "ascii") + + +def primitive_value_to_str(value: "PrimitiveData") -> str: + """ + Coerce a primitive data type into a string value. + + Note that we prefer JSON-style 'true'/'false' for boolean values here. + """ + if value is True: + return "true" + elif value is False: + return "false" + elif value is None: + return "" + return str(value) + + +def is_known_encoding(encoding: str) -> bool: + """ + Return `True` if `encoding` is a known codec. + """ + try: + codecs.lookup(encoding) + except LookupError: + return False + return True + + +def format_form_param(name: str, value: str) -> bytes: + """ + Encode a name/value pair within a multipart form. + """ + + def replacer(match: typing.Match[str]) -> str: + return _HTML5_FORM_ENCODING_REPLACEMENTS[match.group(0)] + + value = _HTML5_FORM_ENCODING_RE.sub(replacer, value) + return f'{name}="{value}"'.encode() + + +# Null bytes; no need to recreate these on each call to guess_json_utf +_null = b"\x00" +_null2 = _null * 2 +_null3 = _null * 3 + + +def guess_json_utf(data: bytes) -> typing.Optional[str]: + # JSON always starts with two ASCII characters, so detection is as + # easy as counting the nulls and from their location and count + # determine the encoding. Also detect a BOM, if present. + sample = data[:4] + if sample in (codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE): + return "utf-32" # BOM included + if sample[:3] == codecs.BOM_UTF8: + return "utf-8-sig" # BOM included, MS style (discouraged) + if sample[:2] in (codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE): + return "utf-16" # BOM included + nullcount = sample.count(_null) + if nullcount == 0: + return "utf-8" + if nullcount == 2: + if sample[::2] == _null2: # 1st and 3rd are null + return "utf-16-be" + if sample[1::2] == _null2: # 2nd and 4th are null + return "utf-16-le" + # Did not detect 2 valid UTF-16 ascii-range characters + if nullcount == 3: + if sample[:3] == _null3: + return "utf-32-be" + if sample[1:] == _null3: + return "utf-32-le" + # Did not detect a valid UTF-32 ascii-range character + return None + + +def get_ca_bundle_from_env() -> typing.Optional[str]: + if "SSL_CERT_FILE" in os.environ: + ssl_file = Path(os.environ["SSL_CERT_FILE"]) + if ssl_file.is_file(): + return str(ssl_file) + if "SSL_CERT_DIR" in os.environ: + ssl_path = Path(os.environ["SSL_CERT_DIR"]) + if ssl_path.is_dir(): + return str(ssl_path) + return None + + +def parse_header_links(value: str) -> typing.List[typing.Dict[str, str]]: + """ + Returns a list of parsed link headers, for more info see: + https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link + The generic syntax of those is: + Link: < uri-reference >; param1=value1; param2="value2" + So for instance: + Link; '; type="image/jpeg",;' + would return + [ + {"url": "http:/.../front.jpeg", "type": "image/jpeg"}, + {"url": "http://.../back.jpeg"}, + ] + :param value: HTTP Link entity-header field + :return: list of parsed link headers + """ + links: typing.List[typing.Dict[str, str]] = [] + replace_chars = " '\"" + value = value.strip(replace_chars) + if not value: + return links + for val in re.split(", *<", value): + try: + url, params = val.split(";", 1) + except ValueError: + url, params = val, "" + link = {"url": url.strip("<> '\"")} + for param in params.split(";"): + try: + key, value = param.split("=") + except ValueError: + break + link[key.strip(replace_chars)] = value.strip(replace_chars) + links.append(link) + return links + + +def parse_content_type_charset(content_type: str) -> typing.Optional[str]: + # We used to use `cgi.parse_header()` here, but `cgi` became a dead battery. + # See: https://peps.python.org/pep-0594/#cgi + msg = email.message.Message() + msg["content-type"] = content_type + return msg.get_content_charset(failobj=None) + + +SENSITIVE_HEADERS = {"authorization", "proxy-authorization"} + + +def obfuscate_sensitive_headers( + items: typing.Iterable[typing.Tuple[typing.AnyStr, typing.AnyStr]] +) -> typing.Iterator[typing.Tuple[typing.AnyStr, typing.AnyStr]]: + for k, v in items: + if to_str(k.lower()) in SENSITIVE_HEADERS: + v = to_bytes_or_str("[secure]", match_type_of=v) + yield k, v + + +def port_or_default(url: "URL") -> typing.Optional[int]: + if url.port is not None: + return url.port + return {"http": 80, "https": 443}.get(url.scheme) + + +def same_origin(url: "URL", other: "URL") -> bool: + """ + Return 'True' if the given URLs share the same origin. + """ + return ( + url.scheme == other.scheme + and url.host == other.host + and port_or_default(url) == port_or_default(other) + ) + + +def is_https_redirect(url: "URL", location: "URL") -> bool: + """ + Return 'True' if 'location' is a HTTPS upgrade of 'url' + """ + if url.host != location.host: + return False + + return ( + url.scheme == "http" + and port_or_default(url) == 80 + and location.scheme == "https" + and port_or_default(location) == 443 + ) + + +def get_environment_proxies() -> typing.Dict[str, typing.Optional[str]]: + """Gets proxy information from the environment""" + + # urllib.request.getproxies() falls back on System + # Registry and Config for proxies on Windows and macOS. + # We don't want to propagate non-HTTP proxies into + # our configuration such as 'TRAVIS_APT_PROXY'. + proxy_info = getproxies() + mounts: typing.Dict[str, typing.Optional[str]] = {} + + for scheme in ("http", "https", "all"): + if proxy_info.get(scheme): + hostname = proxy_info[scheme] + mounts[f"{scheme}://"] = ( + hostname if "://" in hostname else f"http://{hostname}" + ) + + no_proxy_hosts = [host.strip() for host in proxy_info.get("no", "").split(",")] + for hostname in no_proxy_hosts: + # See https://curl.haxx.se/libcurl/c/CURLOPT_NOPROXY.html for details + # on how names in `NO_PROXY` are handled. + if hostname == "*": + # If NO_PROXY=* is used or if "*" occurs as any one of the comma + # separated hostnames, then we should just bypass any information + # from HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and always ignore + # proxies. + return {} + elif hostname: + # NO_PROXY=.google.com is marked as "all://*.google.com, + # which disables "www.google.com" but not "google.com" + # NO_PROXY=google.com is marked as "all://*google.com, + # which disables "www.google.com" and "google.com". + # (But not "wwwgoogle.com") + # NO_PROXY can include domains, IPv6, IPv4 addresses and "localhost" + # NO_PROXY=example.com,::1,localhost,192.168.0.0/16 + if is_ipv4_hostname(hostname): + mounts[f"all://{hostname}"] = None + elif is_ipv6_hostname(hostname): + mounts[f"all://[{hostname}]"] = None + elif hostname.lower() == "localhost": + mounts[f"all://{hostname}"] = None + else: + mounts[f"all://*{hostname}"] = None + + return mounts + + +def to_bytes(value: typing.Union[str, bytes], encoding: str = "utf-8") -> bytes: + return value.encode(encoding) if isinstance(value, str) else value + + +def to_str(value: typing.Union[str, bytes], encoding: str = "utf-8") -> str: + return value if isinstance(value, str) else value.decode(encoding) + + +def to_bytes_or_str(value: str, match_type_of: typing.AnyStr) -> typing.AnyStr: + return value if isinstance(match_type_of, str) else value.encode() + + +def unquote(value: str) -> str: + return value[1:-1] if value[0] == value[-1] == '"' else value + + +def guess_content_type(filename: typing.Optional[str]) -> typing.Optional[str]: + if filename: + return mimetypes.guess_type(filename)[0] or "application/octet-stream" + return None + + +def peek_filelike_length(stream: typing.Any) -> typing.Optional[int]: + """ + Given a file-like stream object, return its length in number of bytes + without reading it into memory. + """ + try: + # Is it an actual file? + fd = stream.fileno() + # Yup, seems to be an actual file. + length = os.fstat(fd).st_size + except (AttributeError, OSError): + # No... Maybe it's something that supports random access, like `io.BytesIO`? + try: + # Assuming so, go to end of stream to figure out its length, + # then put it back in place. + offset = stream.tell() + length = stream.seek(0, os.SEEK_END) + stream.seek(offset) + except (AttributeError, OSError): + # Not even that? Sorry, we're doomed... + return None + + return length + + +class Timer: + async def _get_time(self) -> float: + library = sniffio.current_async_library() + if library == "trio": + import trio + + return trio.current_time() + elif library == "curio": # pragma: no cover + import curio + + return typing.cast(float, await curio.clock()) + + import asyncio + + return asyncio.get_event_loop().time() + + def sync_start(self) -> None: + self.started = time.perf_counter() + + async def async_start(self) -> None: + self.started = await self._get_time() + + def sync_elapsed(self) -> float: + now = time.perf_counter() + return now - self.started + + async def async_elapsed(self) -> float: + now = await self._get_time() + return now - self.started + + +class URLPattern: + """ + A utility class currently used for making lookups against proxy keys... + + # Wildcard matching... + >>> pattern = URLPattern("all") + >>> pattern.matches(httpx.URL("http://example.com")) + True + + # Witch scheme matching... + >>> pattern = URLPattern("https") + >>> pattern.matches(httpx.URL("https://example.com")) + True + >>> pattern.matches(httpx.URL("http://example.com")) + False + + # With domain matching... + >>> pattern = URLPattern("https://example.com") + >>> pattern.matches(httpx.URL("https://example.com")) + True + >>> pattern.matches(httpx.URL("http://example.com")) + False + >>> pattern.matches(httpx.URL("https://other.com")) + False + + # Wildcard scheme, with domain matching... + >>> pattern = URLPattern("all://example.com") + >>> pattern.matches(httpx.URL("https://example.com")) + True + >>> pattern.matches(httpx.URL("http://example.com")) + True + >>> pattern.matches(httpx.URL("https://other.com")) + False + + # With port matching... + >>> pattern = URLPattern("https://example.com:1234") + >>> pattern.matches(httpx.URL("https://example.com:1234")) + True + >>> pattern.matches(httpx.URL("https://example.com")) + False + """ + + def __init__(self, pattern: str) -> None: + from ._urls import URL + + if pattern and ":" not in pattern: + raise ValueError( + f"Proxy keys should use proper URL forms rather " + f"than plain scheme strings. " + f'Instead of "{pattern}", use "{pattern}://"' + ) + + url = URL(pattern) + self.pattern = pattern + self.scheme = "" if url.scheme == "all" else url.scheme + self.host = "" if url.host == "*" else url.host + self.port = url.port + if not url.host or url.host == "*": + self.host_regex: typing.Optional[typing.Pattern[str]] = None + elif url.host.startswith("*."): + # *.example.com should match "www.example.com", but not "example.com" + domain = re.escape(url.host[2:]) + self.host_regex = re.compile(f"^.+\\.{domain}$") + elif url.host.startswith("*"): + # *example.com should match "www.example.com" and "example.com" + domain = re.escape(url.host[1:]) + self.host_regex = re.compile(f"^(.+\\.)?{domain}$") + else: + # example.com should match "example.com" but not "www.example.com" + domain = re.escape(url.host) + self.host_regex = re.compile(f"^{domain}$") + + def matches(self, other: "URL") -> bool: + if self.scheme and self.scheme != other.scheme: + return False + if ( + self.host + and self.host_regex is not None + and not self.host_regex.match(other.host) + ): + return False + if self.port is not None and self.port != other.port: + return False + return True + + @property + def priority(self) -> typing.Tuple[int, int, int]: + """ + The priority allows URLPattern instances to be sortable, so that + we can match from most specific to least specific. + """ + # URLs with a port should take priority over URLs without a port. + port_priority = 0 if self.port is not None else 1 + # Longer hostnames should match first. + host_priority = -len(self.host) + # Longer schemes should match first. + scheme_priority = -len(self.scheme) + return (port_priority, host_priority, scheme_priority) + + def __hash__(self) -> int: + return hash(self.pattern) + + def __lt__(self, other: "URLPattern") -> bool: + return self.priority < other.priority + + def __eq__(self, other: typing.Any) -> bool: + return isinstance(other, URLPattern) and self.pattern == other.pattern + + +def is_ipv4_hostname(hostname: str) -> bool: + try: + ipaddress.IPv4Address(hostname.split("/")[0]) + except Exception: + return False + return True + + +def is_ipv6_hostname(hostname: str) -> bool: + try: + ipaddress.IPv6Address(hostname.split("/")[0]) + except Exception: + return False + return True diff --git a/Backend/venv/lib/python3.12/site-packages/httpx/py.typed b/Backend/venv/lib/python3.12/site-packages/httpx/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/Backend/venv/lib/python3.12/site-packages/idna/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/idna/__pycache__/__init__.cpython-312.pyc index d97f9f474c2eea881f998342699a5d775a779923..f523ba29c532b76a49d9f07d7657d098e1204a40 100644 GIT binary patch delta 20 acmey$_LYtMG%qg~0}!YiDsAM>Vg>*@Qv{R% delta 20 acmey$_LYtMG%qg~0}wooP}s69xc3g9PvZ delta 20 acmeyV_EU}fG%qg~0}xbQlibLiCky~Zc?I79 diff --git a/Backend/venv/lib/python3.12/site-packages/idna/__pycache__/compat.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/idna/__pycache__/compat.cpython-312.pyc index b24016d5835cc1a111ea6470524ce5d60acbd9b9..48e76c54debaf9b60d3c585936a01152b7ac25c5 100644 GIT binary patch delta 20 acmey#_LGhKG%qg~0}!YiDsAM>V+H^^^8}y( delta 20 acmey#_LGhKG%qg~0}xbQlibLi#|!{K=>?Af diff --git a/Backend/venv/lib/python3.12/site-packages/idna/__pycache__/core.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/idna/__pycache__/core.cpython-312.pyc index 8e435daf1e76f32cf8db68c4e939997635d34bb9..f4e1608278635a4bd181f82b0aa8c6d4c0b11344 100644 GIT binary patch delta 20 acmX?FceIZCG%qg~0}!YiDsAMpvIhV|?*%Xb delta 20 acmX?FceIZCG%qg~0}wooP}sBOmHty5Byj%=Gpl+zt$i0=DQN0TQSBOmHty5Byj%=G@H9f9k$WpQqk0zrVu}Xt diff --git a/Backend/venv/lib/python3.12/site-packages/idna/__pycache__/intranges.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/idna/__pycache__/intranges.cpython-312.pyc index 095fb11a5eebedb0edeab67a556f02cc5c9c488e..478f00497af856a10761c4c3ed7dd816a27d659f 100644 GIT binary patch delta 20 acmX>ra#n=ra#n= diff --git a/Backend/venv/lib/python3.12/site-packages/idna/__pycache__/package_data.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/idna/__pycache__/package_data.cpython-312.pyc index dcb1d04d715231decfb266ed2500f09251ed3d8b..6feaa5dc17f8cc16668eedfffd9cbce4a3e708ca 100644 GIT binary patch delta 19 Zcmcb?c!QDqG%qg~0}!YiDox}*3ji?$1dsp# delta 19 Zcmcb?c!QDqG%qg~0}wooP?*Sl763Lg1#, Holger Krekel +License-Expression: MIT +Project-URL: Homepage, https://github.com/pytest-dev/iniconfig +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Utilities +Requires-Python: >=3.10 +Description-Content-Type: text/x-rst +License-File: LICENSE +Dynamic: license-file + +iniconfig: brain-dead simple parsing of ini files +======================================================= + +iniconfig is a small and simple INI-file parser module +having a unique set of features: + +* maintains order of sections and entries +* supports multi-line values with or without line-continuations +* supports "#" comments everywhere +* raises errors with proper line-numbers +* no bells and whistles like automatic substitutions +* iniconfig raises an Error if two sections have the same name. + +If you encounter issues or have feature wishes please report them to: + + https://github.com/RonnyPfannschmidt/iniconfig/issues + +Basic Example +=================================== + +If you have an ini file like this: + +.. code-block:: ini + + # content of example.ini + [section1] # comment + name1=value1 # comment + name1b=value1,value2 # comment + + [section2] + name2= + line1 + line2 + +then you can do: + +.. code-block:: pycon + + >>> import iniconfig + >>> ini = iniconfig.IniConfig("example.ini") + >>> ini['section1']['name1'] # raises KeyError if not exists + 'value1' + >>> ini.get('section1', 'name1b', [], lambda x: x.split(",")) + ['value1', 'value2'] + >>> ini.get('section1', 'notexist', [], lambda x: x.split(",")) + [] + >>> [x.name for x in list(ini)] + ['section1', 'section2'] + >>> list(list(ini)[0].items()) + [('name1', 'value1'), ('name1b', 'value1,value2')] + >>> 'section1' in ini + True + >>> 'inexistendsection' in ini + False diff --git a/Backend/venv/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/RECORD b/Backend/venv/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/RECORD new file mode 100644 index 00000000..c9899e46 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/RECORD @@ -0,0 +1,15 @@ +iniconfig-2.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +iniconfig-2.3.0.dist-info/METADATA,sha256=QNdz-E5OES9JW79PG-nL0tRWwK6271MR910b8yLyFls,2526 +iniconfig-2.3.0.dist-info/RECORD,, +iniconfig-2.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91 +iniconfig-2.3.0.dist-info/licenses/LICENSE,sha256=NAn6kfes5VeJRjJnZlbjImT-XvdYFTVyXcmiN3RVG9Q,1098 +iniconfig-2.3.0.dist-info/top_level.txt,sha256=7KfM0fugdlToj9UW7enKXk2HYALQD8qHiyKtjhSzgN8,10 +iniconfig/__init__.py,sha256=XL5eqUYj4mskAOorZ5jfRAinJvJzTI-fJxpP4xfXtaw,7497 +iniconfig/__pycache__/__init__.cpython-312.pyc,, +iniconfig/__pycache__/_parse.cpython-312.pyc,, +iniconfig/__pycache__/_version.cpython-312.pyc,, +iniconfig/__pycache__/exceptions.cpython-312.pyc,, +iniconfig/_parse.py,sha256=5ncBl7MAQiaPNnpRrs9FR4t6G6DkgOUs458OY_1CR28,5223 +iniconfig/_version.py,sha256=KNFYe-Vtdt7Z-oHyl8jmDAQ9qXoCNMAEXigj6BR1QUI,704 +iniconfig/exceptions.py,sha256=mipQ_aMxD9CvSvFWN1oTXY4QuRnKAMZ1f3sCdmjDTU0,399 +iniconfig/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/Backend/venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/WHEEL b/Backend/venv/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/WHEEL similarity index 65% rename from Backend/venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/WHEEL rename to Backend/venv/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/WHEEL index 1eb3c49d..e7fa31b6 100644 --- a/Backend/venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/WHEEL +++ b/Backend/venv/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/WHEEL @@ -1,5 +1,5 @@ Wheel-Version: 1.0 -Generator: setuptools (78.1.0) +Generator: setuptools (80.9.0) Root-Is-Purelib: true Tag: py3-none-any diff --git a/Backend/venv/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/licenses/LICENSE b/Backend/venv/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/licenses/LICENSE new file mode 100644 index 00000000..46f4b284 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2010 - 2023 Holger Krekel and others + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Backend/venv/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/top_level.txt b/Backend/venv/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/top_level.txt new file mode 100644 index 00000000..9dda5369 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/top_level.txt @@ -0,0 +1 @@ +iniconfig diff --git a/Backend/venv/lib/python3.12/site-packages/iniconfig/__init__.py b/Backend/venv/lib/python3.12/site-packages/iniconfig/__init__.py new file mode 100644 index 00000000..b84809f8 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/iniconfig/__init__.py @@ -0,0 +1,249 @@ +"""brain-dead simple parser for ini-style files. +(C) Ronny Pfannschmidt, Holger Krekel -- MIT licensed +""" + +import os +from collections.abc import Callable +from collections.abc import Iterator +from collections.abc import Mapping +from typing import Final +from typing import TypeVar +from typing import overload + +__all__ = ["IniConfig", "ParseError", "COMMENTCHARS", "iscommentline"] + +from . import _parse +from ._parse import COMMENTCHARS +from ._parse import iscommentline +from .exceptions import ParseError + +_D = TypeVar("_D") +_T = TypeVar("_T") + + +class SectionWrapper: + config: Final["IniConfig"] + name: Final[str] + + def __init__(self, config: "IniConfig", name: str) -> None: + self.config = config + self.name = name + + def lineof(self, name: str) -> int | None: + return self.config.lineof(self.name, name) + + @overload + def get(self, key: str) -> str | None: ... + + @overload + def get( + self, + key: str, + convert: Callable[[str], _T], + ) -> _T | None: ... + + @overload + def get( + self, + key: str, + default: None, + convert: Callable[[str], _T], + ) -> _T | None: ... + + @overload + def get(self, key: str, default: _D, convert: None = None) -> str | _D: ... + + @overload + def get( + self, + key: str, + default: _D, + convert: Callable[[str], _T], + ) -> _T | _D: ... + + # TODO: investigate possible mypy bug wrt matching the passed over data + def get( # type: ignore [misc] + self, + key: str, + default: _D | None = None, + convert: Callable[[str], _T] | None = None, + ) -> _D | _T | str | None: + return self.config.get(self.name, key, convert=convert, default=default) + + def __getitem__(self, key: str) -> str: + return self.config.sections[self.name][key] + + def __iter__(self) -> Iterator[str]: + section: Mapping[str, str] = self.config.sections.get(self.name, {}) + + def lineof(key: str) -> int: + return self.config.lineof(self.name, key) # type: ignore[return-value] + + yield from sorted(section, key=lineof) + + def items(self) -> Iterator[tuple[str, str]]: + for name in self: + yield name, self[name] + + +class IniConfig: + path: Final[str] + sections: Final[Mapping[str, Mapping[str, str]]] + _sources: Final[Mapping[tuple[str, str | None], int]] + + def __init__( + self, + path: str | os.PathLike[str], + data: str | None = None, + encoding: str = "utf-8", + *, + _sections: Mapping[str, Mapping[str, str]] | None = None, + _sources: Mapping[tuple[str, str | None], int] | None = None, + ) -> None: + self.path = os.fspath(path) + + # Determine sections and sources + if _sections is not None and _sources is not None: + # Use provided pre-parsed data (called from parse()) + sections_data = _sections + sources = _sources + else: + # Parse the data (backward compatible path) + if data is None: + with open(self.path, encoding=encoding) as fp: + data = fp.read() + + # Use old behavior (no stripping) for backward compatibility + sections_data, sources = _parse.parse_ini_data( + self.path, data, strip_inline_comments=False + ) + + # Assign once to Final attributes + self._sources = sources + self.sections = sections_data + + @classmethod + def parse( + cls, + path: str | os.PathLike[str], + data: str | None = None, + encoding: str = "utf-8", + *, + strip_inline_comments: bool = True, + strip_section_whitespace: bool = False, + ) -> "IniConfig": + """Parse an INI file. + + Args: + path: Path to the INI file (used for error messages) + data: Optional INI content as string. If None, reads from path. + encoding: Encoding to use when reading the file (default: utf-8) + strip_inline_comments: Whether to strip inline comments from values + (default: True). When True, comments starting with # or ; are + removed from values, matching the behavior for section comments. + strip_section_whitespace: Whether to strip whitespace from section and key names + (default: False). When True, strips Unicode whitespace from section and key names, + addressing issue #4. When False, preserves existing behavior for backward compatibility. + + Returns: + IniConfig instance with parsed configuration + + Example: + # With comment stripping (default): + config = IniConfig.parse("setup.cfg") + # value = "foo" instead of "foo # comment" + + # Without comment stripping (old behavior): + config = IniConfig.parse("setup.cfg", strip_inline_comments=False) + # value = "foo # comment" + + # With section name stripping (opt-in for issue #4): + config = IniConfig.parse("setup.cfg", strip_section_whitespace=True) + # section names and keys have Unicode whitespace stripped + """ + fspath = os.fspath(path) + + if data is None: + with open(fspath, encoding=encoding) as fp: + data = fp.read() + + sections_data, sources = _parse.parse_ini_data( + fspath, + data, + strip_inline_comments=strip_inline_comments, + strip_section_whitespace=strip_section_whitespace, + ) + + # Call constructor with pre-parsed sections and sources + return cls(path=fspath, _sections=sections_data, _sources=sources) + + def lineof(self, section: str, name: str | None = None) -> int | None: + lineno = self._sources.get((section, name)) + return None if lineno is None else lineno + 1 + + @overload + def get( + self, + section: str, + name: str, + ) -> str | None: ... + + @overload + def get( + self, + section: str, + name: str, + convert: Callable[[str], _T], + ) -> _T | None: ... + + @overload + def get( + self, + section: str, + name: str, + default: None, + convert: Callable[[str], _T], + ) -> _T | None: ... + + @overload + def get( + self, section: str, name: str, default: _D, convert: None = None + ) -> str | _D: ... + + @overload + def get( + self, + section: str, + name: str, + default: _D, + convert: Callable[[str], _T], + ) -> _T | _D: ... + + def get( # type: ignore + self, + section: str, + name: str, + default: _D | None = None, + convert: Callable[[str], _T] | None = None, + ) -> _D | _T | str | None: + try: + value: str = self.sections[section][name] + except KeyError: + return default + else: + if convert is not None: + return convert(value) + else: + return value + + def __getitem__(self, name: str) -> SectionWrapper: + if name not in self.sections: + raise KeyError(name) + return SectionWrapper(self, name) + + def __iter__(self) -> Iterator[SectionWrapper]: + for name in sorted(self.sections, key=self.lineof): # type: ignore + yield SectionWrapper(self, name) + + def __contains__(self, arg: str) -> bool: + return arg in self.sections diff --git a/Backend/venv/lib/python3.12/site-packages/iniconfig/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/iniconfig/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d09276271f1f6a5fbbe280c49b146d7fdb9850b4 GIT binary patch literal 10885 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 SkbQYjcvay0?{btgj{Fb6b5v;n literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/iniconfig/__pycache__/_parse.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/iniconfig/__pycache__/_parse.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..57048630e30f5dfd39267d37a4314f3308daeac7 GIT binary patch literal 5585 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 tuple[Mapping[str, Mapping[str, str]], Mapping[tuple[str, str | None], int]]: + """Parse INI data and return sections and sources mappings. + + Args: + path: Path for error messages + data: INI content as string + strip_inline_comments: Whether to strip inline comments from values + strip_section_whitespace: Whether to strip whitespace from section and key names + (default: False). When True, addresses issue #4 by stripping Unicode whitespace. + + Returns: + Tuple of (sections_data, sources) where: + - sections_data: mapping of section -> {name -> value} + - sources: mapping of (section, name) -> line number + """ + tokens = parse_lines( + path, + data.splitlines(True), + strip_inline_comments=strip_inline_comments, + strip_section_whitespace=strip_section_whitespace, + ) + + sources: dict[tuple[str, str | None], int] = {} + sections_data: dict[str, dict[str, str]] = {} + + for lineno, section, name, value in tokens: + if section is None: + raise ParseError(path, lineno, "no section header defined") + sources[section, name] = lineno + if name is None: + if section in sections_data: + raise ParseError(path, lineno, f"duplicate section {section!r}") + sections_data[section] = {} + else: + if name in sections_data[section]: + raise ParseError(path, lineno, f"duplicate name {name!r}") + assert value is not None + sections_data[section][name] = value + + return sections_data, sources + + +def parse_lines( + path: str, + line_iter: list[str], + *, + strip_inline_comments: bool = False, + strip_section_whitespace: bool = False, +) -> list[ParsedLine]: + result: list[ParsedLine] = [] + section = None + for lineno, line in enumerate(line_iter): + name, data = _parseline( + path, line, lineno, strip_inline_comments, strip_section_whitespace + ) + # new value + if name is not None and data is not None: + result.append(ParsedLine(lineno, section, name, data)) + # new section + elif name is not None and data is None: + if not name: + raise ParseError(path, lineno, "empty section name") + section = name + result.append(ParsedLine(lineno, section, None, None)) + # continuation + elif name is None and data is not None: + if not result: + raise ParseError(path, lineno, "unexpected value continuation") + last = result.pop() + if last.name is None: + raise ParseError(path, lineno, "unexpected value continuation") + + if last.value: + last = last._replace(value=f"{last.value}\n{data}") + else: + last = last._replace(value=data) + result.append(last) + return result + + +def _parseline( + path: str, + line: str, + lineno: int, + strip_inline_comments: bool, + strip_section_whitespace: bool, +) -> tuple[str | None, str | None]: + # blank lines + if iscommentline(line): + line = "" + else: + line = line.rstrip() + if not line: + return None, None + # section + if line[0] == "[": + realline = line + for c in COMMENTCHARS: + line = line.split(c)[0].rstrip() + if line[-1] == "]": + section_name = line[1:-1] + # Optionally strip whitespace from section name (issue #4) + if strip_section_whitespace: + section_name = section_name.strip() + return section_name, None + return None, realline.strip() + # value + elif not line[0].isspace(): + try: + name, value = line.split("=", 1) + if ":" in name: + raise ValueError() + except ValueError: + try: + name, value = line.split(":", 1) + except ValueError: + raise ParseError(path, lineno, f"unexpected line: {line!r}") from None + + # Strip key name (always for backward compatibility, optionally with unicode awareness) + key_name = name.strip() + + # Strip value + value = value.strip() + # Strip inline comments from values if requested (issue #55) + if strip_inline_comments: + for c in COMMENTCHARS: + value = value.split(c)[0].rstrip() + + return key_name, value + # continuation + else: + line = line.strip() + # Strip inline comments from continuations if requested (issue #55) + if strip_inline_comments: + for c in COMMENTCHARS: + line = line.split(c)[0].rstrip() + return None, line + + +def iscommentline(line: str) -> bool: + c = line.lstrip()[:1] + return c in COMMENTCHARS diff --git a/Backend/venv/lib/python3.12/site-packages/iniconfig/_version.py b/Backend/venv/lib/python3.12/site-packages/iniconfig/_version.py new file mode 100644 index 00000000..b982b024 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/iniconfig/_version.py @@ -0,0 +1,34 @@ +# file generated by setuptools-scm +# don't change, don't track in version control + +__all__ = [ + "__version__", + "__version_tuple__", + "version", + "version_tuple", + "__commit_id__", + "commit_id", +] + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple + from typing import Union + + VERSION_TUPLE = Tuple[Union[int, str], ...] + COMMIT_ID = Union[str, None] +else: + VERSION_TUPLE = object + COMMIT_ID = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE +commit_id: COMMIT_ID +__commit_id__: COMMIT_ID + +__version__ = version = '2.3.0' +__version_tuple__ = version_tuple = (2, 3, 0) + +__commit_id__ = commit_id = None diff --git a/Backend/venv/lib/python3.12/site-packages/iniconfig/exceptions.py b/Backend/venv/lib/python3.12/site-packages/iniconfig/exceptions.py new file mode 100644 index 00000000..d078bc65 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/iniconfig/exceptions.py @@ -0,0 +1,16 @@ +from typing import Final + + +class ParseError(Exception): + path: Final[str] + lineno: Final[int] + msg: Final[str] + + def __init__(self, path: str, lineno: int, msg: str) -> None: + super().__init__(path, lineno, msg) + self.path = path + self.lineno = lineno + self.msg = msg + + def __str__(self) -> str: + return f"{self.path}:{self.lineno + 1}: {self.msg}" diff --git a/Backend/venv/lib/python3.12/site-packages/iniconfig/py.typed b/Backend/venv/lib/python3.12/site-packages/iniconfig/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/Backend/venv/lib/python3.12/site-packages/pip-25.3.dist-info/RECORD b/Backend/venv/lib/python3.12/site-packages/pip-25.3.dist-info/RECORD index e15bca5c..ab4c123e 100644 --- a/Backend/venv/lib/python3.12/site-packages/pip-25.3.dist-info/RECORD +++ b/Backend/venv/lib/python3.12/site-packages/pip-25.3.dist-info/RECORD @@ -1,6 +1,6 @@ -../../../bin/pip,sha256=sEfserb233VAwZLq2MJoS4ouLsCNFNElVZqmo5vu7Mc,261 -../../../bin/pip3,sha256=sEfserb233VAwZLq2MJoS4ouLsCNFNElVZqmo5vu7Mc,261 -../../../bin/pip3.12,sha256=sEfserb233VAwZLq2MJoS4ouLsCNFNElVZqmo5vu7Mc,261 +../../../bin/pip,sha256=xnV6ugaf9cQyx47UVXbXc1bgi7SW7k_fteJz1seYa60,233 +../../../bin/pip3,sha256=xnV6ugaf9cQyx47UVXbXc1bgi7SW7k_fteJz1seYa60,233 +../../../bin/pip3.12,sha256=xnV6ugaf9cQyx47UVXbXc1bgi7SW7k_fteJz1seYa60,233 pip-25.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 pip-25.3.dist-info/METADATA,sha256=Khugcl59I2--LVxQpP_5yeP-NMpJTyzr3lxFw3kTedM,4672 pip-25.3.dist-info/RECORD,, diff --git a/Backend/venv/lib/python3.12/site-packages/pip/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/__pycache__/__init__.cpython-312.pyc index eb6365aa6f7e5bf1bdbbb4a6c21fc5c4a120179a..133cde35636237629dc4f4acef5ebed840a560ea 100644 GIT binary patch delta 20 acmbQtI+>OGG%qg~0}wngP}<1d!2|#>=LDkw delta 20 acmbQtI+>OGG%qg~0}vEklibMN!2|#?js(yE diff --git a/Backend/venv/lib/python3.12/site-packages/pip/__pycache__/__main__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/__pycache__/__main__.cpython-312.pyc index f47763266d523168097fb7fd3fe1d1b49f3edf7a..dfccec438593d00fb3f6583fd3d5f03ab8f3d62c 100644 GIT binary patch delta 20 acmcc2cA1U)G%qg~0}wngP}<1t!3+R83@xT diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/autocompletion.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/autocompletion.cpython-312.pyc index ba9321567fb7b22bcbdbf468a5f0ff126dfbf435..eb971aed021e52d63eb56c35fced3d59460304d6 100644 GIT binary patch delta 20 acmZ4QzTTbtG%qg~0}wngP}<17L>T}+umzj| delta 20 acmZ4QzTTbtG%qg~0}vEklibL?L>T}-R|Uxc diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/base_command.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/base_command.cpython-312.pyc index 8d512ebdc5f6bda07ee597c28d4b67a433215c91..79ce44c06f2cb3db77287db8173c28ff261179c3 100644 GIT binary patch delta 20 acmaDA^eTw^G%qg~0}wngP};~HuL%H1dIjkK delta 20 acmaDA^eTw^G%qg~0}vEklibK1uL%H2AqExz diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/cmdoptions.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/cmdoptions.cpython-312.pyc index 7a910fed5a99729c48267a127b83753560e88de3..9bf3d2ea89e72ab1fa7921f3895258a4ddcc1561 100644 GIT binary patch delta 22 ccmbREmT}@+M()$Ryj%=G@W4Q6BX?UF09H%};{X5v delta 22 ccmbREmT}@+M()$Ryj%=GP;gChBX?UF09Nw{5C8xG diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/command_context.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/command_context.cpython-312.pyc index 4cfce28dca1b457e5c6ab9fe9726824f2e1114b7..bff8db8ec8bb03397d7b8b545daa973fab68ab06 100644 GIT binary patch delta 20 acmZ3+w~UYbG%qg~0}wngP};~X%?A delta 20 acmdmFvB`q_G%qg~0}vEklibLyE&~8OtOYFq diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/main.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/main.cpython-312.pyc index 9851bda67beb6d1c3e69e23543337201c39bdc5a..2d3e1a58e1bad837c31c8f43cee4f994cb116676 100644 GIT binary patch delta 20 acmcaBcvq17G%qg~0}wngP}<0Si30#WEd`$d delta 20 acmcaBcvq17G%qg~0}vEklibLCi30#W)CJ4{ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/main_parser.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/main_parser.cpython-312.pyc index 751d4618e960594fa882bb9d92d984b3806cb32d..052063e6d4fc51725572bc84d602152cde579dfa 100644 GIT binary patch delta 20 acmaE@`dXFyG%qg~0}wngP}<1-KnMUsHwF6u delta 20 acmaE@`dXFyG%qg~0}vEklibMtKnMUs-UcWD diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/parser.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/parser.cpython-312.pyc index 1e4d6cf850b71ab44d3b7487d435dd485c69993f..8b55a966691734ec743fc82fd582dbcd6a8077b7 100644 GIT binary patch delta 20 acmbPQGPQ*JG%qg~0}wngP}<1NWd#603k5#_ delta 20 acmbPQGPQ*JG%qg~0}vEklibM7Wd#60vIT4a diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/progress_bars.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/progress_bars.cpython-312.pyc index 0acb20aaf6969def8b2255bac6fcc8cd936e2482..88760d393edfef9ea19f79a4d764068e6cab8181 100644 GIT binary patch delta 20 acmaE)|45(vG%qg~0}wngP}<0ST^s;IJO%jx delta 20 acmaE)|45(vG%qg~0}vEklibLCT^s;I;|3-G diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/req_command.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/req_command.cpython-312.pyc index 55cba8ac1986d616d40e26263de6716a96612f0b..df76262b662f09ed9d38df4f216e39698f024fee 100644 GIT binary patch delta 20 acmZ3PvoeSKG%qg~0}wngP};~XX9fU52L)#U delta 20 acmZ3PvoeSKG%qg~0}vEklibKHX9fU5t_73; diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/spinners.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/spinners.cpython-312.pyc index 2aca524de88e837c9b9c00b6e94900ed6906b561..735b1621d670bcf98e4b92c58b7e82269c4d62b4 100644 GIT binary patch delta 20 acmeB(=!oDx&CAQh00a*Vls0nz)dm1O1O?Fm delta 20 acmeB(=!oDx&CAQh00afsBsX&Z)dm1Os|Ef5 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/status_codes.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/cli/__pycache__/status_codes.cpython-312.pyc index c44cf3fcbe020a6868820b5417310cda9acc744b..846bdbc8dc9a8b42393fc300aa87623fa94dc526 100644 GIT binary patch delta 20 acmZo@&CAQh00a*Vls0mgF#-TB&ID)x delta 20 acmZo@&CAQh00afsBsX%GF#-TCbp(|F diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/__init__.cpython-312.pyc index 3508d125656edc5ea321cbb39f418b6c25607383..22242d8d2d1eb4305ebd3480a5def219a5ad0ea8 100644 GIT binary patch delta 20 acmbQQFkgZDG%qg~0}wngP};~XA^-q2p#+Nn delta 20 acmbQQFkgZDG%qg~0}vEklibKHA^-q3NCdb5 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/cache.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/cache.cpython-312.pyc index 8f1bd439e699ac994dea725a816975b2b9570710..8a0e66212f8f6dfeb7ad1c06df3d91442f524865 100644 GIT binary patch delta 20 acmX@+f6SlzG%qg~0}wngP}<17TO9yHYz6fI delta 20 acmX@+f6SlzG%qg~0}vEklibL?TO9yI69ysx diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/check.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/check.cpython-312.pyc index e59271591d8c64612554ca0ad1038cd64315dcbd..346c61fb68bde5a1cce9b65fce9e897854538ed9 100644 GIT binary patch delta 20 acmbOyGEap2G%qg~0}wngP};~X%mn~5!vtyo delta 20 acmbOyGEap2G%qg~0}vEklibKH%mn~6Y6O=6 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/completion.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/completion.cpython-312.pyc index 379a5c94fd91e19b15637f85c326b05fcf299013..4d506edb0af69dc27403d5624c571f7240ba3d14 100644 GIT binary patch delta 20 acmdn3wOfn(G%qg~0}wngP}<0CAPN9GZUp!M delta 20 acmdn3wOfn(G%qg~0}vEklibK{APN9H6$K># diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/configuration.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/configuration.cpython-312.pyc index 4ed9649b66ad731e15da95073214850b715375c7..2e60d59e13fb13bb262f631dfc8fd3b6d4dd957c 100644 GIT binary patch delta 20 acmaEs@hpS;G%qg~0}wngP};~HZ2|yF{RR{O delta 20 acmaEs@hpS;G%qg~0}vEklibK1Z2|yGqy|9% diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/debug.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/debug.cpython-312.pyc index 8e4ff053cc1f168a5cf54c947059bf4e0a1568ca..3b950b8ce7732c50f3925d3f3b282ff38aa04734 100644 GIT binary patch delta 20 acmZ4Jx6qIKG%qg~0}wngP};~XrVao+fCU5q delta 20 acmZ4Jx6qIKG%qg~0}vEklibKHrVao-Cj~J8 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/download.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/download.cpython-312.pyc index 995a05e4c7d06b7e33a022d3cb13c359dfec8577..b4642008cfff78626fbd0a816e38b9d7339bdaab 100644 GIT binary patch delta 20 acmaEE@!W#@G%qg~0}wngP};~HBLe_LO$DC- delta 20 acmaEE@!W#@G%qg~0}vEklibK1BLe_L^aacS diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/freeze.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/freeze.cpython-312.pyc index f3b7458b5f05ee8feda32843ecb1125270443f5d..9dfda7340bdbb5ac32bea56ea8f62725459dfe85 100644 GIT binary patch delta 20 acmX@3ct(-?G%qg~0}wngP}<0SPyhfu%>|SI delta 20 acmX@3ct(-?G%qg~0}vEklibLCPyhfvbOpfx diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/hash.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/hash.cpython-312.pyc index 9b80039d3638ebabef97517d73f80f148e805548..ad2e647185c4129f3307ccb28abfe26e2717628e 100644 GIT binary patch delta 20 acmeAZ?-S=f&CAQh00a*Vls0lVa{~Y}umr&X delta 20 acmeAZ?-S=f&CAQh00afsBsX$5a{~Y~R|M_= diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/help.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/help.cpython-312.pyc index 31d5e7ccc49384cfed2c5f659c61f0a8e8856c50..1fd285dee5c389d41746ec34aa7712f250adecc2 100644 GIT binary patch delta 20 acmeys^MQx^G%qg~0}wngP}<0y$_4;GZv{*M delta 20 acmeys^MQx^G%qg~0}vEklibLi$_4;H76o|# diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/index.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/index.cpython-312.pyc index ffab62b7ba5d938cbe24a61ea742cac8f1ee6739..46104db984362d976e07def3bd51bf575c6a3180 100644 GIT binary patch delta 20 acmaE6@yvqzG%qg~0}wngP};~HEdu~VB?X)S delta 20 acmaE6@yvqzG%qg~0}vEklibK1Edu~V%mv8+ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/inspect.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/inspect.cpython-312.pyc index f4476d0e9501ba82e18c781e127b359134fe882f..2ce29836459e148619e5c52a2909973871cf1dad 100644 GIT binary patch delta 20 acmew?_gRknG%qg~0}wngP}<0y!4CjJcm;w0 delta 20 acmew?_gRknG%qg~0}vEklibLi!4CjK9|f-f diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/install.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/install.cpython-312.pyc index d782f07554ae75b984ce295fc133c6b966f3cd70..ebc45ab54227e9a36af1596cc96388d3921e4b12 100644 GIT binary patch delta 22 ccmaF$g7MW0M()$Ryj%=G@W4Q6BX@iW0AqRwVgLXD delta 22 ccmaF$g7MW0M()$Ryj%=GP;gChBX@iW0AwKtj{pDw diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/list.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/list.cpython-312.pyc index ced13fd94e44c0a0c81c677ae437514b9b940063..13580e00c2482b989fa3df5dc04d37f7452e4364 100644 GIT binary patch delta 22 ccmeBbW$bEYbem0!@G%qg~0}wngP}<0SNE-k}s0JPY delta 20 acmX>bem0!@G%qg~0}vEklibLCNE-k~PX diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/uninstall.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/commands/__pycache__/uninstall.cpython-312.pyc index df86c4816a2f1d0fab5a0a34ed302ea8cb40c6fd..0a644edb00dd80b8bacc876ffb6bcc152f6d25e1 100644 GIT binary patch delta 20 acmaE&@G%qg~0}wngP}<17h8X}h&;=9# delta 20 acmdnazMY->G%qg~0}vEklibL?h8X}icLhNJ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/distributions/__pycache__/base.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/distributions/__pycache__/base.cpython-312.pyc index cf0ed9861cd2e28ea6aa35ae07317da6444e0f97..b0b6c35750a0d693ab5bb2cc947244eb3138d8b4 100644 GIT binary patch delta 20 acmew;_EC)cG%qg~0}wngP}<0y#ti^L1O;dS delta 20 acmew;_EC)cG%qg~0}vEklibLi#ti^Ls|A$+ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/distributions/__pycache__/installed.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/distributions/__pycache__/installed.cpython-312.pyc index 0a2f672b54d5c68b2066beb949e5632bfc74d057..5356946dac852805573cbdaca2334902c4ca782b 100644 GIT binary patch delta 20 acmey&`dChpU`yj%=G@W4Q6Blol=08`Tj`v3p{ delta 22 ccmZ2@l4;>dChpU`yj%=GP;gChBlol=091MhC;$Ke diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/index/__pycache__/sources.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/index/__pycache__/sources.cpython-312.pyc index 8c50ead692dfd0b36b75dcde4c5ed941f590f4d1..3d6a7fe4ffaa2f9ce1b16a98780154e0d27d3f22 100644 GIT binary patch delta 20 acmZ3Rur`7FG%qg~0}wngP}<0?WB>p_@dZ@? delta 20 acmZ3Rur`7FG%qg~0}vEklibLyWB>p`m<56W diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/locations/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/locations/__pycache__/__init__.cpython-312.pyc index 18961d6a9efc4b7e8d8012c98b09ab32ab21fd32..098d9463b6de563d2d497514bbb494f480b13aee 100644 GIT binary patch delta 20 acmca#e!raiG%qg~0}wngP}<0S#To!j2nM78 delta 20 acmca#e!raiG%qg~0}vEklibLC#To!juLjWo diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/locations/__pycache__/_distutils.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/locations/__pycache__/_distutils.cpython-312.pyc index 084300fdbc1769acd254d8e36519717f8868bb20..c4f8996b2490e5c3e475e949424e5c812fd4b534 100644 GIT binary patch delta 20 acmZoLZ7}6N&CAQh00a*Vls0mgN&x^g#{}X4 delta 20 acmZoLZ7}6N&CAQh00afsBsX%GN&x^hZUqkj diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/locations/__pycache__/_sysconfig.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/locations/__pycache__/_sysconfig.cpython-312.pyc index 296f3d486ba15aadd82743e174c1753f730764eb..724d169a7e5abe6f45e95af7d102a2c36422d514 100644 GIT binary patch delta 20 acmexw``?!PG%qg~0}wngP}<1-MGgQ-!v=Ez delta 20 acmexw``?!PG%qg~0}vEklibMtMGgQ;Y6hSH diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/locations/__pycache__/base.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/locations/__pycache__/base.cpython-312.pyc index cc15955631cd6f59bd77df0b7075e816bb9776c5..87d44bae3cc48639ff3899266ea59f804ae3b2b7 100644 GIT binary patch delta 20 acmZpZZIk6b&CAQh00a*Vls0l#^8o-dO9Z$8 delta 20 acmZpZZIk6b&CAQh00afsBsX$b^8o-d@&x4o diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/metadata/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/metadata/__pycache__/__init__.cpython-312.pyc index bae40aba916031aeed842b160d8f80e2e116556c..42cc0ba32689546d4a3e5c34c690ce8e7ce8e891 100644 GIT binary patch delta 20 acmZoLZ7}6N&CAQh00a*Vls0mgN&x^g#{}X4 delta 20 acmZoLZ7}6N&CAQh00afsBsX%GN&x^hZUqkj diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/metadata/__pycache__/_json.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/metadata/__pycache__/_json.cpython-312.pyc index 0239300fd40596343e001701336e8300e453f58c..dc837b2b1f61e5b0b1aa87b2fa24fd6addc19efb 100644 GIT binary patch delta 20 acmX>uc3h16G%qg~0}wngP}<0C!wmpBbOh=E delta 20 acmX>uc3h16G%qg~0}vEklibK{!wmpC8wD2t diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/metadata/__pycache__/base.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/metadata/__pycache__/base.cpython-312.pyc index 936092864a43c6ea1db120a97c476d963c30a846..b91e63392f230b383c78486fb61966cddaf7b10e 100644 GIT binary patch delta 22 ccmZ49%e1_giTgA!FBbz4JTOq&$UUbG085hwcK`qY delta 22 ccmZ49%e1_giTgA!FBbz46kLG%qg~0}wngP}<1N#s~l}Dg+h) delta 20 acmbQpG?9t>G%qg~0}vEklibM7#s~l}(F8*P diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/models/__pycache__/candidate.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/models/__pycache__/candidate.cpython-312.pyc index ffeef5ba06e08ed291027f1736d4b95847df1aa1..5c9da878b2afeb95ff14b2059d43581a0e5e8dfd 100644 GIT binary patch delta 20 acmX@dbB>4mG%qg~0}wngP}<1t%mx5D9|YzA delta 20 acmX@dbB>4mG%qg~0}vEklibMd%mx5D#sw1q diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/models/__pycache__/direct_url.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/models/__pycache__/direct_url.cpython-312.pyc index 960fa94b9a9e0fe8c8ec0abda1217690011a2340..6602847a4c18855e29fd40baefaa54def26fc99e 100644 GIT binary patch delta 20 acmeAP>IvdL&CAQh00a*Vls0lRY61W`$^_2< delta 20 acmeAP>IvdL&CAQh00afsBsX$1Y61W{aRmGT diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/models/__pycache__/format_control.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/models/__pycache__/format_control.cpython-312.pyc index 8bf49db6c7e791ae17f3a38b06cf4d69efa47db8..dc498d1548f5e5c95dd1e43dddcbbfb87683e7ab 100644 GIT binary patch delta 20 acmZ3buu6gZG%qg~0}wngP};~XF8}~HSp=*A delta 20 acmZ3buu6gZG%qg~0}vEklibKHF8}~I00h|p diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/models/__pycache__/index.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/models/__pycache__/index.cpython-312.pyc index f396668c4ef9bcb4cf75af607381d2875d583c48..c1ff97abf2dbedf1a5476a919fdc93a21917b52c 100644 GIT binary patch delta 20 acmZ3%yMmYdG%qg~0}wngP};~nmkj_lQv~(^ delta 20 acmZ3%yMmYdG%qg~0}vEklibKXmkj_l`UN8Z diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/models/__pycache__/installation_report.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/models/__pycache__/installation_report.cpython-312.pyc index 16b10270e50509267a2d01e802979eb7c80d69eb..cd343a3421d7f203e159c486a29a9ca4d64d0afe 100644 GIT binary patch delta 20 acmew^_+60uG%qg~0}wngP}<1-h64aXJq7Im delta 20 acmew^_+60uG%qg~0}vEklibMth64aX delta 20 acmaD6^&*PM()$Ryj%=G@W4Q6BR8uz07tb2hX4Qo delta 22 ccmbO*jd8*>M()$Ryj%=GP;gChBR8uz07zT~v;Y7A diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/network/__pycache__/utils.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/network/__pycache__/utils.cpython-312.pyc index e85d836873a0e1d5b2536aaacd69324f8501488a..70c8285d4a003f1c5d2c6411bd40590d8d431fb2 100644 GIT binary patch delta 20 acmcaDcw3PBG%qg~0}wngP}<0Sfdc?N-36Hd delta 20 acmcaDcw3PBG%qg~0}vEklibLCfdc?OgayU` diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/network/__pycache__/xmlrpc.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/network/__pycache__/xmlrpc.cpython-312.pyc index 193f4103c814d09ac93d77cd083579956cd19e8a..3c0fc6aa75ebde7b410b9fe83246b21025d5812c 100644 GIT binary patch delta 20 acmew(_D78SG%qg~0}wngP};~{$PEBOyakB> delta 20 acmew(_D78SG%qg~0}vEklibK%$PEBPV+FPV diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/operations/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/operations/__pycache__/__init__.cpython-312.pyc index 8aca33805b6926417f5e0b239855a7cbfdb80b86..8f26e35328ad1b4a453cf10e1c27f0b1f07632dd 100644 GIT binary patch delta 19 ZcmX@lc%G5_G%qg~0}wngP@2en1OPM$1uXyo delta 19 ZcmX@lc%G5_G%qg~0}vEklbpzX1OPOG1zG?A diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/operations/__pycache__/check.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/operations/__pycache__/check.cpython-312.pyc index 449cb9e4ede356d88ec02534385853d3dc47556b..04aeb8237d54f9eeec69b7656d631c05b55f33fa 100644 GIT binary patch delta 20 acmdmBvB84-G%qg~0}wngP}<0?DgyvJwFLSA delta 20 acmdmBvB84-G%qg~0}vEklibLyDgyvKTm>fp diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/operations/__pycache__/freeze.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/operations/__pycache__/freeze.cpython-312.pyc index 0dc6c7f691e21079d68afaa3bf96e3aa622a520b..5c2b33307d77a2adf08969764ae03ce8a6fc9ea6 100644 GIT binary patch delta 20 acmbOfFe!lhG%qg~0}wngP}<1Nt^oi#aRl4| delta 20 acmbOfFe!lhG%qg~0}vEklibM7t^oi$7zGIc diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/operations/__pycache__/prepare.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/operations/__pycache__/prepare.cpython-312.pyc index dabc11e3ee112f05406c537a39686a0997b2d29f..31c23e13cf1b9fe9d55d5d62259f903975d2afa0 100644 GIT binary patch delta 22 ccmZ2~fpPr>M()$Ryj%=G@W4Q6BezNh09D-vbpQYW delta 22 ccmZ2~fpPr>M()$Ryj%=GP;gChBezNh09J$sq5uE@ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/operations/install/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/operations/install/__pycache__/__init__.cpython-312.pyc index 47ee022a5a4709c6b2291ae5a5881b51a30275b2..6000dbe7a2921bc6331dcbf0d5f16d29f0a7f43b 100644 GIT binary patch delta 20 acmbQpG?9t>G%qg~0}wngP}<1N#s~l}Dg+h) delta 20 acmbQpG?9t>G%qg~0}vEklibM7#s~l}(F8*P diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/operations/install/__pycache__/wheel.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/operations/install/__pycache__/wheel.cpython-312.pyc index 45e1340a6f384c6fdc7141598e539b8a7ede4a9f..ceccc6dedb2eb4a7902b6bca5c2ccf5d2bf1731a 100644 GIT binary patch delta 22 ccmdnk&9t$biTgA!FBbz4JTOq&$i1u;08H`*kN^Mx delta 22 ccmdnk&9t$biTgA!FBbz46kL diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/req/__pycache__/req_install.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/req/__pycache__/req_install.cpython-312.pyc index e4f970845d61577abf02076dddc4c44cee7cf4e0..3c8b189f4ca397df3459ff8cc1c5fbd88ab75b31 100644 GIT binary patch delta 22 ccmey|$Mm_6iTgA!FBbz4JTOq&$eqy+09Sto-T(jq delta 22 ccmey|$Mm_6iTgA!FBbz46kLpF delta 22 ccmbRKjB)xiM()$Ryj%=GP;gChBR5Ym09D}zkpKVy diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/__pycache__/factory.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/__pycache__/factory.cpython-312.pyc index cf01af79b9e832eadcb630164240d460786dffcf..a0f193fa012cb26054bc42943939fd8e30ffb954 100644 GIT binary patch delta 22 ccmdnp!L+-BiTgA!FBbz4JTOq&$ZgO908J_eCjbBd delta 22 ccmdnp!L+-BiTgA!FBbz46kLH+{dG6g{Z delta 20 acmZpOZiwbS&CAQh00afsBsX%G>H+{d*#&L@ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/__pycache__/reporter.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/__pycache__/reporter.cpython-312.pyc index 5bf22dcacd748b7a75e8fefff6383d8b307a8fe6..54dcb9e115cb13561ff3c84019563e546f22001a 100644 GIT binary patch delta 20 acmdn4yIq(2G%qg~0}wngP}<17MhpNvI|Xw9 delta 20 acmdn4yIq(2G%qg~0}vEklibL?MhpNv;su}p diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/__pycache__/requirements.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/__pycache__/requirements.cpython-312.pyc index 68c1d939a34b911431b5ba92c4832d13e763edd4..5d1535a172e38a710a2920798f94531e915da22f 100644 GIT binary patch delta 20 acmZ2gysDV{G%qg~0}wngP};~n-x2^sE(QAl delta 20 acmZ2gysDV{G%qg~0}vEklibKX-x2^s)dna4 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/__pycache__/resolver.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/__pycache__/resolver.cpython-312.pyc index 7caa37215a073e1ac157e22dd037298531da2f68..6f321e2a2816c57b7f44a7c1933579397dc69b25 100644 GIT binary patch delta 20 acmeyE@G*h=G%qg~0}wngP}<0yW&i+7kp>|E delta 20 acmeyE@G*h=G%qg~0}vEklibLiW&i+8I0jAt diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/__init__.cpython-312.pyc index 7caabe29abe82f262ba4fa76d44f013942598fee..0af4121cc826f131e92701fa9a38d6eb406514a1 100644 GIT binary patch delta 19 ZcmX@bc#4txG%qg~0}wngP@2fS9{@8?1s(tZ delta 19 ZcmX@bc#4txG%qg~0}vEklbp!C9{@AS1xo+` diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/_jaraco_text.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/_jaraco_text.cpython-312.pyc index fac6b56660d3f94dbc3d4742760fa29dd788e0ee..a575fc301923019d87549d298673eb13a63ecdf0 100644 GIT binary patch delta 20 acmdm|yib|?G%qg~0}wngP}<17Sr7m_<^^v6 delta 20 acmdm|yib|?G%qg~0}vEklibL?Sr7m`jRl+l diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/_log.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/_log.cpython-312.pyc index 28564720d3029abdbd7bd38d6eba712c774d9e49..6f7ea42bb40abe4fbdcf7ebad4c35fc281c9dda0 100644 GIT binary patch delta 20 acmcb}cae|#G%qg~0}wngP}<1t#tr~F-~{vl delta 20 acmcb}cae|#G%qg~0}vEklibMd#tr~GhXo-3 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/appdirs.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/appdirs.cpython-312.pyc index c419e302a8678962b2f1cf0dcdf74ce33cc67c63..c2c2651898c46233684a23f195498ca99b208b7f 100644 GIT binary patch delta 20 acmbOyJWrVWG%qg~0}wngP};~nnG*mso&@Ir delta 20 acmbOyJWrVWG%qg~0}vEklibKXnG*mtMFkW9 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/compat.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/compat.cpython-312.pyc index 6d636c4a9f940c3296a3171f674dea177588219c..531b73ffe248c9ebc00b6223cc21f1b253d8ff28 100644 GIT binary patch delta 20 acmcaAepQ_NG%qg~0}wngP}<0SiW>ku$OV=F delta 20 acmcaAepQ_NG%qg~0}vEklibLCiW>kvZw12u diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/compatibility_tags.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/compatibility_tags.cpython-312.pyc index c9ea6dba1decd27603849241d191047a873c6425..bef63b7f8568b1591460208c9ac9110d8b081d8b 100644 GIT binary patch delta 20 acmexi{KJ_0G%qg~0}wngP}<1-RuTY5J_bbq delta 20 acmexi{KJ_0G%qg~0}vEklibMtRuTY5A&joe> delta 20 acmZ3Nvo44GG%qg~0}vEklibLyYz6>Bb_JsV diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/misc.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/misc.cpython-312.pyc index 02730b8f60052774071a216fad0c6e0b5f6e6c1b..551ed61bf15f984521a221526f18187d9ea05c2f 100644 GIT binary patch delta 22 ccmX^7pYia2M()$Ryj%=G@W4Q6BlnJa0AYCtrT_o{ delta 22 ccmX^7pYia2M()$Ryj%=GP;gChBlnJa0Ae5q(*OVf diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/packaging.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/packaging.cpython-312.pyc index 32540bb50476ae4fd2ca5ed91fe1522e7874cab9..a4d5cd54027331104781973a0c0db407adadfa52 100644 GIT binary patch delta 20 acmaFD_k@r8G%qg~0}wngP};~H$qoQL*aa;B delta 20 acmaFD_k@r8G%qg~0}vEklibK1$qoQMe+60q diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/retry.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/retry.cpython-312.pyc index ece68d019fc5a08b1312cb4b4a245115f7b35921..9acbdedb4c1f8552cf2167a2f811de58d9a22839 100644 GIT binary patch delta 20 acmdnZznh=?G%qg~0}wngP}<17fgJ!ilm#>Z delta 20 acmdnZznh=?G%qg~0}vEklibL?fgJ!jI|X3? diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/subprocess.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/subprocess.cpython-312.pyc index 14bd326203b47ff042bdb3a910b2785b83156836..7c9f51b01f4ec0e4020fc130311230964aa56a26 100644 GIT binary patch delta 20 acmccZblZvhG%qg~0}wngP};~Hpa=j%+69yV delta 20 acmccZblZvhG%qg~0}vEklibK1pa=j&fd#<; diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/temp_dir.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/temp_dir.cpython-312.pyc index 962f04423dd70a86b8c00425d5bf0b1343132bac..7469061387ac13eddd13abebf6d38d9dccd6e978 100644 GIT binary patch delta 20 acmdlKyD66YG%qg~0}wngP}<17Tn_+3O9k8j delta 20 acmdlKyD66YG%qg~0}vEklibL?Tn_+3@&*Y2 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/unpacking.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/unpacking.cpython-312.pyc index 0cd6ae34d89db2bf9301fb2c31cc5217c3ee43e0..04849962b19d8196309e261377906e8a41324763 100644 GIT binary patch delta 20 acmeA#=r7utI?QG%qg~0}wngP};~X%K-p1!32H) delta 20 acmZ1>utI?QG%qg~0}vEklibKH%K-p2XauVO diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/virtualenv.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/virtualenv.cpython-312.pyc index 47dfe26acf2d28cdd22cba20cb6177ecce221975..71331461112798ea1efafce317cc041e4d2cf776 100644 GIT binary patch delta 20 acmdm}v{8xsG%qg~0}wngP}<0?CI|pHx&+1m delta 20 acmdm}v{8xsG%qg~0}vEklibLyCI|pIVFdF4 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/wheel.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/utils/__pycache__/wheel.cpython-312.pyc index 6c817076cc82d56dcdb3698ae47e6dce277eb9ff..08aa47a14cb1cc10a933c5aa5de2a8bec34f0b07 100644 GIT binary patch delta 20 acmaE>`&O6xG%qg~0}wngP}<1-NDKf(s|FPS delta 20 acmaE>`&O6xG%qg~0}vEklibMtNDKf)QU*c* diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/vcs/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/vcs/__pycache__/__init__.cpython-312.pyc index 7cbc9068cb8fbbf3c2516e988ec7b8038f4f5358..8a473abd0f40ad197a0af9fa2461e7d0a3aab23b 100644 GIT binary patch delta 20 acmbQsGM9z>G%qg~0}wngP};~X!~_5_Qv^Q% delta 20 acmbQsGM9z>G%qg~0}vEklibKH!~_5_`UGqM diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/vcs/__pycache__/bazaar.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/vcs/__pycache__/bazaar.cpython-312.pyc index 32a0c28c7fdd18f03b7881160475875def690e5c..a438bf5a07e9ad713306b1abc1486156a8a800e3 100644 GIT binary patch delta 20 acmdm}u~CEjG%qg~0}wngP}<0?CISFCl?2rQ delta 20 acmdm}u~CEjG%qg~0}vEklibLyCISFDJOu&( diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/vcs/__pycache__/git.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/vcs/__pycache__/git.cpython-312.pyc index 394d7d1255317883826e50fe7ae78fbf9e96670a..3c9abe27635ce872ead71ddb6cdbc14f7f2165a6 100644 GIT binary patch delta 22 ccmcaHoAJhMM()$Ryj%=G@W4Q6BllTf09BF(>i_@% delta 22 ccmcaHoAJhMM()$Ryj%=GP;gChBllTf09H8%7ytkO diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/vcs/__pycache__/mercurial.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/vcs/__pycache__/mercurial.cpython-312.pyc index 7f0ad29fb40f8744946fbb1b332289af23f9641a..78b6b7f6f17a1e4068ac9228072e361bd6f270da 100644 GIT binary patch delta 20 acmca-bI*qRG%qg~0}wngP};~HEC&EXcLk0B delta 20 acmca-bI*qRG%qg~0}vEklibK1EC&EY9tFDq diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_internal/vcs/__pycache__/subversion.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_internal/vcs/__pycache__/subversion.cpython-312.pyc index 7bb41bba602a84ae907ef42c3ab006099c94a1a3..95444ba059055a4108ff87fc543b8c95b7948902 100644 GIT binary patch delta 20 acmdm!uqT20G%qg~0}wngP}<0CXaE31Nd#`HG%qg~0}wngP}<0CE(HKR^93dV delta 20 acmX?Pa>#`HG%qg~0}vEklibK{E(HKSngvq; diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/__pycache__/cache.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/__pycache__/cache.cpython-312.pyc index 2f9627f6765fdd74c0efeb0ff40cfd7d86cb4447..fcf2d987829c5d472498a5148af9ed8ed9d25bba 100644 GIT binary patch delta 20 acmaDO`$m@gG%qg~0}wngP}<1-kPiStI|b?h delta 20 acmaDO`$m@gG%qg~0}vEklibMtkPiSt;szH0 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/__pycache__/controller.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/__pycache__/controller.cpython-312.pyc index 8841cc4206f073e7dd514beb64c15e8493f9739c..891b4d2cdb065a7ef8e53805cb64e98e2166f466 100644 GIT binary patch delta 22 ccmcc6z<8m7k^3|+FBbz4JTOq&$nEL?08bDG5C8xG delta 22 ccmcc6z<8m7k^3|+FBbz46kLQ delta 20 acmdmDvc-h^G%qg~0}vEklibLyDFpyLY6U3( diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/__pycache__/serialize.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/__pycache__/serialize.cpython-312.pyc index 23a63c99863ca0b549cf178519da8131535a7269..fb42afd481a3d3c331c60e23ca8e15c4e44d7970 100644 GIT binary patch delta 20 acmbQEIY*QGG%qg~0}wngP};~nQ3L=taRnFv delta 20 acmbQEIY*QGG%qg~0}vEklibKXQ3L=u7zITD diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/__pycache__/wrapper.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/__pycache__/wrapper.cpython-312.pyc index e2f851b0ecc48e9ef12ab7658b9dc4ce49b642e0..c489a202382dbcb7e634bea29f92d2e6ecb40722 100644 GIT binary patch delta 20 acmbQrJC&FFG%qg~0}wngP}<1d#RdQ}TLirT delta 20 acmbQrJC&FFG%qg~0}vEklibMN#RdQ~0tD&+ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/caches/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/caches/__pycache__/__init__.cpython-312.pyc index 88dcbced2190eb030a9f73a4b093de67aee79080..cfcbfaf672fd25843fec0be791a4ee52d59b411a 100644 GIT binary patch delta 20 acmdnTypNgtG%qg~0}wngP}<17nGpatRs|mb delta 20 acmdnTypNgtG%qg~0}vEklibL?nGpat{RK<_ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/caches/__pycache__/file_cache.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/caches/__pycache__/file_cache.cpython-312.pyc index d0929b3f28891af0d0032117f3bd92a03ddc168d..a5bc3a39e3af02d3ab0ccfddaa22f49eb2e90494 100644 GIT binary patch delta 20 acmbPkKHZ%AG%qg~0}wngP}<1dBMksJs0Azl delta 20 acmbPkKHZ%AG%qg~0}vEklibMNBMksKPX$>3 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/caches/__pycache__/redis_cache.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/caches/__pycache__/redis_cache.cpython-312.pyc index 34c9034ff4b4c9cbe4ba70fa24af420cbe0cf3ce..3b3054eabc3116b2cf6ccb8f9c834e628d35043a 100644 GIT binary patch delta 20 acmdlhx>uC@G%qg~0}wngP}<17i3uC@G%qg~0}vEklibL?i3IA+3 delta 20 acmX@fbdrhtG%qg~0}vEklibK{#|Qv8kp$}i diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/certifi/__pycache__/__main__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/certifi/__pycache__/__main__.cpython-312.pyc index d73c0c18b59ae8521e287e45a17138a65caf6e80..be3bb9943d0eed9902b6251c79fdf34cb62664db 100644 GIT binary patch delta 20 acmbQhI)Rn@G%qg~0}wngP}<1d$^-x~Zv>hE delta 20 acmbQhI)Rn@G%qg~0}vEklibMN$^-y076iut diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/certifi/__pycache__/core.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/certifi/__pycache__/core.cpython-312.pyc index b72eaaef890c01ccbfb671b4dbebd37cf363c0f3..3824c58e3b492f0e41fa18a2f707744ba83eb4fd 100644 GIT binary patch delta 20 acmZ20uvUQkG%qg~0}wngP}<0?!~p;`i3E)R delta 20 acmZ20uvUQkG%qg~0}vEklibLy!~p;{Fa){) diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/dependency_groups/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/dependency_groups/__pycache__/__init__.cpython-312.pyc index b325d27efa19d114cc3cc36fab111f37b3fa2b7c..8b489485c0062884594422ad65c76428fb9db1d9 100644 GIT binary patch delta 20 acmZo+Zeiv=&CAQh00a*Vls0l#G6DcDMg(yH delta 20 acmZo+Zeiv=&CAQh00afsBsX$bG6DcD?F60x diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/dependency_groups/__pycache__/__main__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/dependency_groups/__pycache__/__main__.cpython-312.pyc index f72102aa8ba0a32b7b8046c621b72effd082eabb..957c0ac797580560226c091e9b677103d99e319a 100644 GIT binary patch delta 20 acmbOxI!%=OG%qg~0}wngP}<1d%>@86rv%gh delta 20 acmbOxI!%=OG%qg~0}vEklibMN%>@87P6Yt~ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/dependency_groups/__pycache__/_implementation.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/dependency_groups/__pycache__/_implementation.cpython-312.pyc index 67e781720590f4e6e10b56452b67f5d637998952..f4cacefa571b01851bf3d2010769964f5b149e32 100644 GIT binary patch delta 20 acmdnzz0aHbG%qg~0}wngP}<17Srq_5ng!DU delta 20 acmdnzz0aHbG%qg~0}vEklibL?Srq_6K?VQ- diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/dependency_groups/__pycache__/_lint_dependency_groups.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/dependency_groups/__pycache__/_lint_dependency_groups.cpython-312.pyc index ce9aaeedafcc316ce3465a5a4839ad9368b4c483..5f491cb654b457d23ecef2f30566b31cfaa7abdb 100644 GIT binary patch delta 20 acmdljwp)z*G%qg~0}wngP}<0CzzqO6lmx~A delta 20 acmdljwp)z*G%qg~0}vEklibK{zzqO7I|TCp diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/dependency_groups/__pycache__/_pip_wrapper.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/dependency_groups/__pycache__/_pip_wrapper.cpython-312.pyc index 10440d85e2012bea9a52f018dcbb67750801a2da..63180e82422b0b0187c4283b26b7fa8f950ae6c1 100644 GIT binary patch delta 20 acmew)^+}5RG%qg~0}wngP}<0y&I delta 20 acmZqRYT)8N&CAQh00afsBsX$@WdQ&%Oa&tV diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/compat.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/compat.cpython-312.pyc index 5280443a276195c99b5f08c725d154fae8625d81..fa0cd966e880b8c086e64b32c823086800e4019b 100644 GIT binary patch delta 22 ccmZ4eglY8?ChpU`yj%=G@W4Q6Be%jP09dRB-2eap delta 22 ccmZ4eglY8?ChpU`yj%=GP;gChBe%jP09jK93IG5A diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/resources.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/resources.cpython-312.pyc index e80e1ee534a96ce73eff1bfd03dac13679e56a3f..a0184701c64621cca55fc8f13cb18a57b23c4286 100644 GIT binary patch delta 22 ccmdnd&bXtUk^3|+FBbz4JTOq&$i3DX08JbQSpWb4 delta 22 ccmdnd&bXtUk^3|+FBbz46kL diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/idna/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/idna/__pycache__/__init__.cpython-312.pyc index 1aadbcaf67b4df8574c05fe4c45e8bfe033a7b3d..0b652dfee2d7a1fd7e9118d94f21c3b24a7d24d0 100644 GIT binary patch delta 20 acmZoVmjeKM)(6Z0 delta 28 icmeCY$=P|6llwF;FBbz46kL;RVmjeKOiwF4t diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/msgpack/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/msgpack/__pycache__/__init__.cpython-312.pyc index 5dea47e56a50932595f29678527d05002dbda760..582b4b4f159dabec6b9a4ee4ab0387f695482872 100644 GIT binary patch delta 20 acmcb{dySX-G%qg~0}wngP}<0SnhgLvg9U&9 delta 20 acmcb{dySX-G%qg~0}vEklibLCnhgLwDg~_o diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/msgpack/__pycache__/exceptions.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/msgpack/__pycache__/exceptions.cpython-312.pyc index c2f21a2a12509b3c4bb1aba52ef405f2608f4da3..dc26f44f7637181652fc68d309c46cf4f8bfecb5 100644 GIT binary patch delta 20 acmeyu|An9XG%qg~0}wngP}<1-k{tj+dX1vjmd> delta 20 acmdnVvXh1TG%qg~0}vEklibLy#{>X2S_HrV diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/_elffile.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/_elffile.cpython-312.pyc index faeb5e4753c754d295302f4ad5dc05d2f379ad51..67ed4c4df824d67678e846a4d73721c7563a4c5f 100644 GIT binary patch delta 20 acmZ3bzDk|@G%qg~0}wngP};~nUl;&6SOq-* delta 20 acmZ3bzDk|@G%qg~0}vEklibKXUl;&6{{?CQ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/_manylinux.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/_manylinux.cpython-312.pyc index c2b840c7f9ce08b0862d672803bfd830e47146a3..fa33a35c2476fab0ba7b3111b9e4c416e6e6924d 100644 GIT binary patch delta 20 acmZ4Fv&e`0G%qg~0}wngP};~Xt_A=+Uj+gH delta 20 acmZ4Fv&e`0G%qg~0}vEklibKHt_A=-1_dtw diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/_musllinux.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/_musllinux.cpython-312.pyc index 5b9b4b4f8467db68433d7f7e145955088a652b61..292078470fa35e216b2fd3705997f39ad7cc2985 100644 GIT binary patch delta 20 acmaE&{6v}iG%qg~0}wngP}<0SQxE__)&=1J delta 20 acmaE&{6v}iG%qg~0}vEklibLCQxE_`eFhEy diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/_parser.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/_parser.cpython-312.pyc index c6c8054dbdf7a3748bdd04788fb17c0d7426674d..127d83303764d2cab1d4cbc1a25c5cdc39966166 100644 GIT binary patch delta 20 acmdm)yEB*jG%qg~0}wngP}<17&I|xWs|FbW delta 20 acmdm)yEB*jG%qg~0}vEklibL?&I|xXQU*o< diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/_structures.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/_structures.cpython-312.pyc index ce640db75f77c888c3829e4fe335906104e2ca11..a03654b329572032e9d1e56ac014dff67f8b4556 100644 GIT binary patch delta 20 acmdlYxkZxuG%qg~0}wngP}<17k_P}d=><3d delta 20 acmdlYxkZxuG%qg~0}vEklibL?k_P}ekOgG` diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/_tokenizer.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/_tokenizer.cpython-312.pyc index a3df5821e860d3cbd2764edb435ee2328cbb9953..270daa652fac8c33fc00e7313fbc387d6c03f181 100644 GIT binary patch delta 20 acmbPkH{Fi=G%qg~0}wngP}<1NBM$&Ls06+M delta 20 acmbPkH{Fi=G%qg~0}vEklibM7BM$&MPXy}# diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/markers.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/markers.cpython-312.pyc index 2f309ea37793b5b51e1b379b2bfaa748625f4cd1..bbd2a5540cb233fb25832280df3ce01ca7115a21 100644 GIT binary patch delta 20 acmaEn{34nAG%qg~0}wngP}<0S*AM_pmIjRg delta 20 acmaEn{34nAG%qg~0}vEklibLC*AM_qJqEe} diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/metadata.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/metadata.cpython-312.pyc index c99997be91fee8d2276274b31d017c9739fcd99a..93cc7ff315ce12cae99f818f8a5684ae3e735cbc 100644 GIT binary patch delta 22 ccmex+h4JqdM()$Ryj%=G@W4Q6BX?020ArO0X#fBK delta 22 ccmex+h4JqdM()$Ryj%=GP;gChBX?020AxG|mH+?% diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/requirements.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/requirements.cpython-312.pyc index c2eb970654d17b5d58ce4344600dd670ef503a91..c0e99597dc92512b22db321c99deda0e9b4ce4df 100644 GIT binary patch delta 20 acmX@AbX1A^G%qg~0}wngP}<0CB?tgJ+64jt delta 20 acmX@AbX1A^G%qg~0}vEklibK{B?tgKfdwxB diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/specifiers.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/specifiers.cpython-312.pyc index 597b255b8e1e72e3c06fc9c265e00851a5791a99..05ca010a032befb9d07d25e97a172e85a28fd1d0 100644 GIT binary patch delta 22 ccmZ3yk!kTpChpU`yj%=G@W4Q6BlnCM08n)X#Q*>R delta 22 ccmZ3yk!kTpChpU`yj%=GP;gChBlnCM08tzU@&Et; diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/tags.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/tags.cpython-312.pyc index 0d6aba310e318f6594d6e54509fc680ac7bdff38..538d8d2d1f9c9eb1a5aa7a1531ba989dfc5ef694 100644 GIT binary patch delta 22 ccmex;kn#6HM()$Ryj%=G@W4Q6Blm{{0AWxEvj6}9 delta 22 ccmex;kn#6HM()$Ryj%=GP;gChBlm{{0AcqB-~a#s diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/utils.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/utils.cpython-312.pyc index 8635b546fd3ade1dd2a389e5be58cb6ccf45bc47..42bde423376cec18cfeca2ef4b5cd630dafc2442 100644 GIT binary patch delta 20 acmexn{LPsAG%qg~0}wngP}<1-S`q+8?gl>q delta 20 acmexn{LPsAG%qg~0}vEklibMtS`q+9l?H48 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/version.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/__pycache__/version.cpython-312.pyc index 86159d360469103804fccc6f0985067b3bda9712..641aa7b33f0226716735a5ed554e288fce7f3a1d 100644 GIT binary patch delta 21 bcmeBPz}UTjk^3|+FBbz4JTOpN$Q=LxLih!N delta 21 bcmeBPz}UTjk^3|+FBbz46kL;B$Q=LxLvjVK diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/licenses/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/licenses/__pycache__/__init__.cpython-312.pyc index 575a219952bb8f5507fac17a0c40153069e6a999..04633ecfb0ce2338fecb03541c20a14481eb92b9 100644 GIT binary patch delta 20 acmZ3ZuttIVG%qg~0}wngP}<0?C;$LAsRXhB delta 20 acmZ3ZuttIVG%qg~0}vEklibLyC;$LBPz2uq diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/licenses/__pycache__/_spdx.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/packaging/licenses/__pycache__/_spdx.cpython-312.pyc index db229955d399ad62e01df6e587138a9763a1f4c8..b9cdaefc7f8f54c23a246ff3592f0fc8c3c9b673 100644 GIT binary patch delta 22 ccmeDG#MJ+ZiTgA!FBbz4JTOq&$j!1709GLetN;K2 delta 22 ccmeDG#MJ+ZiTgA!FBbz46kLZ delta 20 acmdnZznh=?G%qg~0}vEklibL?fgJ!jI|X3? diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/__pycache__/android.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/__pycache__/android.cpython-312.pyc index 2661e6fa6bc3b4938500b45af5070fe891788614..356de440cb7ade1e5f18e9240e6d13f0648b5c9d 100644 GIT binary patch delta 20 acmcZ;=F8 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/__pycache__/macos.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/__pycache__/macos.cpython-312.pyc index 528c5f0e7ee29a712869c2b0ed75bee3e554ee4b..cc4fb52ebdb43f35d601bfd9a8e6132f081fc524 100644 GIT binary patch delta 20 acmdnyw#|+EG%qg~0}wngP}<0?tqcG@qy;Yk delta 20 acmdnyw#|+EG%qg~0}vEklibLytqcG^O9fm2 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/__pycache__/unix.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/__pycache__/unix.cpython-312.pyc index 5035c8fa1aaca8fd9f33860eb0b7683d4e6b666b..7100b4d5efc6ee39b1f845e3eff4db85032b3578 100644 GIT binary patch delta 20 acmZ2lytJ76G%qg~0}wngP};~n%Mt)XW(Dj3 delta 20 acmZ2lytJ76G%qg~0}vEklibKX%Mt)Y4F(wi diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/__pycache__/version.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/__pycache__/version.cpython-312.pyc index 045739eb1916cdd7e3ec0245a1f31a1878705c5d..d723c2490e377cc779443e773e539c6658a0de7a 100644 GIT binary patch delta 20 acmZ3-wvLVaG%qg~0}wngP}<0?%nSfDLjb0# diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/__pycache__/windows.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/__pycache__/windows.cpython-312.pyc index 172386cc2f28cea5ad9e46b53f749d01c570e48c..6d9d8693a96d2909949299d09cf1f7aaf95ab22a 100644 GIT binary patch delta 20 acmey8^(Bk@G%qg~0}wngP}<0yX$k;Mb_PQL delta 20 acmey8^(Bk@G%qg~0}vEklibLiX$k;N9R_d! diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/__init__.cpython-312.pyc index 097bec2ce4eb1e37729fa60d28f745627bad5d00..5e03fadf4dca5de3c89036895ab710ca9c65bd13 100644 GIT binary patch delta 20 acmZ1@y+)e*G%qg~0}wngP}<17kQV?q@&zRT delta 20 acmZ1@y+)e*G%qg~0}vEklibL?kQV?rnFUe+ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/__main__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/__main__.cpython-312.pyc index f7ed307ded002ccdf146508b5f3548e43342bfb8..bfb2cc257c73488fc7f5c6840b2e62fa700b2227 100644 GIT binary patch delta 20 acmaFG`ihnNG%qg~0}wngP}<0Sp9ugye+8%j delta 20 acmaFG`ihnNG%qg~0}vEklibLCp9ugzCI!_1 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/console.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/console.cpython-312.pyc index d99890af870b3755cc274f7e7efe55eda636e3f5..ab73e8fdc4332540d19fc188ea8fc54c49c53b4d 100644 GIT binary patch delta 20 acmcaAa#e);G%qg~0}wngP}<1t#RUL7TLljQ delta 20 acmcaAa#e);G%qg~0}vEklibMd#RUL80tGw( diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/filter.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/filter.cpython-312.pyc index f91a136c292321cddd1170cbb19f64a9b62c4724..1becadd190e092122140cf3048a5783679dd2219 100644 GIT binary patch delta 20 acmZ1`xlEG#G%qg~0}wngP};~nn+E_kxdjdY delta 20 acmZ1`xlEG#G%qg~0}vEklibKXn+E_lU diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/formatter.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/formatter.cpython-312.pyc index 996f15b4c867f5ab872b566e7b1f0fec38e91d1e..369d16de1f08a8010a8eba101876e49e96c78cec 100644 GIT binary patch delta 20 acmeyP@<)aHG%qg~0}wngP};~{CvzCYZG%qg~0}wngP}<0?#0CH~_XL9g delta 20 acmZ3>vzCYZG%qg~0}vEklibLy#0CI0o&>M} diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/plugin.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/plugin.cpython-312.pyc index df18d9f5d4364633e14169e3a6a86255bff42686..5f9591e23bca2d5c85de3d7d8c20444a48f5211d 100644 GIT binary patch delta 20 acmca0azTXqG%qg~0}wngP}<1t$^`&BlLY_( delta 20 acmca0azTXqG%qg~0}vEklibMd$^`&CIt48N diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/regexopt.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/regexopt.cpython-312.pyc index b41fc7bad404393a7b1052e78b54b834a31aa7bf..1526b58456c403de8da90f5aa72bc2cb36990201 100644 GIT binary patch delta 20 acmew>|5u*-G%qg~0}wngP}<1-i5~z(ECwO~ delta 20 acmew>|5u*-G%qg~0}vEklibMti5~z((*{of diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/scanner.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/scanner.cpython-312.pyc index 93fc2202ccbd0c0e920dad835366139e73bee1ab..60eed60bab9256bc224c5edd78ec0cd68891f740 100644 GIT binary patch delta 20 acmZ3Wx delta 20 acmX?Ua?*tRG%qg~0}vEklibK{Cj|gMvIS@W diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/token.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/token.cpython-312.pyc index 60028b428749b27b4ac82f796d7e0d5fb9ace9b5..63a7e2e9b2e1a8be1fa3c5542ebeb1fa0e698789 100644 GIT binary patch delta 20 acmeBk=yTvc&CAQh00a*Vls0lRD*ymDi3F$s delta 20 acmeBk=yTvc&CAQh00afsBsX$1D*ymEFa*^A diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/unistring.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/__pycache__/unistring.cpython-312.pyc index d2ae7e6381ca487365bfb70da5161be4d1418c20..2fa974ec8651beb5c368965cd5430e4d980dad4f 100644 GIT binary patch delta 22 ccmez0$n?LFiTgA!FBbz4JTOq&$o-`O09b(sVE_OC delta 22 ccmez0$n?LFiTgA!FBbz46kL(App&CAQh00a*Vls0m+m;(SlKm{HE delta 20 acmeCr>(App&CAQh00afsBsX%im;(Sl=LJgu diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/filters/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/filters/__pycache__/__init__.cpython-312.pyc index 7c95091072180c77aa3bd39b5375e9cdea08d7bc..20da6aed3326397a9e8f20b8ac2d90982b038e74 100644 GIT binary patch delta 22 ccmaF9g6a7RChpU`yj%=G@W4Q6BX`Uc09k$p`v3p{ delta 22 ccmaF9g6a7RChpU`yj%=GP;gChBX`Uc09qvnC;$Ke diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/__pycache__/__init__.cpython-312.pyc index 1707812b7f0af7da131ea27d0aa437b18d64d204..c9b55d915acdadc37fc93f605e8ea93e55f26c7c 100644 GIT binary patch delta 20 acmdmLw$+ULG%qg~0}wngP}<0?B@F;PGzA0z delta 20 acmdmLw$+ULG%qg~0}vEklibLyB@F;P+XXQI diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/__pycache__/_mapping.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/__pycache__/_mapping.cpython-312.pyc index d1dfeb97fb568ae2697e6e7fb0cd84a7750849fd..318510f049a80058fb63fdb04e8b671c19191166 100644 GIT binary patch delta 20 acmZoxY*yqx&CAQh00a*Vls0l#2mk;wWdykZ delta 20 acmZoxY*yqx&CAQh00afsBsX$b2mk;x3K5&!@I diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/styles/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/styles/__pycache__/__init__.cpython-312.pyc index 30352d13f2a3a96abd8c7ce44066b0cbbffb373f..18d9aa31b3265ba18a78b3e58ecb039232030cf4 100644 GIT binary patch delta 20 acmew@@>_)aG%qg~0}wngP};~{zy$z8O9g=d delta 20 acmew@@>_)aG%qg~0}vEklibK%zy$z8@&&E{ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/styles/__pycache__/_mapping.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pygments/styles/__pycache__/_mapping.cpython-312.pyc index 23dfc7801388b40c9e61872352cbdf60a2409691..1f7af8b9f2f797982cb008bbeb15a653926ec453 100644 GIT binary patch delta 20 acmX>jb4G^yG%qg~0}wngP}<1t$OiyC7X=6a delta 20 acmX>jb4G^yG%qg~0}vEklibMd$OiyCz6CV^ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/__pycache__/__init__.cpython-312.pyc index b4cc453a796be53aaa256fec1c75e6b19c69d0f3..b0a546edd3d0441ab8546bbe1565fa72a48ce9c3 100644 GIT binary patch delta 20 acmey)`kj^gG%qg~0}wngP}<1-h6w;bhXv07 delta 20 acmey)`kj^gG%qg~0}vEklibMth6w;cE(QDm diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/__pycache__/_impl.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/__pycache__/_impl.cpython-312.pyc index 492a5e3c03c8b4ddcd9a8108b3980293884c0454..bb54964541b92d8a6c8f5fff38563636ea746538 100644 GIT binary patch delta 22 ccmZ48%ecCik^3|+FBbz4JTOq&$i2V~0882iK>z>% delta 22 ccmZ48%ecCik^3|+FBbz46kL1QG%qg~0}wngP}<1dCISF8xditB delta 20 acmbQJIZ>1QG%qg~0}vEklibMNCISF9UjdNg delta 20 acmcb@a)pKaG%qg~0}vEklibMd$pip8k_8a} diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/_internal_utils.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/_internal_utils.cpython-312.pyc index aa90d8c93c391b40e946524e16ca188b44cd67e7..9391ac70df60af4aace62f7c1cab6274c8e1bf4b 100644 GIT binary patch delta 20 acmeyu|An9XG%qg~0}wngP}<1-k{tj+dM()$Ryj%=G@W4Q6Blp7`0Aha!!TM()$Ryj%=GP;gChBlp7`0AnTx?*IS* diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/api.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/api.cpython-312.pyc index b83f52354f654a0796d542ce64555208a5494bec..2a6d32f4b0d6e02d1563c382d618037838272daf 100644 GIT binary patch delta 20 acmdmBvB84-G%qg~0}wngP}<0?DgyvJwFLSA delta 20 acmdmBvB84-G%qg~0}vEklibLyDgyvKTm>fp diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/auth.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/auth.cpython-312.pyc index ffbd930412bdf3a07d44860d8d3013af59b86030..7142ce47021618a8e04de3a9d4d705c205ad9439 100644 GIT binary patch delta 20 acmaE#^FD|BG%qg~0}wngP}<0yVg>+A_Xart delta 20 acmaE#^FD|BG%qg~0}vEklibLiVg>+Bo(5(B diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/certs.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/certs.cpython-312.pyc index 54ea79590f98d9e2a66be72dd23daec1829a2d86..2437574d628a2f202c3095ec1f100e0d66855720 100644 GIT binary patch delta 20 acmdnQx`~zhG%qg~0}wngP}<17oCyFmzXbpQ delta 20 acmdnQx`~zhG%qg~0}vEklibL?oCyFnW(6$( diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/compat.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/compat.cpython-312.pyc index a09111665ad867402882ccf40b33ade9a9431cbf..fe34d49bd160c0309f71ea8d537848b93a404993 100644 GIT binary patch delta 20 acmX@hf0m#7G%qg~0}wngP}<0Sh#desWCdyf delta 20 acmX@hf0m#7G%qg~0}vEklibLCh#det3k8<| diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/cookies.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/cookies.cpython-312.pyc index 7f41a9874af9575162adfd0fd7033d1dc60ebc5f..a8ffae9f438ec5d3f104f3706b4bf78292897f9c 100644 GIT binary patch delta 22 ccmX?cl<~w-M()$Ryj%=G@W4Q6Blq4U09joJBme*a delta 22 ccmX?cl<~w-M()$Ryj%=GP;gChBlq4U09phGQ2+n{ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/exceptions.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/exceptions.cpython-312.pyc index a843db2853936e5f45719ed75824efe2851a25d0..820cf55b83fbc85e5530cf23aa9ecb8a1d6c89fe 100644 GIT binary patch delta 20 acmdmGy~~>WG%qg~0}wngP}<17UKRj8vIU<2 delta 20 acmdmGy~~>WG%qg~0}vEklibL?UKRj9Sq01h diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/help.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/help.cpython-312.pyc index 87f9918900169f937a55091312f83724169580d2..92ffdb310c4b5f0a82744c22f5b5051328da1021 100644 GIT binary patch delta 20 acmbQBI6;y7G%qg~0}wngP}<1dDgXd9Z3N%| delta 20 acmbQBI6;y7G%qg~0}vEklibMNDgXdA6a@_c diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/hooks.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/hooks.cpython-312.pyc index e93e9243167fa2c108e8bb1a1524337f58a914c3..a4e2ca7682f103889019ae69cf2a50920e66dc5d 100644 GIT binary patch delta 20 acmZ3%v4Vs9G%qg~0}wngP};~X%K`v1odjwC delta 20 acmZ3%v4Vs9G%qg~0}vEklibKH%K`v2L{ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/sessions.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/sessions.cpython-312.pyc index 5f2c8fc73871a6d1989ff80280840ed159dd5b9f..a30df361561026759a04f303a2b6635bb45aa032 100644 GIT binary patch delta 22 ccmex&lkwk8M()$Ryj%=G@W4Q6BlqVV0A+;-_y7O^ delta 22 ccmex&lkwk8M()$Ryj%=GP;gChBlqVV0A?%*B>(^b diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/status_codes.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/status_codes.cpython-312.pyc index 9bf18a8ac4d876eb4b1a10ca625c3e018499ca8f..e499c7a5382eff70874cde9010727a212728c0d5 100644 GIT binary patch delta 20 acmbQOKU<&sG%qg~0}wngP};~nK^y=$Bn2b@ delta 20 acmbQOKU<&sG%qg~0}vEklibKXK^y=$%LP#Y diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/structures.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/structures.cpython-312.pyc index 18fe133e3acdc3793db6c51573b0e54ade824517..0fda36a3a6bb22980b2be55d1edd7120a60951d0 100644 GIT binary patch delta 20 acmeyX{a2g&G%qg~0}wngP}<1-NfZD^;s!tf delta 20 acmeyX{a2g&G%qg~0}vEklibMtNfZD_i3V)| diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/utils.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/requests/__pycache__/utils.cpython-312.pyc index ea796cfcc5dec2cf32d5ad0e80fb88c00975c1fb..0b7a2d4eebba827f96638f347735635b2d70b76b 100644 GIT binary patch delta 22 ccmaDgi|N%YChpU`yj%=G@W4Q6BX@i+09V!q;Q#;t delta 22 ccmaDgi|N%YChpU`yj%=GP;gChBX@i+09bto4gdfE diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/__pycache__/__init__.cpython-312.pyc index e50c517fe4de80328648afdfeb37765a9e2370aa..199b3f7bd2396c2bf4c5f0b5c103b7886ef81e7f 100644 GIT binary patch delta 20 acmZo;ZDZv=&CAQh00a*Vls0l#GXVfD-voRB delta 20 acmZo;ZDZv=&CAQh00afsBsX$bGXVfEh6Jeq diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/__pycache__/providers.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/__pycache__/providers.cpython-312.pyc index 1fa77989edd62138ee2b567e7d52c55de73ff80d..002e7e933252e1e19e5c9d80a0354ffac1b01fef 100644 GIT binary patch delta 20 acmbR4Ki!}EG%qg~0}wngP}<1dqYeN(6a{Di delta 20 acmbR4Ki!}EG%qg~0}vEklibMNqYeN(y9Jd1 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/__pycache__/reporters.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/__pycache__/reporters.cpython-312.pyc index 86657bf2d3efa6fd62baa23d1fd06e6ce2110e13..b27122d09c9460acaf3194461edef36c448bec58 100644 GIT binary patch delta 20 acmaDV`BakoG%qg~0}wngP}<0Siw6Kekp;^D delta 20 acmaDV`BakoG%qg~0}vEklibLCiw6KfI0g6s diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/__pycache__/structs.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/__pycache__/structs.cpython-312.pyc index 835d94f4f3974211a700adf757e154403e2d8717..66bbc7fdc10d54aa405055b9c8a35ba9a556d967 100644 GIT binary patch delta 20 acmZ3NxGs_VG%qg~0}wngP}<17*Z=@RJq6kT delta 20 acmZ3NxGs_VG%qg~0}vEklibL?*Z=@R1O^`f delta 20 acmew(|3{wtG%qg~0}vEklibMtkskm>s|HK} diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/resolvers/__pycache__/resolution.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/resolvers/__pycache__/resolution.cpython-312.pyc index 27031dd0c4239b8dc04f75d06d06140dc146dbeb..45f8d22bf62c725ff9bf24d969385d5e2e8e36c7 100644 GIT binary patch delta 22 ccmcb0gz?r9M()$Ryj%=G@W4Q6Be#DN09!l;*Z=?k delta 22 ccmcb0gz?r9M()$Ryj%=GP;gChBe#DN09)e+1poj5 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/__init__.cpython-312.pyc index d2db6d07b9ada20d434cbd21b85fe9908153e765..b00538180a126680cd60ce3f645d8cf4a260ef6f 100644 GIT binary patch delta 20 acmext_Sua4G%qg~0}wngP}<0yAq@aUi delta 20 acmccRb<2zUG%qg~0}vEklibMduL=M~eFf40 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_cell_widths.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_cell_widths.cpython-312.pyc index 5bb972ac8883f3d6e66eb2cd650e5748b746e6eb..57992b68c65e80726f544aca2f58affe4bedde82 100644 GIT binary patch delta 20 acmX?Od&ZXgG%qg~0}wngP}<0SP!0e=%LUp1 delta 20 acmX?Od&ZXgG%qg~0}vEklibLCP!0e>as~$g diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_emoji_codes.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_emoji_codes.cpython-312.pyc index 4c9cf9ef8854752c0f094b39f969fd1292835b02..5e6ad96d656523f084e3040b8dcf69d6d4fdb3a3 100644 GIT binary patch delta 31 lcmZ2-l4r?D9`4h;yj%=G@W4Q+k-L?fv6Y)?D>w5@6#$j?2u=V1 delta 31 lcmZ2-l4r?D9`4h;yj%=GP;gDMk-L?fv6Y)?D>w5@6#$qr2zvkk diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_emoji_replace.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_emoji_replace.cpython-312.pyc index a9e4d9f5fa53c522fc0add4198974bbf1c6a322b..ab2c751668bbf9eab973eedc7274898b9f51cc33 100644 GIT binary patch delta 20 acmX@hdzP2`G%qg~0}wngP}<0Shz$Tc8wF(m delta 20 acmX@hdzP2`G%qg~0}vEklibLChz$Tc!Ud85 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_export_format.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_export_format.cpython-312.pyc index 1f04234d3cc6b7635edf6a177df5441e72b1be9d..a7f5b84a56b072b16e111a8e4203673e97361c8b 100644 GIT binary patch delta 20 acmdlfv{Q)tG%qg~0}wngP}<0?#|Z#8vIMpO delta 20 acmdlfv{Q)tG%qg~0}vEklibLy#|Z#9Sp?$% diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_extension.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_extension.cpython-312.pyc index 4e81483b555b69f9f2e7e06505190c8f68f9f776..dc2c05043dd11eb326f15c949b46bf0d14c8386d 100644 GIT binary patch delta 20 acmZ3=vXq7UG%qg~0}wngP};~X#RLE{lLS`) delta 20 acmZ3=vXq7UG%qg~0}vEklibKH#RLE|Is}9O diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_fileno.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_fileno.cpython-312.pyc index ef63b23b7904cdaa22464b24ab68f7feed66403a..af4d647a1f0def090bda351c64705ba09d39285e 100644 GIT binary patch delta 20 acmaFN_LzIB&U delta 20 acmZ2ww91J4G%qg~0}vEklibKHF9`rTkp%_- diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_spinners.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_spinners.cpython-312.pyc index 821b5487f30e99322a2a95da26edff4651ac774b..75f530773cd20b6021ca79957df71da6f773f81b 100644 GIT binary patch delta 20 acmZoqZ%^kw&CAQh00a*Vls0nL7y|%300nda delta 20 acmZoqZ%^kw&CAQh00afsBsX%`7y|%3rv;$^ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_stack.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_stack.cpython-312.pyc index 321d4c3cc9bb690b47cb0a3eadba3c332f79322f..e4b39fb37838d813eb85e6356f9fb3a07c7edd25 100644 GIT binary patch delta 20 acmcb}evzH~G%qg~0}wngP}<0Sj2Qqr;00m; delta 20 acmcb}evzH~G%qg~0}vEklibLCj2QqshXs!S diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_timer.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_timer.cpython-312.pyc index 06f961acb030040c097ca3adfaab26e584fc4512..a91dc9e5d5abf8b7ae5613f60528f9cc1655a2f4 100644 GIT binary patch delta 20 acmaFM_LhzNG%qg~0}wngP}<0y#0&sF90fE0 delta 20 acmaFM_LhzNG%qg~0}vEklibLi#0&sF!v$dg diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_win32_console.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_win32_console.cpython-312.pyc index 708fb62d207364de8dbbf433dd9a1e9f58aa8068..1dffca985c4ca8e70751dfb1521d83fd86dc11f0 100644 GIT binary patch delta 22 ccmbRKka7A$M()$Ryj%=G@W4Q6BX>^$09AAb*8l(j delta 22 ccmbRKka7A$M()$Ryj%=GP;gChBX>^$09G3Z1ONa4 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_windows.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_windows.cpython-312.pyc index db66afbaab9160a5915e130222893710cc84e84f..beebf0f85dba0b5b12209a15d03312fbdbaa581f 100644 GIT binary patch delta 20 acmX>md`y`8G%qg~0}wngP}<17n-c&!-UVL( delta 20 acmX>md`y`8G%qg~0}vEklibL?n-c&#g$0ZN diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_windows_renderer.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_windows_renderer.cpython-312.pyc index 9b5104ec7aed193f5d4cbd184549e8c3b5d8bf2f..3c88578e64031dc8c8cee2252963acc149a59fa2 100644 GIT binary patch delta 20 acmZpaX_VnU&CAQh00a*Vls0mI;{^aRUIiuq delta 20 acmZpaX_VnU&CAQh00afsBsX$@;{^aS1qD+8 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_wrap.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/_wrap.cpython-312.pyc index 6f17409ae55dbe13db2874d1535cbbaacc7803f0..136ba0a0b211196e320fc310b97470547f09fd2a 100644 GIT binary patch delta 20 acmbOtHARa1G%qg~0}wngP}<1N$qN884g^~O delta 20 acmbOtHARa1G%qg~0}vEklibM7$qN88wFHO& diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/abc.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/abc.cpython-312.pyc index 8d545d0b10abb584dd18a9713899f128d1fedd5a..a9846a3c4cc7db1dd9403832799f1d8120e020f3 100644 GIT binary patch delta 20 acmcb@bA^ZdG%qg~0}wngP}<1t$p!#B4+QuC delta 20 acmcb@bA^ZdG%qg~0}vEklibMd$p!#Bwgn{s diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/align.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/align.cpython-312.pyc index 3db2f2a242ad33d89c1c83ccdaa4bcfc190af156..5b2fee0ff4e72ac15a7c5bd78b16743b80cb8131 100644 GIT binary patch delta 20 acmZopXinfh&CAQh00a*Vls0nz&<6lKU2L;0b diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/bar.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/bar.cpython-312.pyc index 7ec0b608e1b942ccd6afa9357bff9f344c179ad7..564c21dfd1f8d78424cd6acf3ec25c1f0aaa82a0 100644 GIT binary patch delta 20 acmdm^xJQxuG%qg~0}wngP}<17Q2+otO$B5C delta 20 acmdm^xJQxuG%qg~0}vEklibL?Q2+ot^aYUs diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/box.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/box.cpython-312.pyc index 8957de3723cd82c5041561b4c6395043aa5092ea..cf4ae3cc17330df4106e3db1d22a7d070f5f8a2e 100644 GIT binary patch delta 20 acmX>deL9-^G%qg~0}wngP}<0SKoINbJ delta 20 acmX>deL9-^G%qg~0}vEklibLCKohX diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/default_styles.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/default_styles.cpython-312.pyc index 728b7d652195fe15cec8379a7d29868c9fadeca5..19e2dea0e5fa9985bc3cb0d67c2b6c0ccc21ee7e 100644 GIT binary patch delta 20 acmZ1zv?hrAG%qg~0}wngP}<0?s0jc*$^|a~ delta 20 acmZ1zv?hrAG%qg~0}vEklibLys0jc+aRpoe diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/diagnose.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/diagnose.cpython-312.pyc index 78457ed4e838b9e9542669f0f31541c4836f3313..26de12b43bed5238a22826c27f52e5eecc4a1397 100644 GIT binary patch delta 20 acmeyv{fC?TG%qg~0}wngP}<1-kre{#Ts)G%qg~0}wngP}<1-i5mb!2nG%S delta 20 acmew>{#Ts)G%qg~0}vEklibMti5mb!uLe5+ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/highlighter.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/highlighter.cpython-312.pyc index ea2e0937ae1317769a3daa11e689af1f48ad663a..351208cbe00b38b50810720533378fda0b055dfc 100644 GIT binary patch delta 20 acmdntyTh0JG%qg~0}wngP}<17Rt*3^^99BL delta 20 acmdntyTh0JG%qg~0}vEklibL?Rt*3_ng#O! diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/json.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/json.cpython-312.pyc index e30829337404874b67183a6a2b877f1bccbf42ef..d5d516454e3f7b536f8a65614ef95d6d721ce4ff 100644 GIT binary patch delta 20 acmbQQKVP5wG%qg~0}wngP};~nMH~P)#04+_ delta 20 acmbQQKVP5wG%qg~0}vEklibKXMH~P*YXw~Z diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/jupyter.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/jupyter.cpython-312.pyc index affedddbc9b976b73d22d58f66e470318382e3d6..27ea0a875034c5fd05c5a8e74fc1c8f577f14a5d 100644 GIT binary patch delta 20 acmaE=@l=ERG%qg~0}wngP};~HB?16JwFPki delta 20 acmaE=@l=ERG%qg~0}vEklibK1B?16KTm_y0 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/layout.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/layout.cpython-312.pyc index f68d8f53e51f52a8aad1f0036a1f8d4fe360e7b9..4da253e12c8f0e5fbfef18f76028e8bc2acc84f9 100644 GIT binary patch delta 22 ccmeB}$JjBCk^3|+FBbz4JTOq&$oG%qg~0}wngP}<1-QUU-*RR%Nw delta 20 acmexj_{EU>G%qg~0}vEklibMtQUU-*{03nF diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/padding.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/padding.cpython-312.pyc index aaa2219c24de51db7bed1dc03956aa020e3598b6..d682754f0564ce31b1e70c691ab313123bc55ae6 100644 GIT binary patch delta 20 acmZ2&w%UyQG%qg~0}wngP}<0?APoRIqy**w delta 20 acmZ2&w%UyQG%qg~0}vEklibLyAPoRJO9c}E diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/pager.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/pager.cpython-312.pyc index 35541c0eec5a646b0eb7d3d2957d79e8d57b82f1..129768a806a24a4345488373b55f6879d43d894b 100644 GIT binary patch delta 20 acmZ3%w}OxRG%qg~0}wngP};~X%MJiEcm#O> delta 20 acmZ3%w}OxRG%qg~0}vEklibKH%MJiF9|WcV diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/palette.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/palette.cpython-312.pyc index 9f9837f242db1c1edcaefc12642b073c89c34bb0..b63c45c08f1ea6835ecd1d92d2ec5ac3dbbc0e19 100644 GIT binary patch delta 20 acmX@7c}|o2G%qg~0}wngP}<0SSOfq+K?SY= delta 20 acmX@7c}|o2G%qg~0}vEklibLCSOfq+=mpyV diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/panel.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/panel.cpython-312.pyc index 5f78a256bad251bca8ebdea23a620e6482f38175..65f2a0921ea39d55d58f01d3a6b6c8b7e86103ee 100644 GIT binary patch delta 20 acmX?_d^DN+G%qg~0}wngP}<17%MbuY9tI=; delta 20 acmX?_d^DN+G%qg~0}vEklibL?%MbuY#RgFT diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/pretty.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/pretty.cpython-312.pyc index 105109c2d843f91848b59db7563373eb4a5c5f71..8c52d9eb1a9f428c8931c5d289ba06b484c4bdb5 100644 GIT binary patch delta 22 ccmX@Qm+9zUChpU`yj%=G@W4Q6BloU(09XYFNdN!< delta 22 ccmX@Qm+9zUChpU`yj%=GP;gChBloU(09dRCb^rhX diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/progress.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/progress.cpython-312.pyc index deabee30def8e1976664ddeaaaff347d00b8e13f..e6224e06b1900867cfb4470e75e12436df86eaa2 100644 GIT binary patch delta 25 fcmex=isk<)7Vgu$yj%=G@W4Q+k$WpQV~HvNcz_43 delta 25 fcmex=isk<)7Vgu$yj%=GP;gDMk$WpQV~HvNd9DZA diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/progress_bar.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/progress_bar.cpython-312.pyc index 857dcdfbcc59adc5c140e11d4d16a7ad96a31943..d457175aa1ae7fe2740a291de3a75c8031cb23a5 100644 GIT binary patch delta 20 acmZ1&xG<3WG%qg~0}wngP};~nO#=Wu?FEYf delta 20 acmZ1&xG<3WG%qg~0}vEklibKXO#=Wvlm)l| diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/prompt.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/prompt.cpython-312.pyc index ef84c26d07772a94f400f69c94c8681ff4b32687..29a77ee2514ffac61e1c1ab45c41eedd6b76b139 100644 GIT binary patch delta 20 acmeCI?X2ZK&CAQh00a*Vls0nL*#Q7Ra|N*g delta 20 acmeCI?X2ZK&CAQh00afsBsX%`*#Q7S8U@|} diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/protocol.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/protocol.cpython-312.pyc index 4a9684f724fa986b09ca8bf51dc5d70ccf73d054..23eb67f4cf2c882863243bd73748ad210a90ea49 100644 GIT binary patch delta 20 acmeC->*3=*&CAQh00a*Vls0lRvI77x7z8B% delta 20 acmeC->*3=*&CAQh00afsBsX$1vI77xzXVbM diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/region.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/region.cpython-312.pyc index 9c542b1e795ff26f8dfab94d14ae8a0b45543999..e8ccceefab1836a7df0e5608f90600dcb954805b 100644 GIT binary patch delta 20 acmX@ia+rntG%qg~0}wngP}<0C!2|#|LjIB{Z diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/repr.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/repr.cpython-312.pyc index dbea2850881f5e919486136eb4d9c03895f8320f..5921e6ad5e675e8d6df20dc58b8abc12c1f5fe6c 100644 GIT binary patch delta 20 acmaE3{KlC3G%qg~0}wngP}<1-P!a$|UIrlm delta 20 acmaE3{KlC3G%qg~0}vEklibMtP!a$}1qMz4 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/rule.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/rule.cpython-312.pyc index 1d506f0b1ec21218caec70e11ddd1bcc71ff1062..59df5cdf3f0f21902a418a194921385b6e4731d3 100644 GIT binary patch delta 20 acmdmDyv3ONG%qg~0}wngP}<17QW5|>o&|XT delta 20 acmdmDyv3ONG%qg~0}vEklibL?QW5|?MFpk+ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/scope.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/scope.cpython-312.pyc index 1fd8d1fd191b48052e396e825bb47489c2d0a916..a0a38b62e4a9eb0232e25c7a6fda9c8769d6511d 100644 GIT binary patch delta 20 acmZpYYm(zW&CAQh00a*Vls0mI=K}yT&jl_3 delta 20 acmZpYYm(zW&CAQh00afsBsX$@=K}yUb_H7i diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/screen.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/screen.cpython-312.pyc index 7ada7b8d7bf7873dc197f34b0f6f1873c2559060..de10ef2cc2a897bce94ebfc1fa0532983d943218 100644 GIT binary patch delta 20 acmX>gd_b7{G%qg~0}wngP}<17l@kCt?gdQ% delta 20 acmX>gd_b7{G%qg~0}vEklibL?l@kCul?8eL diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/segment.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/segment.cpython-312.pyc index b6bae75bd504207b3961fb617f36371c1545df2d..13b67f5c7eaf3ada791c733cf8e3ed8de84b9a63 100644 GIT binary patch delta 22 ccmdmdpK;;nm delta 20 acmX@0e?Xu6G%qg~0}vEklibL?RU805lLf#4 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/style.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/style.cpython-312.pyc index c6f140089d0ff6c8c441fdde1f241da3a23eee0c..7e1abbd1b24caf7c05a8b31eafad864b875cbaa8 100644 GIT binary patch delta 22 ccmZ47%CxwZiTgA!FBbz4JTOq&$UUP807c@h5!Hn delta 22 ccmZ2;fN9MEChpU`yj%=GP;gChBe&uL08{V=vj6}9 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/table.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/table.cpython-312.pyc index a35e9b9090506048dfb6ccbafce70cba61480397..b140502eb328042ec93727b12eb497249bf8b62f 100644 GIT binary patch delta 22 ccmex%jp^GpChpU`yj%=G@W4Q6BX{;{0Am~nn*aa+ delta 22 ccmex%jp^GpChpU`yj%=GP;gChBX{;{0As@k$N&HU diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/terminal_theme.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/terminal_theme.cpython-312.pyc index 765429cebc0defd70f90fabd90c74483f3c147cf..5f42af1da008eba79fe4650a6df93c1f565b9085 100644 GIT binary patch delta 20 acmZ1=wLpsdG%qg~0}wngP};~X$_oHA?gW1T delta 20 acmZ1=wLpsdG%qg~0}vEklibKH$_oHBl?1E+ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/text.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/text.cpython-312.pyc index f1ea584b7a6443dd20cbb1bbd20ac9d10aa064fd..45e6f43ac89506b8877ee5de3b854b1fc9f12f82 100644 GIT binary patch delta 22 ccmca~kNMI)X71Cxyj%=G@W4Q6Be(l|0A8^Ng#Z8m delta 22 ccmca~kNMI)X71Cxyj%=GP;gChBe(l|0AE-KvH$=8 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/theme.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/theme.cpython-312.pyc index 81c548f5f2e1d2efadbd1e28153727e2e3f1b772..cca5408c02b637c852759d214b4d2153e0980052 100644 GIT binary patch delta 20 acmX?Mc*2nTG%qg~0}wngP}<17R{{V(bp@~h delta 20 acmX?Mc*2nTG%qg~0}vEklibL?R{{V)90lC~ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/themes.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/themes.cpython-312.pyc index ee8f9a4d343ed3407a098e45d12680e68c4eeb89..999f82d4fe9ffcde8b12cc498e7e8ec943d105d8 100644 GIT binary patch delta 20 acmX@cbc~7nG%qg~0}wngP}<0C%?JQDas;&i delta 20 acmX@cbc~7nG%qg~0}vEklibK{%?JQE83f`0 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/traceback.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/traceback.cpython-312.pyc index 8ef04e964db3c58b3786f4027166f866ff2d7e55..b8b538e0639dd5a940bf55c60545b30a637483a4 100644 GIT binary patch delta 22 ccmbO;n`zE$ChpU`yj%=G@W4Q6BlpB!088Trc>n+a delta 22 ccmbO;n`zE$ChpU`yj%=GP;gChBlpB!08EMorT_o{ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/tree.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/rich/__pycache__/tree.cpython-312.pyc index 6fcb98053a7f163e7f55a32fb443c02b351ba05a..5037c7f6e26056672907232a7f3e3a41a7a27174 100644 GIT binary patch delta 20 acmZ1wvml21G%qg~0}wngP};~Xss{i*R|O;h delta 20 acmZ1wvml21G%qg~0}vEklibKHss{i*{smD0 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/tomli/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/tomli/__pycache__/__init__.cpython-312.pyc index 61e208e42478205b3dc710b3759969b31105794e..b142e3d426da26c7b1f67b6e82c70d05167ce7bd 100644 GIT binary patch delta 20 acmcb|bdQPqG%qg~0}wngP};~H%m@HGG6ekq delta 20 acmcb|bdQPqG%qg~0}vEklibK1%m@HG*##;9 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/tomli/__pycache__/_parser.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/tomli/__pycache__/_parser.cpython-312.pyc index d7f198e4253b71c6599de9b8764627649c75fe9f..b4a769924eec8b48dd2d77a0b5ff83bce1c35885 100644 GIT binary patch delta 22 ccmeBu#@PFek^3|+FBbz4JTOq&$jwv?08@+xMgRZ+ delta 22 ccmeBu#@PFek^3|+FBbz46kL<#2T&CAQh00a*Vls0lVX#fB^%>_>Y delta 20 acmeAT><#2T&CAQh00afsBsX$5X#fB_bOn3> diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/truststore/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/truststore/__pycache__/__init__.cpython-312.pyc index fa130eaf4965a5a9365a9c8b9ecd682c520a2fd5..275eff45787fdf90c9dca88784bce0cbda02808c 100644 GIT binary patch delta 20 acmdnNy@Q+kG%qg~0}wngP}<17mK6XuiUlD6 delta 20 acmdnNy@Q+kG%qg~0}vEklibL?mK6XvF$GQl diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/truststore/__pycache__/_api.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/truststore/__pycache__/_api.cpython-312.pyc index d76eceebe17f630b5660d862d7b1d4b5e6db6e96..5808faa113ece8197cc8f783419435ed9be17b13 100644 GIT binary patch delta 22 ccmbQ*$vCx>k^3|+FBbz4JTOq&$lc`v07r@i^#A|> delta 22 ccmbQ*$vCx>k^3|+FBbz46kLlamM()$Ryj%=G@W4Q6Be$U!08YvV1^@s6 delta 22 ccmdlpg>lamM()$Ryj%=GP;gChBe$U!08eoSGXMYp diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/truststore/__pycache__/_openssl.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/truststore/__pycache__/_openssl.cpython-312.pyc index 6d031fcb033accff8720d806fc1260137b6ad20a..1568f45dc5e3b0a2637d38b8b10226602ebc8eac 100644 GIT binary patch delta 20 acmaDR_)L)dG%qg~0}wngP}<0Sn*#tpl?A#0 delta 20 acmaDR_)L)dG%qg~0}vEklibLCn*#tqJO$?f diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/truststore/__pycache__/_ssl_constants.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/truststore/__pycache__/_ssl_constants.cpython-312.pyc index 66d004720474e61fce4e41d218f84447ef854bc3..8a812ed73754e2b6cc2e66bdbc9b762206e1b2ee 100644 GIT binary patch delta 20 acmcc4ah-$vG%qg~0}wngP}<1t!vX+0^#u0- delta 20 acmcc4ah-$vG%qg~0}vEklibMd!vX+1oCPER diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/truststore/__pycache__/_windows.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/truststore/__pycache__/_windows.cpython-312.pyc index 888c9651cf5eab0986aecd93209f6050a3d8f76c..ebdb0975c250f9e8975db1d96b49e40e751f12f5 100644 GIT binary patch delta 20 acmZ2oy}FwFG%qg~0}wngP}<17z!m^SdIk~z delta 20 acmZ2oy}FwFG%qg~0}vEklibL?z!m^TAqGDH diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/__init__.cpython-312.pyc index 88d165fbdd3587c2fdf33f0b9755369fb8512fc2..61b539b8089b23b7034be0023c46f88484359eec 100644 GIT binary patch delta 20 acmca9byJG_G%qg~0}wngP}<1t#|r>G)&(j6 delta 20 acmca9byJG_G%qg~0}vEklibMd#|r>HeFawl diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/_collections.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/_collections.cpython-312.pyc index 9628dd8b5f82e06f77ce0fbefe0668bda8f2ad2e..0052f69fce4d0fb7c0b4d0da172a9d9676f32cc8 100644 GIT binary patch delta 22 ccmey;!1$$sk^3|+FBbz4JTOq&$erl`0970Yd;kCd delta 22 ccmey;!1$$sk^3|+FBbz46kL@~ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/_version.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/_version.cpython-312.pyc index b780e7a9705166bd5fea5915677982956efae583..fa5a774ea548d87fa7cfe0a9b2bbcdaaffcdead4 100644 GIT binary patch delta 19 ZcmaFH_>7VJG%qg~0}wngP@2en8vr+31#$oY delta 19 ZcmaFH_>7VJG%qg~0}vEklbpzX8vr-e1)l%_ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/connection.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/connection.cpython-312.pyc index 1dee23069d4cd68b84847754903de0a59f22e3c4..e8d5d9244c850a7ef4a87d6a47a5d99f968c6cc1 100644 GIT binary patch delta 22 ccmX>+pYiZ~M()$Ryj%=G@W4Q6Bliw}08*(2wg3PC delta 22 ccmX>+pYiZ~M()$Ryj%=GP;gChBliw}08>x~;{X5v diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/connectionpool.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/connectionpool.cpython-312.pyc index 17fbcb6ae1f4a3b8f9997f6f9897cd4f1fd78c60..0ec988d26aad92b824421873b4d015e14c2c0d0e 100644 GIT binary patch delta 22 ccmX>-m+ACeChpU`yj%=G@W4Q6Blm$m08|$T3jhEB delta 22 ccmX>-m+ACeChpU`yj%=GP;gChBlm$m093vQH~;_u diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/exceptions.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/exceptions.cpython-312.pyc index 62b0411d7fce403c08506ad30330a36b2773d8fc..ac298f96e75cc394a14720ded1629720c1014539 100644 GIT binary patch delta 20 acmX?@c`%dvG%qg~0}wngP}<17%>)2OSq3Kn delta 20 acmX?@c`%dvG%qg~0}vEklibL?%>)2P00vY5 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/fields.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/fields.cpython-312.pyc index ed9d2ae2cfaf62125e1316ca16226aef1a225a7d..8b973218398e3434ed96a494c3035a94f1c703aa 100644 GIT binary patch delta 20 acmdlPxHFLZG%qg~0}wngP}<17P6Gfztp(En delta 20 acmdlPxHFLZG%qg~0}vEklibL?P6Gf!R0aS5 diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/filepost.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/filepost.cpython-312.pyc index 33f215466518d48ca5ba1896df706365db75bff6..3f1b9732e1dba303d1a54db0beeba0594ed3167d 100644 GIT binary patch delta 20 acmdldzfYd~G%qg~0}wngP}<17nI8ZEor~m)} diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/request.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/request.cpython-312.pyc index d12842f72f6172dbd22098aaaa81fe4efdc4d8dc..8776a91f91b57e711ed476af1edb830ca463dd30 100644 GIT binary patch delta 20 acmeCO?6TxO&CAQh00a*Vls0nL%K!j2=>-Y^ delta 20 acmeCO?6TxO&CAQh00afsBsX%`%K!j3kOemY diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/response.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/response.cpython-312.pyc index f6bb69568e6fe12c439aba6081759dc1ef4747b5..7142faf8f556f7a3bf614893314a3f8e57a45995 100644 GIT binary patch delta 22 ccmdnf$+V}FiTgA!FBbz4JTOq&$i1-z08V@ctN;K2 delta 22 ccmdnf$+V}FiTgA!FBbz46kLt<8 delta 22 ccmaDmmFewNChpU`yj%=GP;gChBln{&09iN(iU0rr diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__pycache__/socks.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__pycache__/socks.cpython-312.pyc index 6b6103cd7a78f434c01ffb3cde2cd94393d79f8d..13eb7681fbd9711d189876a0b4be9247b76909b8 100644 GIT binary patch delta 20 acmaEC_1KF0G%qg~0}wngP};~HAqxOQ_63*# delta 20 acmaEC_1KF0G%qg~0}vEklibK1AqxORodv}J diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__pycache__/__init__.cpython-312.pyc index 1e6de3b38907f94e0d4b036fdb4f628e10e32803..21c1688fb62c2f1d19fddfa8d0f720a1019ad2b1 100644 GIT binary patch delta 19 ZcmaFN_?VIVG%qg~0}wngP@2en0{}Mc1!({P delta 19 ZcmaFN_?VIVG%qg~0}vEklbpzX0{}N>1(pB+ diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__pycache__/bindings.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__pycache__/bindings.cpython-312.pyc index a42533326a2d4d14dfb3ece23e432c87f2c1ac1f..3a805339d157b9cebdb56c1cb0af325927ad001e 100644 GIT binary patch delta 22 ccmbQ=!8pHzk^3|+FBbz4JTOq&$SvXm07wx9kpKVy delta 22 ccmbQ=!8pHzk^3|+FBbz46kL><@1y=w7 delta 19 Zcmcc1c$bm;G%qg~0}vEklbpzX2>>>T1%vx0a9lG%qg~0}wngP}<0?#0~&8Km>>Y delta 20 acmZ3>x0a9lG%qg~0}vEklibLy#0~&8=LEF? diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/packages/backports/__pycache__/weakref_finalize.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/packages/backports/__pycache__/weakref_finalize.cpython-312.pyc index dcc8458fbd977ed832199b521e47edda39888a64..0e486cf655441b23ed79e7d6e11b05cc1acfc8f6 100644 GIT binary patch delta 20 acmZ2)x!#idG%qg~0}wngP}<17LK delta 20 acmcb@b%l%jG%qg~0}vEklibMd$qE2FZ3Q3z diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/__pycache__/request.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/__pycache__/request.cpython-312.pyc index f5e031a686687f7262d746ffb9c2801c4f5e8579..0f0bf6956561ef806a14f5b844af5ef99bca335d 100644 GIT binary patch delta 20 acmaE?@K}NSG%qg~0}wngP};~HApihCK?PR; delta 20 acmaE?@K}NSG%qg~0}vEklibK1ApihC=mmrT diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/__pycache__/response.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/__pycache__/response.cpython-312.pyc index 1187ad1383b5142881994d4cff387cca0c398ba5..445fef98ec548db403ead55086b6debc24f301f1 100644 GIT binary patch delta 20 acmdlfzEhn0G%qg~0}wngP}<17jvD|uXazz5 delta 20 acmdlfzEhn0G%qg~0}vEklibL?jvD|v4+U=k diff --git a/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/__pycache__/retry.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/__pycache__/retry.cpython-312.pyc index e958696ee1d8e8b0b425f50a172b23e117de51e6..c1b42a114ea5c131051492a7e567c155d618d1dd 100644 GIT binary patch delta 22 ccmaE~lJU_>M()$Ryj%=G@W4Q6BlqM()$Ryj%=GP;gChBlqICor delta 20 acmdn5v|ow)G%qg~0}vEklibK{A_xFGkp&$9 diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/INSTALLER b/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/METADATA b/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/METADATA new file mode 100644 index 00000000..12345f88 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/METADATA @@ -0,0 +1,152 @@ +Metadata-Version: 2.4 +Name: pluggy +Version: 1.6.0 +Summary: plugin and hook calling mechanisms for python +Author-email: Holger Krekel +License: MIT +Classifier: Development Status :: 6 - Mature +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: POSIX +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Topic :: Software Development :: Testing +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Utilities +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Requires-Python: >=3.9 +Description-Content-Type: text/x-rst +License-File: LICENSE +Provides-Extra: dev +Requires-Dist: pre-commit; extra == "dev" +Requires-Dist: tox; extra == "dev" +Provides-Extra: testing +Requires-Dist: pytest; extra == "testing" +Requires-Dist: pytest-benchmark; extra == "testing" +Requires-Dist: coverage; extra == "testing" +Dynamic: license-file + +==================================================== +pluggy - A minimalist production ready plugin system +==================================================== + +|pypi| |conda-forge| |versions| |github-actions| |gitter| |black| |codecov| + +This is the core framework used by the `pytest`_, `tox`_, and `devpi`_ projects. + +Please `read the docs`_ to learn more! + +A definitive example +==================== +.. code-block:: python + + import pluggy + + hookspec = pluggy.HookspecMarker("myproject") + hookimpl = pluggy.HookimplMarker("myproject") + + + class MySpec: + """A hook specification namespace.""" + + @hookspec + def myhook(self, arg1, arg2): + """My special little hook that you can customize.""" + + + class Plugin_1: + """A hook implementation namespace.""" + + @hookimpl + def myhook(self, arg1, arg2): + print("inside Plugin_1.myhook()") + return arg1 + arg2 + + + class Plugin_2: + """A 2nd hook implementation namespace.""" + + @hookimpl + def myhook(self, arg1, arg2): + print("inside Plugin_2.myhook()") + return arg1 - arg2 + + + # create a manager and add the spec + pm = pluggy.PluginManager("myproject") + pm.add_hookspecs(MySpec) + + # register plugins + pm.register(Plugin_1()) + pm.register(Plugin_2()) + + # call our ``myhook`` hook + results = pm.hook.myhook(arg1=1, arg2=2) + print(results) + + +Running this directly gets us:: + + $ python docs/examples/toy-example.py + inside Plugin_2.myhook() + inside Plugin_1.myhook() + [-1, 3] + + +.. badges + +.. |pypi| image:: https://img.shields.io/pypi/v/pluggy.svg + :target: https://pypi.org/pypi/pluggy + +.. |versions| image:: https://img.shields.io/pypi/pyversions/pluggy.svg + :target: https://pypi.org/pypi/pluggy + +.. |github-actions| image:: https://github.com/pytest-dev/pluggy/workflows/main/badge.svg + :target: https://github.com/pytest-dev/pluggy/actions + +.. |conda-forge| image:: https://img.shields.io/conda/vn/conda-forge/pluggy.svg + :target: https://anaconda.org/conda-forge/pytest + +.. |gitter| image:: https://badges.gitter.im/pytest-dev/pluggy.svg + :alt: Join the chat at https://gitter.im/pytest-dev/pluggy + :target: https://gitter.im/pytest-dev/pluggy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge + +.. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/ambv/black + +.. |codecov| image:: https://codecov.io/gh/pytest-dev/pluggy/branch/master/graph/badge.svg + :target: https://codecov.io/gh/pytest-dev/pluggy + :alt: Code coverage Status + +.. links +.. _pytest: + http://pytest.org +.. _tox: + https://tox.readthedocs.org +.. _devpi: + http://doc.devpi.net +.. _read the docs: + https://pluggy.readthedocs.io/en/latest/ + + +Support pluggy +-------------- + +`Open Collective`_ is an online funding platform for open and transparent communities. +It provides tools to raise money and share your finances in full transparency. + +It is the platform of choice for individuals and companies that want to make one-time or +monthly donations directly to the project. + +``pluggy`` is part of the ``pytest-dev`` project, see more details in the `pytest collective`_. + +.. _Open Collective: https://opencollective.com +.. _pytest collective: https://opencollective.com/pytest diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/RECORD b/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/RECORD new file mode 100644 index 00000000..fa76f4af --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/RECORD @@ -0,0 +1,23 @@ +pluggy-1.6.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +pluggy-1.6.0.dist-info/METADATA,sha256=dDjDXuJaCV63QW-EtGHC10Qlxec0rVTDkSRTxlJE4Bw,4811 +pluggy-1.6.0.dist-info/RECORD,, +pluggy-1.6.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91 +pluggy-1.6.0.dist-info/licenses/LICENSE,sha256=1rZebCE6XQtXeRHTTW5ZSbn1nXbCOMUHGi8_wWz7JgY,1110 +pluggy-1.6.0.dist-info/top_level.txt,sha256=xKSCRhai-v9MckvMuWqNz16c1tbsmOggoMSwTgcpYHE,7 +pluggy/__init__.py,sha256=D6dp1gmEDjtDp8hAwQc-qrgaulnL4iltrqkLDd-g9tg,811 +pluggy/__pycache__/__init__.cpython-312.pyc,, +pluggy/__pycache__/_callers.cpython-312.pyc,, +pluggy/__pycache__/_hooks.cpython-312.pyc,, +pluggy/__pycache__/_manager.cpython-312.pyc,, +pluggy/__pycache__/_result.cpython-312.pyc,, +pluggy/__pycache__/_tracing.cpython-312.pyc,, +pluggy/__pycache__/_version.cpython-312.pyc,, +pluggy/__pycache__/_warnings.cpython-312.pyc,, +pluggy/_callers.py,sha256=gEZllGaSYVssZ2UmpNfmYC0bdVgh2jYbAFeYKvuRMjY,5991 +pluggy/_hooks.py,sha256=E6f3nYcI6dbEuO0Gmy61ozgGU_59_e69kC08a06EBuo,25218 +pluggy/_manager.py,sha256=K4Ip_pkEjvT2oOIfQPp8CwAWoXVnENgQRcy9tlGii0o,20219 +pluggy/_result.py,sha256=3Xfy7DrjXbYb7puRquyY2VbidIWNq6Pp7QnuElMdj8Q,3098 +pluggy/_tracing.py,sha256=nXd2BCmDgf8jJxV-HO3PqxR-WV53eWnF8B4AF1nJGgo,2073 +pluggy/_version.py,sha256=5FGJNp9Lkk9uOxeCjXpoCGBF79Ar6LGPOR7-atBqb_4,511 +pluggy/_warnings.py,sha256=td0AvZBpfamriCC3OqsLwxMh-SzAMjfjmc58T5vP3lw,828 +pluggy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/WHEEL b/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/WHEEL new file mode 100644 index 00000000..e9653ae0 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.7.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/licenses/LICENSE b/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/licenses/LICENSE new file mode 100644 index 00000000..85f4dd63 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 holger krekel (rather uses bitbucket/hpk42) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/top_level.txt b/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/top_level.txt new file mode 100644 index 00000000..11bdb5c1 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/top_level.txt @@ -0,0 +1 @@ +pluggy diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy/__init__.py b/Backend/venv/lib/python3.12/site-packages/pluggy/__init__.py new file mode 100644 index 00000000..8a651f49 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pluggy/__init__.py @@ -0,0 +1,30 @@ +__all__ = [ + "__version__", + "PluginManager", + "PluginValidationError", + "HookCaller", + "HookCallError", + "HookspecOpts", + "HookimplOpts", + "HookImpl", + "HookRelay", + "HookspecMarker", + "HookimplMarker", + "Result", + "PluggyWarning", + "PluggyTeardownRaisedWarning", +] +from ._hooks import HookCaller +from ._hooks import HookImpl +from ._hooks import HookimplMarker +from ._hooks import HookimplOpts +from ._hooks import HookRelay +from ._hooks import HookspecMarker +from ._hooks import HookspecOpts +from ._manager import PluginManager +from ._manager import PluginValidationError +from ._result import HookCallError +from ._result import Result +from ._version import version as __version__ +from ._warnings import PluggyTeardownRaisedWarning +from ._warnings import PluggyWarning diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e61f81b7d12441dd6e7a02dea085f1b1920de2cf GIT binary patch literal 865 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 Z8*?TdGbH%)TFZzaNVSNh}F~FNqQvMYArNGpV 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 G<^KiSUJge9 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/_manager.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/_manager.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..675a6b35b70274c87896ec881ed3b653ac7e6d36 GIT binary patch literal 25638 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/_result.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/_result.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9e32453b46eeb974ca6bfbab09bb41f9f5057824 GIT binary patch literal 4152 zcmb7H+iw)t89#H``_1b$U>pd8xy0MT8Ydx5+z2UzOI%SC1(%kk(RjRb)*hJM*`70N zv#WI>5f4U6grz*dLm%L&MMza2`#7M0?RiiFhWp>GSOQbk|-eP?FJYbTYOvHYFy zeCPW8zT4TqWHLzt-?t}5t&ub#f5%Smi}{SS`Bz}>5tEdODVee@)#S1)A+Ojg zqjG(w)u14Ve`wgYF=r#MRt)Y!8>w1#!v-dHp4KU|Dmd?A1NZR7xc0KayyPe1VC+_c zTZ$&oR@xkZ?ma?DSpqxArc_p1a#8UzczrzhEMpF9@AI`WxK;rfH8rbVaTuc&*KU=j zi>epb^}11`y6z=)z2=xr8~L=Z|E6i!!AMNkO{b#kkH{w8;nmgg1*b;G=j%7d&rp8d zbsFOzIWD!2pN6xo`uzB50D;!c@kLr+9Jj5x@kYyCaO!WBCf*$9mP?N}V1hAE`FO)_ z&d;~Tbsy$Zqs20Cf*?b{@CcCG#8Z#5LQlR8j=;>)<{}Jlhr^W1YJzP2nJH7bt(GG+ zYNk!~wi+5g29k^!>6651!pxe{K1tHdnK4+MG6&2!v}rSMCZNrKm(l<%7EB z%v}R?wI&C!T?g>yU_nZZo2tGNqQ^Oc1m&Cn(ptxnD^^!Z#;gYSKm1Ww3L&6XZ1 zUMv6!3`K8LsPGx!-JaJ~0xSRsLS*=dzO;E6n0urnb%>5~@pWYA1&%~WNB(TEBVSW{ zqgyS^3ioVDChpPKeN85Cz|cJxZCZn?VN&iT-mdV&Km%zq5z7U`zR@arq;7wL_WL(t@&ojkR6 z@@MypUyn?zk4&tnU*^_6p20@=P9;Q$$m4Rj6qKY}?N38)$+ zxx!uNj_$WY|1yJWn~#+R0jX-MdPvpJf>PjC31+ky?w}G!qUr;PGniVLQHu%^4rK4 zd;NAvS|T@;D`ZIm>`|GSX-KY_(*~z!d+}0?dP>FSo?30zD_*|OLl-O)5+6GV>x8cC zC~!Oq1e_8+-!BtY=G1gdr6Crfs$GZoa6n!&C^iUZDo1yzf2x3>x;9Lj)AwMD~jQjIS-P1_82nga=Sk7*9%Lbhaa z(S;N&BGQ(jY8+ujAoXRi_Dg)oe024)W=uQxWA@Te56h zgL6!3Qe+-713lmswV+5LBo(jzbQ$rK`X#G$T|9`CrQ`ahNCsG&Ye&Bb0 z%TQLY^;<8Pu&i|CTO%$$WyI%VRuLb}kN5bkb}R8gSx#-qdt66(99aEx2;}x3&u@EH zUqqDM}uHS@>6nhp1QrT%Ljh2CAtc8IYu4?U7Gkw zIXf6s(oFuYen(%CMM?czVbAZ9P)jd7yzrzjv6h~AHn{8S!Nco=haY_MWN>UPJtnH! zG+h56?AH#?!Uww6(WV`E&7-5f8roKHf+2kHAWEK!6Tz{uShREc*&CH9sQkgKK+b*w zgP{5`oHLAXdHfM^_3Ed`G+#eF^HYC|L0@vUZva-}1$|WU9QK~SA6}2)dqOPrrT$IC z++vntC1*J#SbSI>h?mfHZaXfA;&=%A@dk6?MdP;E8#o6y^->kv;2hq93y#UK8nF{d z-bC^al0hT|B>22zyL$v*i~J^#+vK?_MTX&>lYjTtR98(zejz;@IM|IK1Ja?fZcMOo zlFV)<1iPC=voGXm<9`FcX-wY1{Z*P3F!G1&HvSd=fgAKx8)AZUd7+{f1RCTXJ8s80RA};cq~ZLb6J+u zf5nM(=v#8&DS7QF+4GbPKP4lA9|8Q*`PHG-+Is%*gW8{d_2=69$qQ@w3+t($-->^e z8Cw1Ilg#L?k4_-lu0W8SxjK} EU$vIt$N&HU literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/_tracing.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/_tracing.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb57ea568c14b4135c937f9b5286dd97f9b331b1 GIT binary patch literal 4029 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 F{R0LkvA6&L literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/_warnings.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pluggy/__pycache__/_warnings.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81e3da7fedf2aa05cbb166422e6c296cb8b54acd GIT binary patch literal 1303 zcmb7E&1)1f6wmDJZcFWoUk9xq!IKN^jvp5(MX-pUP{a>}1(~Lkx4WU4%rwck9dAPa z0B>HsihqiKfeV5l9=uhyCr`d)W^EM_3`6GSeZSv($%js7f#CXj`Jz}03HjmJN z>1`z55Ked?NgDKnfHcS*!W(;phtPPBc5d}U2t2=u{1${W{`*Qy`pvT4o%`la-C)uh z2*qSl%lt2>57RS9ydgmP0Vn+i5Bec*^n)_&HYc5jvKS7_XG|-hhA(^T%m7U!GlmW_ zO_`K*T*(ZDR9Fj~Zk06mR=r8PA~xPNC-ZTvSPF4GS%~8_*O*ibpR%y@;gPit^HqGIgzY z2|u)3K^zG7@CfsU}~v1WM= zdV%6vLs9HS@ez@^u}N(my0_pJ9~a5!FoK~>167%&Bc`}K+OTB|%W29=ny>;JZHg@e zI|V4~{mk4NYBRb8=n|5`b(~pRDA>&*aVINMFv}Fj19i-{z=SzPA3}pH>y)01grWFh znZlR?D57CUFat)Uxr7vLn8T#Dxi|1Chftw?fn#ZL0)Q{mf!Os51s=659g4n;f0!Q{ zo3XHn5*Aj$u3&rwc3R_ZD&T*I+n4I#S{)QBS>4oN3Tb6x7dV_d# literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy/_callers.py b/Backend/venv/lib/python3.12/site-packages/pluggy/_callers.py new file mode 100644 index 00000000..472d5dd0 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pluggy/_callers.py @@ -0,0 +1,169 @@ +""" +Call loop machinery +""" + +from __future__ import annotations + +from collections.abc import Generator +from collections.abc import Mapping +from collections.abc import Sequence +from typing import cast +from typing import NoReturn +import warnings + +from ._hooks import HookImpl +from ._result import HookCallError +from ._result import Result +from ._warnings import PluggyTeardownRaisedWarning + + +# Need to distinguish between old- and new-style hook wrappers. +# Wrapping with a tuple is the fastest type-safe way I found to do it. +Teardown = Generator[None, object, object] + + +def run_old_style_hookwrapper( + hook_impl: HookImpl, hook_name: str, args: Sequence[object] +) -> Teardown: + """ + backward compatibility wrapper to run a old style hookwrapper as a wrapper + """ + + teardown: Teardown = cast(Teardown, hook_impl.function(*args)) + try: + next(teardown) + except StopIteration: + _raise_wrapfail(teardown, "did not yield") + try: + res = yield + result = Result(res, None) + except BaseException as exc: + result = Result(None, exc) + try: + teardown.send(result) + except StopIteration: + pass + except BaseException as e: + _warn_teardown_exception(hook_name, hook_impl, e) + raise + else: + _raise_wrapfail(teardown, "has second yield") + finally: + teardown.close() + return result.get_result() + + +def _raise_wrapfail( + wrap_controller: Generator[None, object, object], + msg: str, +) -> NoReturn: + co = wrap_controller.gi_code # type: ignore[attr-defined] + raise RuntimeError( + f"wrap_controller at {co.co_name!r} {co.co_filename}:{co.co_firstlineno} {msg}" + ) + + +def _warn_teardown_exception( + hook_name: str, hook_impl: HookImpl, e: BaseException +) -> None: + msg = "A plugin raised an exception during an old-style hookwrapper teardown.\n" + msg += f"Plugin: {hook_impl.plugin_name}, Hook: {hook_name}\n" + msg += f"{type(e).__name__}: {e}\n" + msg += "For more information see https://pluggy.readthedocs.io/en/stable/api_reference.html#pluggy.PluggyTeardownRaisedWarning" # noqa: E501 + warnings.warn(PluggyTeardownRaisedWarning(msg), stacklevel=6) + + +def _multicall( + hook_name: str, + hook_impls: Sequence[HookImpl], + caller_kwargs: Mapping[str, object], + firstresult: bool, +) -> object | list[object]: + """Execute a call into multiple python functions/methods and return the + result(s). + + ``caller_kwargs`` comes from HookCaller.__call__(). + """ + __tracebackhide__ = True + results: list[object] = [] + exception = None + try: # run impl and wrapper setup functions in a loop + teardowns: list[Teardown] = [] + try: + for hook_impl in reversed(hook_impls): + try: + args = [caller_kwargs[argname] for argname in hook_impl.argnames] + except KeyError as e: + # coverage bug - this is tested + for argname in hook_impl.argnames: # pragma: no cover + if argname not in caller_kwargs: + raise HookCallError( + f"hook call must provide argument {argname!r}" + ) from e + + if hook_impl.hookwrapper: + function_gen = run_old_style_hookwrapper(hook_impl, hook_name, args) + + next(function_gen) # first yield + teardowns.append(function_gen) + + elif hook_impl.wrapper: + try: + # If this cast is not valid, a type error is raised below, + # which is the desired response. + res = hook_impl.function(*args) + function_gen = cast(Generator[None, object, object], res) + next(function_gen) # first yield + teardowns.append(function_gen) + except StopIteration: + _raise_wrapfail(function_gen, "did not yield") + else: + res = hook_impl.function(*args) + if res is not None: + results.append(res) + if firstresult: # halt further impl calls + break + except BaseException as exc: + exception = exc + finally: + if firstresult: # first result hooks return a single value + result = results[0] if results else None + else: + result = results + + # run all wrapper post-yield blocks + for teardown in reversed(teardowns): + try: + if exception is not None: + try: + teardown.throw(exception) + except RuntimeError as re: + # StopIteration from generator causes RuntimeError + # even for coroutine usage - see #544 + if ( + isinstance(exception, StopIteration) + and re.__cause__ is exception + ): + teardown.close() + continue + else: + raise + else: + teardown.send(result) + # Following is unreachable for a well behaved hook wrapper. + # Try to force finalizers otherwise postponed till GC action. + # Note: close() may raise if generator handles GeneratorExit. + teardown.close() + except StopIteration as si: + result = si.value + exception = None + continue + except BaseException as e: + exception = e + continue + _raise_wrapfail(teardown, "has second yield") + + if exception is not None: + raise exception + else: + return result diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy/_hooks.py b/Backend/venv/lib/python3.12/site-packages/pluggy/_hooks.py new file mode 100644 index 00000000..97fef0d7 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pluggy/_hooks.py @@ -0,0 +1,714 @@ +""" +Internal hook annotation, representation and calling machinery. +""" + +from __future__ import annotations + +from collections.abc import Generator +from collections.abc import Mapping +from collections.abc import Sequence +from collections.abc import Set +import inspect +import sys +from types import ModuleType +from typing import Any +from typing import Callable +from typing import Final +from typing import final +from typing import Optional +from typing import overload +from typing import TYPE_CHECKING +from typing import TypedDict +from typing import TypeVar +from typing import Union +import warnings + +from ._result import Result + + +_T = TypeVar("_T") +_F = TypeVar("_F", bound=Callable[..., object]) +_Namespace = Union[ModuleType, type] +_Plugin = object +_HookExec = Callable[ + [str, Sequence["HookImpl"], Mapping[str, object], bool], + Union[object, list[object]], +] +_HookImplFunction = Callable[..., Union[_T, Generator[None, Result[_T], None]]] + + +class HookspecOpts(TypedDict): + """Options for a hook specification.""" + + #: Whether the hook is :ref:`first result only `. + firstresult: bool + #: Whether the hook is :ref:`historic `. + historic: bool + #: Whether the hook :ref:`warns when implemented `. + warn_on_impl: Warning | None + #: Whether the hook warns when :ref:`certain arguments are requested + #: `. + #: + #: .. versionadded:: 1.5 + warn_on_impl_args: Mapping[str, Warning] | None + + +class HookimplOpts(TypedDict): + """Options for a hook implementation.""" + + #: Whether the hook implementation is a :ref:`wrapper `. + wrapper: bool + #: Whether the hook implementation is an :ref:`old-style wrapper + #: `. + hookwrapper: bool + #: Whether validation against a hook specification is :ref:`optional + #: `. + optionalhook: bool + #: Whether to try to order this hook implementation :ref:`first + #: `. + tryfirst: bool + #: Whether to try to order this hook implementation :ref:`last + #: `. + trylast: bool + #: The name of the hook specification to match, see :ref:`specname`. + specname: str | None + + +@final +class HookspecMarker: + """Decorator for marking functions as hook specifications. + + Instantiate it with a project_name to get a decorator. + Calling :meth:`PluginManager.add_hookspecs` later will discover all marked + functions if the :class:`PluginManager` uses the same project name. + """ + + __slots__ = ("project_name",) + + def __init__(self, project_name: str) -> None: + self.project_name: Final = project_name + + @overload + def __call__( + self, + function: _F, + firstresult: bool = False, + historic: bool = False, + warn_on_impl: Warning | None = None, + warn_on_impl_args: Mapping[str, Warning] | None = None, + ) -> _F: ... + + @overload # noqa: F811 + def __call__( # noqa: F811 + self, + function: None = ..., + firstresult: bool = ..., + historic: bool = ..., + warn_on_impl: Warning | None = ..., + warn_on_impl_args: Mapping[str, Warning] | None = ..., + ) -> Callable[[_F], _F]: ... + + def __call__( # noqa: F811 + self, + function: _F | None = None, + firstresult: bool = False, + historic: bool = False, + warn_on_impl: Warning | None = None, + warn_on_impl_args: Mapping[str, Warning] | None = None, + ) -> _F | Callable[[_F], _F]: + """If passed a function, directly sets attributes on the function + which will make it discoverable to :meth:`PluginManager.add_hookspecs`. + + If passed no function, returns a decorator which can be applied to a + function later using the attributes supplied. + + :param firstresult: + If ``True``, the 1:N hook call (N being the number of registered + hook implementation functions) will stop at I<=N when the I'th + function returns a non-``None`` result. See :ref:`firstresult`. + + :param historic: + If ``True``, every call to the hook will be memorized and replayed + on plugins registered after the call was made. See :ref:`historic`. + + :param warn_on_impl: + If given, every implementation of this hook will trigger the given + warning. See :ref:`warn_on_impl`. + + :param warn_on_impl_args: + If given, every implementation of this hook which requests one of + the arguments in the dict will trigger the corresponding warning. + See :ref:`warn_on_impl`. + + .. versionadded:: 1.5 + """ + + def setattr_hookspec_opts(func: _F) -> _F: + if historic and firstresult: + raise ValueError("cannot have a historic firstresult hook") + opts: HookspecOpts = { + "firstresult": firstresult, + "historic": historic, + "warn_on_impl": warn_on_impl, + "warn_on_impl_args": warn_on_impl_args, + } + setattr(func, self.project_name + "_spec", opts) + return func + + if function is not None: + return setattr_hookspec_opts(function) + else: + return setattr_hookspec_opts + + +@final +class HookimplMarker: + """Decorator for marking functions as hook implementations. + + Instantiate it with a ``project_name`` to get a decorator. + Calling :meth:`PluginManager.register` later will discover all marked + functions if the :class:`PluginManager` uses the same project name. + """ + + __slots__ = ("project_name",) + + def __init__(self, project_name: str) -> None: + self.project_name: Final = project_name + + @overload + def __call__( + self, + function: _F, + hookwrapper: bool = ..., + optionalhook: bool = ..., + tryfirst: bool = ..., + trylast: bool = ..., + specname: str | None = ..., + wrapper: bool = ..., + ) -> _F: ... + + @overload # noqa: F811 + def __call__( # noqa: F811 + self, + function: None = ..., + hookwrapper: bool = ..., + optionalhook: bool = ..., + tryfirst: bool = ..., + trylast: bool = ..., + specname: str | None = ..., + wrapper: bool = ..., + ) -> Callable[[_F], _F]: ... + + def __call__( # noqa: F811 + self, + function: _F | None = None, + hookwrapper: bool = False, + optionalhook: bool = False, + tryfirst: bool = False, + trylast: bool = False, + specname: str | None = None, + wrapper: bool = False, + ) -> _F | Callable[[_F], _F]: + """If passed a function, directly sets attributes on the function + which will make it discoverable to :meth:`PluginManager.register`. + + If passed no function, returns a decorator which can be applied to a + function later using the attributes supplied. + + :param optionalhook: + If ``True``, a missing matching hook specification will not result + in an error (by default it is an error if no matching spec is + found). See :ref:`optionalhook`. + + :param tryfirst: + If ``True``, this hook implementation will run as early as possible + in the chain of N hook implementations for a specification. See + :ref:`callorder`. + + :param trylast: + If ``True``, this hook implementation will run as late as possible + in the chain of N hook implementations for a specification. See + :ref:`callorder`. + + :param wrapper: + If ``True`` ("new-style hook wrapper"), the hook implementation + needs to execute exactly one ``yield``. The code before the + ``yield`` is run early before any non-hook-wrapper function is run. + The code after the ``yield`` is run after all non-hook-wrapper + functions have run. The ``yield`` receives the result value of the + inner calls, or raises the exception of inner calls (including + earlier hook wrapper calls). The return value of the function + becomes the return value of the hook, and a raised exception becomes + the exception of the hook. See :ref:`hookwrapper`. + + :param hookwrapper: + If ``True`` ("old-style hook wrapper"), the hook implementation + needs to execute exactly one ``yield``. The code before the + ``yield`` is run early before any non-hook-wrapper function is run. + The code after the ``yield`` is run after all non-hook-wrapper + function have run The ``yield`` receives a :class:`Result` object + representing the exception or result outcome of the inner calls + (including earlier hook wrapper calls). This option is mutually + exclusive with ``wrapper``. See :ref:`old_style_hookwrapper`. + + :param specname: + If provided, the given name will be used instead of the function + name when matching this hook implementation to a hook specification + during registration. See :ref:`specname`. + + .. versionadded:: 1.2.0 + The ``wrapper`` parameter. + """ + + def setattr_hookimpl_opts(func: _F) -> _F: + opts: HookimplOpts = { + "wrapper": wrapper, + "hookwrapper": hookwrapper, + "optionalhook": optionalhook, + "tryfirst": tryfirst, + "trylast": trylast, + "specname": specname, + } + setattr(func, self.project_name + "_impl", opts) + return func + + if function is None: + return setattr_hookimpl_opts + else: + return setattr_hookimpl_opts(function) + + +def normalize_hookimpl_opts(opts: HookimplOpts) -> None: + opts.setdefault("tryfirst", False) + opts.setdefault("trylast", False) + opts.setdefault("wrapper", False) + opts.setdefault("hookwrapper", False) + opts.setdefault("optionalhook", False) + opts.setdefault("specname", None) + + +_PYPY = hasattr(sys, "pypy_version_info") + + +def varnames(func: object) -> tuple[tuple[str, ...], tuple[str, ...]]: + """Return tuple of positional and keywrord argument names for a function, + method, class or callable. + + In case of a class, its ``__init__`` method is considered. + For methods the ``self`` parameter is not included. + """ + if inspect.isclass(func): + try: + func = func.__init__ + except AttributeError: # pragma: no cover - pypy special case + return (), () + elif not inspect.isroutine(func): # callable object? + try: + func = getattr(func, "__call__", func) + except Exception: # pragma: no cover - pypy special case + return (), () + + try: + # func MUST be a function or method here or we won't parse any args. + sig = inspect.signature( + func.__func__ if inspect.ismethod(func) else func # type:ignore[arg-type] + ) + except TypeError: # pragma: no cover + return (), () + + _valid_param_kinds = ( + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + ) + _valid_params = { + name: param + for name, param in sig.parameters.items() + if param.kind in _valid_param_kinds + } + args = tuple(_valid_params) + defaults = ( + tuple( + param.default + for param in _valid_params.values() + if param.default is not param.empty + ) + or None + ) + + if defaults: + index = -len(defaults) + args, kwargs = args[:index], tuple(args[index:]) + else: + kwargs = () + + # strip any implicit instance arg + # pypy3 uses "obj" instead of "self" for default dunder methods + if not _PYPY: + implicit_names: tuple[str, ...] = ("self",) + else: # pragma: no cover + implicit_names = ("self", "obj") + if args: + qualname: str = getattr(func, "__qualname__", "") + if inspect.ismethod(func) or ("." in qualname and args[0] in implicit_names): + args = args[1:] + + return args, kwargs + + +@final +class HookRelay: + """Hook holder object for performing 1:N hook calls where N is the number + of registered plugins.""" + + __slots__ = ("__dict__",) + + def __init__(self) -> None: + """:meta private:""" + + if TYPE_CHECKING: + + def __getattr__(self, name: str) -> HookCaller: ... + + +# Historical name (pluggy<=1.2), kept for backward compatibility. +_HookRelay = HookRelay + + +_CallHistory = list[tuple[Mapping[str, object], Optional[Callable[[Any], None]]]] + + +class HookCaller: + """A caller of all registered implementations of a hook specification.""" + + __slots__ = ( + "name", + "spec", + "_hookexec", + "_hookimpls", + "_call_history", + ) + + def __init__( + self, + name: str, + hook_execute: _HookExec, + specmodule_or_class: _Namespace | None = None, + spec_opts: HookspecOpts | None = None, + ) -> None: + """:meta private:""" + #: Name of the hook getting called. + self.name: Final = name + self._hookexec: Final = hook_execute + # The hookimpls list. The caller iterates it *in reverse*. Format: + # 1. trylast nonwrappers + # 2. nonwrappers + # 3. tryfirst nonwrappers + # 4. trylast wrappers + # 5. wrappers + # 6. tryfirst wrappers + self._hookimpls: Final[list[HookImpl]] = [] + self._call_history: _CallHistory | None = None + # TODO: Document, or make private. + self.spec: HookSpec | None = None + if specmodule_or_class is not None: + assert spec_opts is not None + self.set_specification(specmodule_or_class, spec_opts) + + # TODO: Document, or make private. + def has_spec(self) -> bool: + return self.spec is not None + + # TODO: Document, or make private. + def set_specification( + self, + specmodule_or_class: _Namespace, + spec_opts: HookspecOpts, + ) -> None: + if self.spec is not None: + raise ValueError( + f"Hook {self.spec.name!r} is already registered " + f"within namespace {self.spec.namespace}" + ) + self.spec = HookSpec(specmodule_or_class, self.name, spec_opts) + if spec_opts.get("historic"): + self._call_history = [] + + def is_historic(self) -> bool: + """Whether this caller is :ref:`historic `.""" + return self._call_history is not None + + def _remove_plugin(self, plugin: _Plugin) -> None: + for i, method in enumerate(self._hookimpls): + if method.plugin == plugin: + del self._hookimpls[i] + return + raise ValueError(f"plugin {plugin!r} not found") + + def get_hookimpls(self) -> list[HookImpl]: + """Get all registered hook implementations for this hook.""" + return self._hookimpls.copy() + + def _add_hookimpl(self, hookimpl: HookImpl) -> None: + """Add an implementation to the callback chain.""" + for i, method in enumerate(self._hookimpls): + if method.hookwrapper or method.wrapper: + splitpoint = i + break + else: + splitpoint = len(self._hookimpls) + if hookimpl.hookwrapper or hookimpl.wrapper: + start, end = splitpoint, len(self._hookimpls) + else: + start, end = 0, splitpoint + + if hookimpl.trylast: + self._hookimpls.insert(start, hookimpl) + elif hookimpl.tryfirst: + self._hookimpls.insert(end, hookimpl) + else: + # find last non-tryfirst method + i = end - 1 + while i >= start and self._hookimpls[i].tryfirst: + i -= 1 + self._hookimpls.insert(i + 1, hookimpl) + + def __repr__(self) -> str: + return f"" + + def _verify_all_args_are_provided(self, kwargs: Mapping[str, object]) -> None: + # This is written to avoid expensive operations when not needed. + if self.spec: + for argname in self.spec.argnames: + if argname not in kwargs: + notincall = ", ".join( + repr(argname) + for argname in self.spec.argnames + # Avoid self.spec.argnames - kwargs.keys() + # it doesn't preserve order. + if argname not in kwargs.keys() + ) + warnings.warn( + f"Argument(s) {notincall} which are declared in the hookspec " + "cannot be found in this hook call", + stacklevel=2, + ) + break + + def __call__(self, **kwargs: object) -> Any: + """Call the hook. + + Only accepts keyword arguments, which should match the hook + specification. + + Returns the result(s) of calling all registered plugins, see + :ref:`calling`. + """ + assert not self.is_historic(), ( + "Cannot directly call a historic hook - use call_historic instead." + ) + self._verify_all_args_are_provided(kwargs) + firstresult = self.spec.opts.get("firstresult", False) if self.spec else False + # Copy because plugins may register other plugins during iteration (#438). + return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult) + + def call_historic( + self, + result_callback: Callable[[Any], None] | None = None, + kwargs: Mapping[str, object] | None = None, + ) -> None: + """Call the hook with given ``kwargs`` for all registered plugins and + for all plugins which will be registered afterwards, see + :ref:`historic`. + + :param result_callback: + If provided, will be called for each non-``None`` result obtained + from a hook implementation. + """ + assert self._call_history is not None + kwargs = kwargs or {} + self._verify_all_args_are_provided(kwargs) + self._call_history.append((kwargs, result_callback)) + # Historizing hooks don't return results. + # Remember firstresult isn't compatible with historic. + # Copy because plugins may register other plugins during iteration (#438). + res = self._hookexec(self.name, self._hookimpls.copy(), kwargs, False) + if result_callback is None: + return + if isinstance(res, list): + for x in res: + result_callback(x) + + def call_extra( + self, methods: Sequence[Callable[..., object]], kwargs: Mapping[str, object] + ) -> Any: + """Call the hook with some additional temporarily participating + methods using the specified ``kwargs`` as call parameters, see + :ref:`call_extra`.""" + assert not self.is_historic(), ( + "Cannot directly call a historic hook - use call_historic instead." + ) + self._verify_all_args_are_provided(kwargs) + opts: HookimplOpts = { + "wrapper": False, + "hookwrapper": False, + "optionalhook": False, + "trylast": False, + "tryfirst": False, + "specname": None, + } + hookimpls = self._hookimpls.copy() + for method in methods: + hookimpl = HookImpl(None, "", method, opts) + # Find last non-tryfirst nonwrapper method. + i = len(hookimpls) - 1 + while i >= 0 and ( + # Skip wrappers. + (hookimpls[i].hookwrapper or hookimpls[i].wrapper) + # Skip tryfirst nonwrappers. + or hookimpls[i].tryfirst + ): + i -= 1 + hookimpls.insert(i + 1, hookimpl) + firstresult = self.spec.opts.get("firstresult", False) if self.spec else False + return self._hookexec(self.name, hookimpls, kwargs, firstresult) + + def _maybe_apply_history(self, method: HookImpl) -> None: + """Apply call history to a new hookimpl if it is marked as historic.""" + if self.is_historic(): + assert self._call_history is not None + for kwargs, result_callback in self._call_history: + res = self._hookexec(self.name, [method], kwargs, False) + if res and result_callback is not None: + # XXX: remember firstresult isn't compat with historic + assert isinstance(res, list) + result_callback(res[0]) + + +# Historical name (pluggy<=1.2), kept for backward compatibility. +_HookCaller = HookCaller + + +class _SubsetHookCaller(HookCaller): + """A proxy to another HookCaller which manages calls to all registered + plugins except the ones from remove_plugins.""" + + # This class is unusual: in inhertits from `HookCaller` so all of + # the *code* runs in the class, but it delegates all underlying *data* + # to the original HookCaller. + # `subset_hook_caller` used to be implemented by creating a full-fledged + # HookCaller, copying all hookimpls from the original. This had problems + # with memory leaks (#346) and historic calls (#347), which make a proxy + # approach better. + # An alternative implementation is to use a `_getattr__`/`__getattribute__` + # proxy, however that adds more overhead and is more tricky to implement. + + __slots__ = ( + "_orig", + "_remove_plugins", + ) + + def __init__(self, orig: HookCaller, remove_plugins: Set[_Plugin]) -> None: + self._orig = orig + self._remove_plugins = remove_plugins + self.name = orig.name # type: ignore[misc] + self._hookexec = orig._hookexec # type: ignore[misc] + + @property # type: ignore[misc] + def _hookimpls(self) -> list[HookImpl]: + return [ + impl + for impl in self._orig._hookimpls + if impl.plugin not in self._remove_plugins + ] + + @property + def spec(self) -> HookSpec | None: # type: ignore[override] + return self._orig.spec + + @property + def _call_history(self) -> _CallHistory | None: # type: ignore[override] + return self._orig._call_history + + def __repr__(self) -> str: + return f"<_SubsetHookCaller {self.name!r}>" + + +@final +class HookImpl: + """A hook implementation in a :class:`HookCaller`.""" + + __slots__ = ( + "function", + "argnames", + "kwargnames", + "plugin", + "opts", + "plugin_name", + "wrapper", + "hookwrapper", + "optionalhook", + "tryfirst", + "trylast", + ) + + def __init__( + self, + plugin: _Plugin, + plugin_name: str, + function: _HookImplFunction[object], + hook_impl_opts: HookimplOpts, + ) -> None: + """:meta private:""" + #: The hook implementation function. + self.function: Final = function + argnames, kwargnames = varnames(self.function) + #: The positional parameter names of ``function```. + self.argnames: Final = argnames + #: The keyword parameter names of ``function```. + self.kwargnames: Final = kwargnames + #: The plugin which defined this hook implementation. + self.plugin: Final = plugin + #: The :class:`HookimplOpts` used to configure this hook implementation. + self.opts: Final = hook_impl_opts + #: The name of the plugin which defined this hook implementation. + self.plugin_name: Final = plugin_name + #: Whether the hook implementation is a :ref:`wrapper `. + self.wrapper: Final = hook_impl_opts["wrapper"] + #: Whether the hook implementation is an :ref:`old-style wrapper + #: `. + self.hookwrapper: Final = hook_impl_opts["hookwrapper"] + #: Whether validation against a hook specification is :ref:`optional + #: `. + self.optionalhook: Final = hook_impl_opts["optionalhook"] + #: Whether to try to order this hook implementation :ref:`first + #: `. + self.tryfirst: Final = hook_impl_opts["tryfirst"] + #: Whether to try to order this hook implementation :ref:`last + #: `. + self.trylast: Final = hook_impl_opts["trylast"] + + def __repr__(self) -> str: + return f"" + + +@final +class HookSpec: + __slots__ = ( + "namespace", + "function", + "name", + "argnames", + "kwargnames", + "opts", + "warn_on_impl", + "warn_on_impl_args", + ) + + def __init__(self, namespace: _Namespace, name: str, opts: HookspecOpts) -> None: + self.namespace = namespace + self.function: Callable[..., object] = getattr(namespace, name) + self.name = name + self.argnames, self.kwargnames = varnames(self.function) + self.opts = opts + self.warn_on_impl = opts.get("warn_on_impl") + self.warn_on_impl_args = opts.get("warn_on_impl_args") diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy/_manager.py b/Backend/venv/lib/python3.12/site-packages/pluggy/_manager.py new file mode 100644 index 00000000..ff1e3ce6 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pluggy/_manager.py @@ -0,0 +1,523 @@ +from __future__ import annotations + +from collections.abc import Iterable +from collections.abc import Mapping +from collections.abc import Sequence +import inspect +import types +from typing import Any +from typing import Callable +from typing import cast +from typing import Final +from typing import TYPE_CHECKING +import warnings + +from . import _tracing +from ._callers import _multicall +from ._hooks import _HookImplFunction +from ._hooks import _Namespace +from ._hooks import _Plugin +from ._hooks import _SubsetHookCaller +from ._hooks import HookCaller +from ._hooks import HookImpl +from ._hooks import HookimplOpts +from ._hooks import HookRelay +from ._hooks import HookspecOpts +from ._hooks import normalize_hookimpl_opts +from ._result import Result + + +if TYPE_CHECKING: + # importtlib.metadata import is slow, defer it. + import importlib.metadata + + +_BeforeTrace = Callable[[str, Sequence[HookImpl], Mapping[str, Any]], None] +_AfterTrace = Callable[[Result[Any], str, Sequence[HookImpl], Mapping[str, Any]], None] + + +def _warn_for_function(warning: Warning, function: Callable[..., object]) -> None: + func = cast(types.FunctionType, function) + warnings.warn_explicit( + warning, + type(warning), + lineno=func.__code__.co_firstlineno, + filename=func.__code__.co_filename, + ) + + +class PluginValidationError(Exception): + """Plugin failed validation. + + :param plugin: The plugin which failed validation. + :param message: Error message. + """ + + def __init__(self, plugin: _Plugin, message: str) -> None: + super().__init__(message) + #: The plugin which failed validation. + self.plugin = plugin + + +class DistFacade: + """Emulate a pkg_resources Distribution""" + + def __init__(self, dist: importlib.metadata.Distribution) -> None: + self._dist = dist + + @property + def project_name(self) -> str: + name: str = self.metadata["name"] + return name + + def __getattr__(self, attr: str, default: Any | None = None) -> Any: + return getattr(self._dist, attr, default) + + def __dir__(self) -> list[str]: + return sorted(dir(self._dist) + ["_dist", "project_name"]) + + +class PluginManager: + """Core class which manages registration of plugin objects and 1:N hook + calling. + + You can register new hooks by calling :meth:`add_hookspecs(module_or_class) + `. + + You can register plugin objects (which contain hook implementations) by + calling :meth:`register(plugin) `. + + For debugging purposes you can call :meth:`PluginManager.enable_tracing` + which will subsequently send debug information to the trace helper. + + :param project_name: + The short project name. Prefer snake case. Make sure it's unique! + """ + + def __init__(self, project_name: str) -> None: + #: The project name. + self.project_name: Final = project_name + self._name2plugin: Final[dict[str, _Plugin]] = {} + self._plugin_distinfo: Final[list[tuple[_Plugin, DistFacade]]] = [] + #: The "hook relay", used to call a hook on all registered plugins. + #: See :ref:`calling`. + self.hook: Final = HookRelay() + #: The tracing entry point. See :ref:`tracing`. + self.trace: Final[_tracing.TagTracerSub] = _tracing.TagTracer().get( + "pluginmanage" + ) + self._inner_hookexec = _multicall + + def _hookexec( + self, + hook_name: str, + methods: Sequence[HookImpl], + kwargs: Mapping[str, object], + firstresult: bool, + ) -> object | list[object]: + # called from all hookcaller instances. + # enable_tracing will set its own wrapping function at self._inner_hookexec + return self._inner_hookexec(hook_name, methods, kwargs, firstresult) + + def register(self, plugin: _Plugin, name: str | None = None) -> str | None: + """Register a plugin and return its name. + + :param name: + The name under which to register the plugin. If not specified, a + name is generated using :func:`get_canonical_name`. + + :returns: + The plugin name. If the name is blocked from registering, returns + ``None``. + + If the plugin is already registered, raises a :exc:`ValueError`. + """ + plugin_name = name or self.get_canonical_name(plugin) + + if plugin_name in self._name2plugin: + if self._name2plugin.get(plugin_name, -1) is None: + return None # blocked plugin, return None to indicate no registration + raise ValueError( + "Plugin name already registered: " + f"{plugin_name}={plugin}\n{self._name2plugin}" + ) + + if plugin in self._name2plugin.values(): + raise ValueError( + "Plugin already registered under a different name: " + f"{plugin_name}={plugin}\n{self._name2plugin}" + ) + + # XXX if an error happens we should make sure no state has been + # changed at point of return + self._name2plugin[plugin_name] = plugin + + # register matching hook implementations of the plugin + for name in dir(plugin): + hookimpl_opts = self.parse_hookimpl_opts(plugin, name) + if hookimpl_opts is not None: + normalize_hookimpl_opts(hookimpl_opts) + method: _HookImplFunction[object] = getattr(plugin, name) + hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts) + name = hookimpl_opts.get("specname") or name + hook: HookCaller | None = getattr(self.hook, name, None) + if hook is None: + hook = HookCaller(name, self._hookexec) + setattr(self.hook, name, hook) + elif hook.has_spec(): + self._verify_hook(hook, hookimpl) + hook._maybe_apply_history(hookimpl) + hook._add_hookimpl(hookimpl) + return plugin_name + + def parse_hookimpl_opts(self, plugin: _Plugin, name: str) -> HookimplOpts | None: + """Try to obtain a hook implementation from an item with the given name + in the given plugin which is being searched for hook impls. + + :returns: + The parsed hookimpl options, or None to skip the given item. + + This method can be overridden by ``PluginManager`` subclasses to + customize how hook implementation are picked up. By default, returns the + options for items decorated with :class:`HookimplMarker`. + """ + method: object = getattr(plugin, name) + if not inspect.isroutine(method): + return None + try: + res: HookimplOpts | None = getattr( + method, self.project_name + "_impl", None + ) + except Exception: # pragma: no cover + res = {} # type: ignore[assignment] #pragma: no cover + if res is not None and not isinstance(res, dict): + # false positive + res = None # type:ignore[unreachable] #pragma: no cover + return res + + def unregister( + self, plugin: _Plugin | None = None, name: str | None = None + ) -> Any | None: + """Unregister a plugin and all of its hook implementations. + + The plugin can be specified either by the plugin object or the plugin + name. If both are specified, they must agree. + + Returns the unregistered plugin, or ``None`` if not found. + """ + if name is None: + assert plugin is not None, "one of name or plugin needs to be specified" + name = self.get_name(plugin) + assert name is not None, "plugin is not registered" + + if plugin is None: + plugin = self.get_plugin(name) + if plugin is None: + return None + + hookcallers = self.get_hookcallers(plugin) + if hookcallers: + for hookcaller in hookcallers: + hookcaller._remove_plugin(plugin) + + # if self._name2plugin[name] == None registration was blocked: ignore + if self._name2plugin.get(name): + assert name is not None + del self._name2plugin[name] + + return plugin + + def set_blocked(self, name: str) -> None: + """Block registrations of the given name, unregister if already registered.""" + self.unregister(name=name) + self._name2plugin[name] = None + + def is_blocked(self, name: str) -> bool: + """Return whether the given plugin name is blocked.""" + return name in self._name2plugin and self._name2plugin[name] is None + + def unblock(self, name: str) -> bool: + """Unblocks a name. + + Returns whether the name was actually blocked. + """ + if self._name2plugin.get(name, -1) is None: + del self._name2plugin[name] + return True + return False + + def add_hookspecs(self, module_or_class: _Namespace) -> None: + """Add new hook specifications defined in the given ``module_or_class``. + + Functions are recognized as hook specifications if they have been + decorated with a matching :class:`HookspecMarker`. + """ + names = [] + for name in dir(module_or_class): + spec_opts = self.parse_hookspec_opts(module_or_class, name) + if spec_opts is not None: + hc: HookCaller | None = getattr(self.hook, name, None) + if hc is None: + hc = HookCaller(name, self._hookexec, module_or_class, spec_opts) + setattr(self.hook, name, hc) + else: + # Plugins registered this hook without knowing the spec. + hc.set_specification(module_or_class, spec_opts) + for hookfunction in hc.get_hookimpls(): + self._verify_hook(hc, hookfunction) + names.append(name) + + if not names: + raise ValueError( + f"did not find any {self.project_name!r} hooks in {module_or_class!r}" + ) + + def parse_hookspec_opts( + self, module_or_class: _Namespace, name: str + ) -> HookspecOpts | None: + """Try to obtain a hook specification from an item with the given name + in the given module or class which is being searched for hook specs. + + :returns: + The parsed hookspec options for defining a hook, or None to skip the + given item. + + This method can be overridden by ``PluginManager`` subclasses to + customize how hook specifications are picked up. By default, returns the + options for items decorated with :class:`HookspecMarker`. + """ + method = getattr(module_or_class, name) + opts: HookspecOpts | None = getattr(method, self.project_name + "_spec", None) + return opts + + def get_plugins(self) -> set[Any]: + """Return a set of all registered plugin objects.""" + return {x for x in self._name2plugin.values() if x is not None} + + def is_registered(self, plugin: _Plugin) -> bool: + """Return whether the plugin is already registered.""" + return any(plugin == val for val in self._name2plugin.values()) + + def get_canonical_name(self, plugin: _Plugin) -> str: + """Return a canonical name for a plugin object. + + Note that a plugin may be registered under a different name + specified by the caller of :meth:`register(plugin, name) `. + To obtain the name of a registered plugin use :meth:`get_name(plugin) + ` instead. + """ + name: str | None = getattr(plugin, "__name__", None) + return name or str(id(plugin)) + + def get_plugin(self, name: str) -> Any | None: + """Return the plugin registered under the given name, if any.""" + return self._name2plugin.get(name) + + def has_plugin(self, name: str) -> bool: + """Return whether a plugin with the given name is registered.""" + return self.get_plugin(name) is not None + + def get_name(self, plugin: _Plugin) -> str | None: + """Return the name the plugin is registered under, or ``None`` if + is isn't.""" + for name, val in self._name2plugin.items(): + if plugin == val: + return name + return None + + def _verify_hook(self, hook: HookCaller, hookimpl: HookImpl) -> None: + if hook.is_historic() and (hookimpl.hookwrapper or hookimpl.wrapper): + raise PluginValidationError( + hookimpl.plugin, + f"Plugin {hookimpl.plugin_name!r}\nhook {hook.name!r}\n" + "historic incompatible with yield/wrapper/hookwrapper", + ) + + assert hook.spec is not None + if hook.spec.warn_on_impl: + _warn_for_function(hook.spec.warn_on_impl, hookimpl.function) + + # positional arg checking + notinspec = set(hookimpl.argnames) - set(hook.spec.argnames) + if notinspec: + raise PluginValidationError( + hookimpl.plugin, + f"Plugin {hookimpl.plugin_name!r} for hook {hook.name!r}\n" + f"hookimpl definition: {_formatdef(hookimpl.function)}\n" + f"Argument(s) {notinspec} are declared in the hookimpl but " + "can not be found in the hookspec", + ) + + if hook.spec.warn_on_impl_args: + for hookimpl_argname in hookimpl.argnames: + argname_warning = hook.spec.warn_on_impl_args.get(hookimpl_argname) + if argname_warning is not None: + _warn_for_function(argname_warning, hookimpl.function) + + if ( + hookimpl.wrapper or hookimpl.hookwrapper + ) and not inspect.isgeneratorfunction(hookimpl.function): + raise PluginValidationError( + hookimpl.plugin, + f"Plugin {hookimpl.plugin_name!r} for hook {hook.name!r}\n" + f"hookimpl definition: {_formatdef(hookimpl.function)}\n" + "Declared as wrapper=True or hookwrapper=True " + "but function is not a generator function", + ) + + if hookimpl.wrapper and hookimpl.hookwrapper: + raise PluginValidationError( + hookimpl.plugin, + f"Plugin {hookimpl.plugin_name!r} for hook {hook.name!r}\n" + f"hookimpl definition: {_formatdef(hookimpl.function)}\n" + "The wrapper=True and hookwrapper=True options are mutually exclusive", + ) + + def check_pending(self) -> None: + """Verify that all hooks which have not been verified against a + hook specification are optional, otherwise raise + :exc:`PluginValidationError`.""" + for name in self.hook.__dict__: + if name[0] == "_": + continue + hook: HookCaller = getattr(self.hook, name) + if not hook.has_spec(): + for hookimpl in hook.get_hookimpls(): + if not hookimpl.optionalhook: + raise PluginValidationError( + hookimpl.plugin, + f"unknown hook {name!r} in plugin {hookimpl.plugin!r}", + ) + + def load_setuptools_entrypoints(self, group: str, name: str | None = None) -> int: + """Load modules from querying the specified setuptools ``group``. + + :param group: + Entry point group to load plugins. + :param name: + If given, loads only plugins with the given ``name``. + + :return: + The number of plugins loaded by this call. + """ + import importlib.metadata + + count = 0 + for dist in list(importlib.metadata.distributions()): + for ep in dist.entry_points: + if ( + ep.group != group + or (name is not None and ep.name != name) + # already registered + or self.get_plugin(ep.name) + or self.is_blocked(ep.name) + ): + continue + plugin = ep.load() + self.register(plugin, name=ep.name) + self._plugin_distinfo.append((plugin, DistFacade(dist))) + count += 1 + return count + + def list_plugin_distinfo(self) -> list[tuple[_Plugin, DistFacade]]: + """Return a list of (plugin, distinfo) pairs for all + setuptools-registered plugins.""" + return list(self._plugin_distinfo) + + def list_name_plugin(self) -> list[tuple[str, _Plugin]]: + """Return a list of (name, plugin) pairs for all registered plugins.""" + return list(self._name2plugin.items()) + + def get_hookcallers(self, plugin: _Plugin) -> list[HookCaller] | None: + """Get all hook callers for the specified plugin. + + :returns: + The hook callers, or ``None`` if ``plugin`` is not registered in + this plugin manager. + """ + if self.get_name(plugin) is None: + return None + hookcallers = [] + for hookcaller in self.hook.__dict__.values(): + for hookimpl in hookcaller.get_hookimpls(): + if hookimpl.plugin is plugin: + hookcallers.append(hookcaller) + return hookcallers + + def add_hookcall_monitoring( + self, before: _BeforeTrace, after: _AfterTrace + ) -> Callable[[], None]: + """Add before/after tracing functions for all hooks. + + Returns an undo function which, when called, removes the added tracers. + + ``before(hook_name, hook_impls, kwargs)`` will be called ahead + of all hook calls and receive a hookcaller instance, a list + of HookImpl instances and the keyword arguments for the hook call. + + ``after(outcome, hook_name, hook_impls, kwargs)`` receives the + same arguments as ``before`` but also a :class:`~pluggy.Result` object + which represents the result of the overall hook call. + """ + oldcall = self._inner_hookexec + + def traced_hookexec( + hook_name: str, + hook_impls: Sequence[HookImpl], + caller_kwargs: Mapping[str, object], + firstresult: bool, + ) -> object | list[object]: + before(hook_name, hook_impls, caller_kwargs) + outcome = Result.from_call( + lambda: oldcall(hook_name, hook_impls, caller_kwargs, firstresult) + ) + after(outcome, hook_name, hook_impls, caller_kwargs) + return outcome.get_result() + + self._inner_hookexec = traced_hookexec + + def undo() -> None: + self._inner_hookexec = oldcall + + return undo + + def enable_tracing(self) -> Callable[[], None]: + """Enable tracing of hook calls. + + Returns an undo function which, when called, removes the added tracing. + """ + hooktrace = self.trace.root.get("hook") + + def before( + hook_name: str, methods: Sequence[HookImpl], kwargs: Mapping[str, object] + ) -> None: + hooktrace.root.indent += 1 + hooktrace(hook_name, kwargs) + + def after( + outcome: Result[object], + hook_name: str, + methods: Sequence[HookImpl], + kwargs: Mapping[str, object], + ) -> None: + if outcome.exception is None: + hooktrace("finish", hook_name, "-->", outcome.get_result()) + hooktrace.root.indent -= 1 + + return self.add_hookcall_monitoring(before, after) + + def subset_hook_caller( + self, name: str, remove_plugins: Iterable[_Plugin] + ) -> HookCaller: + """Return a proxy :class:`~pluggy.HookCaller` instance for the named + method which manages calls to all registered plugins except the ones + from remove_plugins.""" + orig: HookCaller = getattr(self.hook, name) + plugins_to_remove = {plug for plug in remove_plugins if hasattr(plug, name)} + if plugins_to_remove: + return _SubsetHookCaller(orig, plugins_to_remove) + return orig + + +def _formatdef(func: Callable[..., object]) -> str: + return f"{func.__name__}{inspect.signature(func)}" diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy/_result.py b/Backend/venv/lib/python3.12/site-packages/pluggy/_result.py new file mode 100644 index 00000000..656a5841 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pluggy/_result.py @@ -0,0 +1,107 @@ +""" +Hook wrapper "result" utilities. +""" + +from __future__ import annotations + +from types import TracebackType +from typing import Callable +from typing import cast +from typing import final +from typing import Generic +from typing import Optional +from typing import TypeVar + + +_ExcInfo = tuple[type[BaseException], BaseException, Optional[TracebackType]] +ResultType = TypeVar("ResultType") + + +class HookCallError(Exception): + """Hook was called incorrectly.""" + + +@final +class Result(Generic[ResultType]): + """An object used to inspect and set the result in a :ref:`hook wrapper + `.""" + + __slots__ = ("_result", "_exception", "_traceback") + + def __init__( + self, + result: ResultType | None, + exception: BaseException | None, + ) -> None: + """:meta private:""" + self._result = result + self._exception = exception + # Exception __traceback__ is mutable, this keeps the original. + self._traceback = exception.__traceback__ if exception is not None else None + + @property + def excinfo(self) -> _ExcInfo | None: + """:meta private:""" + exc = self._exception + if exc is None: + return None + else: + return (type(exc), exc, self._traceback) + + @property + def exception(self) -> BaseException | None: + """:meta private:""" + return self._exception + + @classmethod + def from_call(cls, func: Callable[[], ResultType]) -> Result[ResultType]: + """:meta private:""" + __tracebackhide__ = True + result = exception = None + try: + result = func() + except BaseException as exc: + exception = exc + return cls(result, exception) + + def force_result(self, result: ResultType) -> None: + """Force the result(s) to ``result``. + + If the hook was marked as a ``firstresult`` a single value should + be set, otherwise set a (modified) list of results. Any exceptions + found during invocation will be deleted. + + This overrides any previous result or exception. + """ + self._result = result + self._exception = None + self._traceback = None + + def force_exception(self, exception: BaseException) -> None: + """Force the result to fail with ``exception``. + + This overrides any previous result or exception. + + .. versionadded:: 1.1.0 + """ + self._result = None + self._exception = exception + self._traceback = exception.__traceback__ if exception is not None else None + + def get_result(self) -> ResultType: + """Get the result(s) for this hook call. + + If the hook was marked as a ``firstresult`` only a single value + will be returned, otherwise a list of results. + """ + __tracebackhide__ = True + exc = self._exception + tb = self._traceback + if exc is None: + return cast(ResultType, self._result) + else: + raise exc.with_traceback(tb) + + +# Historical name (pluggy<=1.2), kept for backward compatibility. +_Result = Result diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy/_tracing.py b/Backend/venv/lib/python3.12/site-packages/pluggy/_tracing.py new file mode 100644 index 00000000..f0b36db1 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pluggy/_tracing.py @@ -0,0 +1,72 @@ +""" +Tracing utils +""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any +from typing import Callable + + +_Writer = Callable[[str], object] +_Processor = Callable[[tuple[str, ...], tuple[Any, ...]], object] + + +class TagTracer: + def __init__(self) -> None: + self._tags2proc: dict[tuple[str, ...], _Processor] = {} + self._writer: _Writer | None = None + self.indent = 0 + + def get(self, name: str) -> TagTracerSub: + return TagTracerSub(self, (name,)) + + def _format_message(self, tags: Sequence[str], args: Sequence[object]) -> str: + if isinstance(args[-1], dict): + extra = args[-1] + args = args[:-1] + else: + extra = {} + + content = " ".join(map(str, args)) + indent = " " * self.indent + + lines = ["{}{} [{}]\n".format(indent, content, ":".join(tags))] + + for name, value in extra.items(): + lines.append(f"{indent} {name}: {value}\n") + + return "".join(lines) + + def _processmessage(self, tags: tuple[str, ...], args: tuple[object, ...]) -> None: + if self._writer is not None and args: + self._writer(self._format_message(tags, args)) + try: + processor = self._tags2proc[tags] + except KeyError: + pass + else: + processor(tags, args) + + def setwriter(self, writer: _Writer | None) -> None: + self._writer = writer + + def setprocessor(self, tags: str | tuple[str, ...], processor: _Processor) -> None: + if isinstance(tags, str): + tags = tuple(tags.split(":")) + else: + assert isinstance(tags, tuple) + self._tags2proc[tags] = processor + + +class TagTracerSub: + def __init__(self, root: TagTracer, tags: tuple[str, ...]) -> None: + self.root = root + self.tags = tags + + def __call__(self, *args: object) -> None: + self.root._processmessage(self.tags, args) + + def get(self, name: str) -> TagTracerSub: + return self.__class__(self.root, self.tags + (name,)) diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy/_version.py b/Backend/venv/lib/python3.12/site-packages/pluggy/_version.py new file mode 100644 index 00000000..6b8420c0 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pluggy/_version.py @@ -0,0 +1,21 @@ +# file generated by setuptools-scm +# don't change, don't track in version control + +__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple + from typing import Union + + VERSION_TUPLE = Tuple[Union[int, str], ...] +else: + VERSION_TUPLE = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE + +__version__ = version = '1.6.0' +__version_tuple__ = version_tuple = (1, 6, 0) diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy/_warnings.py b/Backend/venv/lib/python3.12/site-packages/pluggy/_warnings.py new file mode 100644 index 00000000..6356c770 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pluggy/_warnings.py @@ -0,0 +1,27 @@ +from typing import final + + +class PluggyWarning(UserWarning): + """Base class for all warnings emitted by pluggy.""" + + __module__ = "pluggy" + + +@final +class PluggyTeardownRaisedWarning(PluggyWarning): + """A plugin raised an exception during an :ref:`old-style hookwrapper + ` teardown. + + Such exceptions are not handled by pluggy, and may cause subsequent + teardowns to be executed at unexpected times, or be skipped entirely. + + This is an issue in the plugin implementation. + + If the exception is unintended, fix the underlying cause. + + If the exception is intended, switch to :ref:`new-style hook wrappers + `, or use :func:`result.force_exception() + ` to set the exception instead of raising. + """ + + __module__ = "pluggy" diff --git a/Backend/venv/lib/python3.12/site-packages/pluggy/py.typed b/Backend/venv/lib/python3.12/site-packages/pluggy/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/Backend/venv/lib/python3.12/site-packages/py.py b/Backend/venv/lib/python3.12/site-packages/py.py new file mode 100644 index 00000000..5c661e66 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/py.py @@ -0,0 +1,15 @@ +# shim for pylib going away +# if pylib is installed this file will get skipped +# (`py/__init__.py` has higher precedence) +from __future__ import annotations + +import sys + +import _pytest._py.error as error +import _pytest._py.path as path + + +sys.modules["py.error"] = error +sys.modules["py.path"] = path + +__all__ = ["error", "path"] diff --git a/Backend/venv/lib/python3.12/site-packages/pydantic/__pycache__/functional_validators.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pydantic/__pycache__/functional_validators.cpython-312.pyc index c41c311ecc5fe0ca3e76daa1d7f5d218c1698d45..99005068631083e80a612e1f2cb4f601fd86154d 100644 GIT binary patch delta 22 ccmZp;$k=d^k^3|+FBbz4JdIG;$X%KN08l6fl>h($ delta 22 ccmZp;$k=d^k^3|+FBbz4)LfI?$X%KN08knRqW}N^ diff --git a/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/INSTALLER b/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/METADATA b/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/METADATA new file mode 100644 index 00000000..2eff6a0c --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/METADATA @@ -0,0 +1,58 @@ +Metadata-Version: 2.4 +Name: Pygments +Version: 2.19.2 +Summary: Pygments is a syntax highlighting package written in Python. +Project-URL: Homepage, https://pygments.org +Project-URL: Documentation, https://pygments.org/docs +Project-URL: Source, https://github.com/pygments/pygments +Project-URL: Bug Tracker, https://github.com/pygments/pygments/issues +Project-URL: Changelog, https://github.com/pygments/pygments/blob/master/CHANGES +Author-email: Georg Brandl +Maintainer: Matthäus G. Chajdas +Maintainer-email: Georg Brandl , Jean Abou Samra +License: BSD-2-Clause +License-File: AUTHORS +License-File: LICENSE +Keywords: syntax highlighting +Classifier: Development Status :: 6 - Mature +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: End Users/Desktop +Classifier: Intended Audience :: System Administrators +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Text Processing :: Filters +Classifier: Topic :: Utilities +Requires-Python: >=3.8 +Provides-Extra: plugins +Provides-Extra: windows-terminal +Requires-Dist: colorama>=0.4.6; extra == 'windows-terminal' +Description-Content-Type: text/x-rst + +Pygments +~~~~~~~~ + +Pygments is a syntax highlighting package written in Python. + +It is a generic syntax highlighter suitable for use in code hosting, forums, +wikis or other applications that need to prettify source code. Highlights +are: + +* a wide range of over 500 languages and other text formats is supported +* special attention is paid to details, increasing quality by a fair amount +* support for new languages and formats are added easily +* a number of output formats, presently HTML, LaTeX, RTF, SVG, all image + formats that PIL supports and ANSI sequences +* it is usable as a command-line tool and as a library + +Copyright 2006-2025 by the Pygments team, see ``AUTHORS``. +Licensed under the BSD, see ``LICENSE`` for details. diff --git a/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/RECORD b/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/RECORD new file mode 100644 index 00000000..f4d51844 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/RECORD @@ -0,0 +1,684 @@ +../../../bin/pygmentize,sha256=ghpZ_TR_z61Et86zpsyj0Putnh2sCbRNRxBuDwvzQhI,227 +pygments-2.19.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +pygments-2.19.2.dist-info/METADATA,sha256=euEA1n1nAGxkeYA92DX89HqbWfrHlEQeqOZqp_WYTYI,2512 +pygments-2.19.2.dist-info/RECORD,, +pygments-2.19.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87 +pygments-2.19.2.dist-info/entry_points.txt,sha256=uUXw-XhMKBEX4pWcCtpuTTnPhL3h7OEE2jWi51VQsa8,53 +pygments-2.19.2.dist-info/licenses/AUTHORS,sha256=BmDjGKbyFYAq3Icxq4XQxl_yfPzKP10oWX8wZHYZW9k,10824 +pygments-2.19.2.dist-info/licenses/LICENSE,sha256=qdZvHVJt8C4p3Oc0NtNOVuhjL0bCdbvf_HBWnogvnxc,1331 +pygments/__init__.py,sha256=_3UT86TGpHuW8FekdZ8uLidEZH1NhmcLiOy2KKNPCt4,2959 +pygments/__main__.py,sha256=p8AJyoyCOMYGvzWHdnq0_A9qaaVqaj02nIu3xhJp1_4,348 +pygments/__pycache__/__init__.cpython-312.pyc,, +pygments/__pycache__/__main__.cpython-312.pyc,, +pygments/__pycache__/cmdline.cpython-312.pyc,, +pygments/__pycache__/console.cpython-312.pyc,, +pygments/__pycache__/filter.cpython-312.pyc,, +pygments/__pycache__/formatter.cpython-312.pyc,, +pygments/__pycache__/lexer.cpython-312.pyc,, +pygments/__pycache__/modeline.cpython-312.pyc,, +pygments/__pycache__/plugin.cpython-312.pyc,, +pygments/__pycache__/regexopt.cpython-312.pyc,, +pygments/__pycache__/scanner.cpython-312.pyc,, +pygments/__pycache__/sphinxext.cpython-312.pyc,, +pygments/__pycache__/style.cpython-312.pyc,, +pygments/__pycache__/token.cpython-312.pyc,, +pygments/__pycache__/unistring.cpython-312.pyc,, +pygments/__pycache__/util.cpython-312.pyc,, +pygments/cmdline.py,sha256=4pL9Kpn2PUEKPobgrsQgg-vCx2NjsrapKzQ6LxQR7Q0,23536 +pygments/console.py,sha256=AagDWqwea2yBWf10KC9ptBgMpMjxKp8yABAmh-NQOVk,1718 +pygments/filter.py,sha256=YLtpTnZiu07nY3oK9nfR6E9Y1FBHhP5PX8gvkJWcfag,1910 +pygments/filters/__init__.py,sha256=B00KqPCQh5E0XhzaDK74Qa1E4fDSTlD6b0Pvr1v-vEQ,40344 +pygments/filters/__pycache__/__init__.cpython-312.pyc,, +pygments/formatter.py,sha256=H_4J-moKkKfRWUOW9J0u7hhw6n1LiO-2Xu1q2B0sE5w,4366 +pygments/formatters/__init__.py,sha256=7OuvmoYLyoPzoOQV_brHG8GSKYB_wjFSkAQng6x2y9g,5349 +pygments/formatters/__pycache__/__init__.cpython-312.pyc,, +pygments/formatters/__pycache__/_mapping.cpython-312.pyc,, +pygments/formatters/__pycache__/bbcode.cpython-312.pyc,, +pygments/formatters/__pycache__/groff.cpython-312.pyc,, +pygments/formatters/__pycache__/html.cpython-312.pyc,, +pygments/formatters/__pycache__/img.cpython-312.pyc,, +pygments/formatters/__pycache__/irc.cpython-312.pyc,, +pygments/formatters/__pycache__/latex.cpython-312.pyc,, +pygments/formatters/__pycache__/other.cpython-312.pyc,, +pygments/formatters/__pycache__/pangomarkup.cpython-312.pyc,, +pygments/formatters/__pycache__/rtf.cpython-312.pyc,, +pygments/formatters/__pycache__/svg.cpython-312.pyc,, +pygments/formatters/__pycache__/terminal.cpython-312.pyc,, +pygments/formatters/__pycache__/terminal256.cpython-312.pyc,, +pygments/formatters/_mapping.py,sha256=1Cw37FuQlNacnxRKmtlPX4nyLoX9_ttko5ZwscNUZZ4,4176 +pygments/formatters/bbcode.py,sha256=s0Ka35OKuIchoSgEAGf6rj0rl2a9ym9L31JVNSRbZFQ,3296 +pygments/formatters/groff.py,sha256=pLcIHj4jJS_lRAVFnyJODKDu1Xlyl9_AEIdOtbl3DT0,5082 +pygments/formatters/html.py,sha256=FrHJ69FUliEyPY0zTfab0C1gPf7LXsKgeRlhwkniqIs,35953 +pygments/formatters/img.py,sha256=aRpFo8mBmWTL3sBUjRCWkeS3rc6FZrSFC4EksDrl53g,23301 +pygments/formatters/irc.py,sha256=R0Js0TYWySlI2yE9sW6tN4d4X-x3k9ZmudsijGPnLmU,4945 +pygments/formatters/latex.py,sha256=BRYtbLeW_YD1kwhhnFInhJIKylurnri8CF1lP069KWE,19258 +pygments/formatters/other.py,sha256=8pYW27sU_7XicLUqOEt2yWSO0h1IEUM3TIv34KODLwo,4986 +pygments/formatters/pangomarkup.py,sha256=pcFvEC7K1Me0EjGeOZth4oCnEY85bfqc77XzZASEPpY,2206 +pygments/formatters/rtf.py,sha256=kcKMCxTXu-2-hpgEftlGJRm7Ss-yA_Sy8OsHH_qzykA,11921 +pygments/formatters/svg.py,sha256=R6A2ME6JsMQWFiyn8wcKwFUOD6vsu-HLwiIztLu-77E,7138 +pygments/formatters/terminal.py,sha256=J_F_dFXwR9LHWvatIDnwqRYJyjVmSo1Zx8K_XDh6SyM,4626 +pygments/formatters/terminal256.py,sha256=7GQFLE5cfmeu53CAzANO74-kBk2BFkXfn5phmZjYkhM,11717 +pygments/lexer.py,sha256=ib-F_0GxHkwGpb6vWP0DeLMLc7EYgjo3hWFKN5IgOq0,35109 +pygments/lexers/__init__.py,sha256=6YhzxGKlWk38P6JpIJUQ1rVvV0DEZjEmdYsdMQ58hSk,12067 +pygments/lexers/__pycache__/__init__.cpython-312.pyc,, +pygments/lexers/__pycache__/_ada_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_asy_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_cl_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_cocoa_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_csound_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_css_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_googlesql_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_julia_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_lasso_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_lilypond_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_lua_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_luau_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_mapping.cpython-312.pyc,, +pygments/lexers/__pycache__/_mql_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_mysql_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_openedge_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_php_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_postgres_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_qlik_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_scheme_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_scilab_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_sourcemod_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_sql_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_stan_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_stata_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_tsql_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_usd_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_vbscript_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/_vim_builtins.cpython-312.pyc,, +pygments/lexers/__pycache__/actionscript.cpython-312.pyc,, +pygments/lexers/__pycache__/ada.cpython-312.pyc,, +pygments/lexers/__pycache__/agile.cpython-312.pyc,, +pygments/lexers/__pycache__/algebra.cpython-312.pyc,, +pygments/lexers/__pycache__/ambient.cpython-312.pyc,, +pygments/lexers/__pycache__/amdgpu.cpython-312.pyc,, +pygments/lexers/__pycache__/ampl.cpython-312.pyc,, +pygments/lexers/__pycache__/apdlexer.cpython-312.pyc,, +pygments/lexers/__pycache__/apl.cpython-312.pyc,, +pygments/lexers/__pycache__/archetype.cpython-312.pyc,, +pygments/lexers/__pycache__/arrow.cpython-312.pyc,, +pygments/lexers/__pycache__/arturo.cpython-312.pyc,, +pygments/lexers/__pycache__/asc.cpython-312.pyc,, +pygments/lexers/__pycache__/asm.cpython-312.pyc,, +pygments/lexers/__pycache__/asn1.cpython-312.pyc,, +pygments/lexers/__pycache__/automation.cpython-312.pyc,, +pygments/lexers/__pycache__/bare.cpython-312.pyc,, +pygments/lexers/__pycache__/basic.cpython-312.pyc,, +pygments/lexers/__pycache__/bdd.cpython-312.pyc,, +pygments/lexers/__pycache__/berry.cpython-312.pyc,, +pygments/lexers/__pycache__/bibtex.cpython-312.pyc,, +pygments/lexers/__pycache__/blueprint.cpython-312.pyc,, +pygments/lexers/__pycache__/boa.cpython-312.pyc,, +pygments/lexers/__pycache__/bqn.cpython-312.pyc,, +pygments/lexers/__pycache__/business.cpython-312.pyc,, +pygments/lexers/__pycache__/c_cpp.cpython-312.pyc,, +pygments/lexers/__pycache__/c_like.cpython-312.pyc,, +pygments/lexers/__pycache__/capnproto.cpython-312.pyc,, +pygments/lexers/__pycache__/carbon.cpython-312.pyc,, +pygments/lexers/__pycache__/cddl.cpython-312.pyc,, +pygments/lexers/__pycache__/chapel.cpython-312.pyc,, +pygments/lexers/__pycache__/clean.cpython-312.pyc,, +pygments/lexers/__pycache__/codeql.cpython-312.pyc,, +pygments/lexers/__pycache__/comal.cpython-312.pyc,, +pygments/lexers/__pycache__/compiled.cpython-312.pyc,, +pygments/lexers/__pycache__/configs.cpython-312.pyc,, +pygments/lexers/__pycache__/console.cpython-312.pyc,, +pygments/lexers/__pycache__/cplint.cpython-312.pyc,, +pygments/lexers/__pycache__/crystal.cpython-312.pyc,, +pygments/lexers/__pycache__/csound.cpython-312.pyc,, +pygments/lexers/__pycache__/css.cpython-312.pyc,, +pygments/lexers/__pycache__/d.cpython-312.pyc,, +pygments/lexers/__pycache__/dalvik.cpython-312.pyc,, +pygments/lexers/__pycache__/data.cpython-312.pyc,, +pygments/lexers/__pycache__/dax.cpython-312.pyc,, +pygments/lexers/__pycache__/devicetree.cpython-312.pyc,, +pygments/lexers/__pycache__/diff.cpython-312.pyc,, +pygments/lexers/__pycache__/dns.cpython-312.pyc,, +pygments/lexers/__pycache__/dotnet.cpython-312.pyc,, +pygments/lexers/__pycache__/dsls.cpython-312.pyc,, +pygments/lexers/__pycache__/dylan.cpython-312.pyc,, +pygments/lexers/__pycache__/ecl.cpython-312.pyc,, +pygments/lexers/__pycache__/eiffel.cpython-312.pyc,, +pygments/lexers/__pycache__/elm.cpython-312.pyc,, +pygments/lexers/__pycache__/elpi.cpython-312.pyc,, +pygments/lexers/__pycache__/email.cpython-312.pyc,, +pygments/lexers/__pycache__/erlang.cpython-312.pyc,, +pygments/lexers/__pycache__/esoteric.cpython-312.pyc,, +pygments/lexers/__pycache__/ezhil.cpython-312.pyc,, +pygments/lexers/__pycache__/factor.cpython-312.pyc,, +pygments/lexers/__pycache__/fantom.cpython-312.pyc,, +pygments/lexers/__pycache__/felix.cpython-312.pyc,, +pygments/lexers/__pycache__/fift.cpython-312.pyc,, +pygments/lexers/__pycache__/floscript.cpython-312.pyc,, +pygments/lexers/__pycache__/forth.cpython-312.pyc,, +pygments/lexers/__pycache__/fortran.cpython-312.pyc,, +pygments/lexers/__pycache__/foxpro.cpython-312.pyc,, +pygments/lexers/__pycache__/freefem.cpython-312.pyc,, +pygments/lexers/__pycache__/func.cpython-312.pyc,, +pygments/lexers/__pycache__/functional.cpython-312.pyc,, +pygments/lexers/__pycache__/futhark.cpython-312.pyc,, +pygments/lexers/__pycache__/gcodelexer.cpython-312.pyc,, +pygments/lexers/__pycache__/gdscript.cpython-312.pyc,, +pygments/lexers/__pycache__/gleam.cpython-312.pyc,, +pygments/lexers/__pycache__/go.cpython-312.pyc,, +pygments/lexers/__pycache__/grammar_notation.cpython-312.pyc,, +pygments/lexers/__pycache__/graph.cpython-312.pyc,, +pygments/lexers/__pycache__/graphics.cpython-312.pyc,, +pygments/lexers/__pycache__/graphql.cpython-312.pyc,, +pygments/lexers/__pycache__/graphviz.cpython-312.pyc,, +pygments/lexers/__pycache__/gsql.cpython-312.pyc,, +pygments/lexers/__pycache__/hare.cpython-312.pyc,, +pygments/lexers/__pycache__/haskell.cpython-312.pyc,, +pygments/lexers/__pycache__/haxe.cpython-312.pyc,, +pygments/lexers/__pycache__/hdl.cpython-312.pyc,, +pygments/lexers/__pycache__/hexdump.cpython-312.pyc,, +pygments/lexers/__pycache__/html.cpython-312.pyc,, +pygments/lexers/__pycache__/idl.cpython-312.pyc,, +pygments/lexers/__pycache__/igor.cpython-312.pyc,, +pygments/lexers/__pycache__/inferno.cpython-312.pyc,, +pygments/lexers/__pycache__/installers.cpython-312.pyc,, +pygments/lexers/__pycache__/int_fiction.cpython-312.pyc,, +pygments/lexers/__pycache__/iolang.cpython-312.pyc,, +pygments/lexers/__pycache__/j.cpython-312.pyc,, +pygments/lexers/__pycache__/javascript.cpython-312.pyc,, +pygments/lexers/__pycache__/jmespath.cpython-312.pyc,, +pygments/lexers/__pycache__/jslt.cpython-312.pyc,, +pygments/lexers/__pycache__/json5.cpython-312.pyc,, +pygments/lexers/__pycache__/jsonnet.cpython-312.pyc,, +pygments/lexers/__pycache__/jsx.cpython-312.pyc,, +pygments/lexers/__pycache__/julia.cpython-312.pyc,, +pygments/lexers/__pycache__/jvm.cpython-312.pyc,, +pygments/lexers/__pycache__/kuin.cpython-312.pyc,, +pygments/lexers/__pycache__/kusto.cpython-312.pyc,, +pygments/lexers/__pycache__/ldap.cpython-312.pyc,, +pygments/lexers/__pycache__/lean.cpython-312.pyc,, +pygments/lexers/__pycache__/lilypond.cpython-312.pyc,, +pygments/lexers/__pycache__/lisp.cpython-312.pyc,, +pygments/lexers/__pycache__/macaulay2.cpython-312.pyc,, +pygments/lexers/__pycache__/make.cpython-312.pyc,, +pygments/lexers/__pycache__/maple.cpython-312.pyc,, +pygments/lexers/__pycache__/markup.cpython-312.pyc,, +pygments/lexers/__pycache__/math.cpython-312.pyc,, +pygments/lexers/__pycache__/matlab.cpython-312.pyc,, +pygments/lexers/__pycache__/maxima.cpython-312.pyc,, +pygments/lexers/__pycache__/meson.cpython-312.pyc,, +pygments/lexers/__pycache__/mime.cpython-312.pyc,, +pygments/lexers/__pycache__/minecraft.cpython-312.pyc,, +pygments/lexers/__pycache__/mips.cpython-312.pyc,, +pygments/lexers/__pycache__/ml.cpython-312.pyc,, +pygments/lexers/__pycache__/modeling.cpython-312.pyc,, +pygments/lexers/__pycache__/modula2.cpython-312.pyc,, +pygments/lexers/__pycache__/mojo.cpython-312.pyc,, +pygments/lexers/__pycache__/monte.cpython-312.pyc,, +pygments/lexers/__pycache__/mosel.cpython-312.pyc,, +pygments/lexers/__pycache__/ncl.cpython-312.pyc,, +pygments/lexers/__pycache__/nimrod.cpython-312.pyc,, +pygments/lexers/__pycache__/nit.cpython-312.pyc,, +pygments/lexers/__pycache__/nix.cpython-312.pyc,, +pygments/lexers/__pycache__/numbair.cpython-312.pyc,, +pygments/lexers/__pycache__/oberon.cpython-312.pyc,, +pygments/lexers/__pycache__/objective.cpython-312.pyc,, +pygments/lexers/__pycache__/ooc.cpython-312.pyc,, +pygments/lexers/__pycache__/openscad.cpython-312.pyc,, +pygments/lexers/__pycache__/other.cpython-312.pyc,, +pygments/lexers/__pycache__/parasail.cpython-312.pyc,, +pygments/lexers/__pycache__/parsers.cpython-312.pyc,, +pygments/lexers/__pycache__/pascal.cpython-312.pyc,, +pygments/lexers/__pycache__/pawn.cpython-312.pyc,, +pygments/lexers/__pycache__/pddl.cpython-312.pyc,, +pygments/lexers/__pycache__/perl.cpython-312.pyc,, +pygments/lexers/__pycache__/phix.cpython-312.pyc,, +pygments/lexers/__pycache__/php.cpython-312.pyc,, +pygments/lexers/__pycache__/pointless.cpython-312.pyc,, +pygments/lexers/__pycache__/pony.cpython-312.pyc,, +pygments/lexers/__pycache__/praat.cpython-312.pyc,, +pygments/lexers/__pycache__/procfile.cpython-312.pyc,, +pygments/lexers/__pycache__/prolog.cpython-312.pyc,, +pygments/lexers/__pycache__/promql.cpython-312.pyc,, +pygments/lexers/__pycache__/prql.cpython-312.pyc,, +pygments/lexers/__pycache__/ptx.cpython-312.pyc,, +pygments/lexers/__pycache__/python.cpython-312.pyc,, +pygments/lexers/__pycache__/q.cpython-312.pyc,, +pygments/lexers/__pycache__/qlik.cpython-312.pyc,, +pygments/lexers/__pycache__/qvt.cpython-312.pyc,, +pygments/lexers/__pycache__/r.cpython-312.pyc,, +pygments/lexers/__pycache__/rdf.cpython-312.pyc,, +pygments/lexers/__pycache__/rebol.cpython-312.pyc,, +pygments/lexers/__pycache__/rego.cpython-312.pyc,, +pygments/lexers/__pycache__/resource.cpython-312.pyc,, +pygments/lexers/__pycache__/ride.cpython-312.pyc,, +pygments/lexers/__pycache__/rita.cpython-312.pyc,, +pygments/lexers/__pycache__/rnc.cpython-312.pyc,, +pygments/lexers/__pycache__/roboconf.cpython-312.pyc,, +pygments/lexers/__pycache__/robotframework.cpython-312.pyc,, +pygments/lexers/__pycache__/ruby.cpython-312.pyc,, +pygments/lexers/__pycache__/rust.cpython-312.pyc,, +pygments/lexers/__pycache__/sas.cpython-312.pyc,, +pygments/lexers/__pycache__/savi.cpython-312.pyc,, +pygments/lexers/__pycache__/scdoc.cpython-312.pyc,, +pygments/lexers/__pycache__/scripting.cpython-312.pyc,, +pygments/lexers/__pycache__/sgf.cpython-312.pyc,, +pygments/lexers/__pycache__/shell.cpython-312.pyc,, +pygments/lexers/__pycache__/sieve.cpython-312.pyc,, +pygments/lexers/__pycache__/slash.cpython-312.pyc,, +pygments/lexers/__pycache__/smalltalk.cpython-312.pyc,, +pygments/lexers/__pycache__/smithy.cpython-312.pyc,, +pygments/lexers/__pycache__/smv.cpython-312.pyc,, +pygments/lexers/__pycache__/snobol.cpython-312.pyc,, +pygments/lexers/__pycache__/solidity.cpython-312.pyc,, +pygments/lexers/__pycache__/soong.cpython-312.pyc,, +pygments/lexers/__pycache__/sophia.cpython-312.pyc,, +pygments/lexers/__pycache__/special.cpython-312.pyc,, +pygments/lexers/__pycache__/spice.cpython-312.pyc,, +pygments/lexers/__pycache__/sql.cpython-312.pyc,, +pygments/lexers/__pycache__/srcinfo.cpython-312.pyc,, +pygments/lexers/__pycache__/stata.cpython-312.pyc,, +pygments/lexers/__pycache__/supercollider.cpython-312.pyc,, +pygments/lexers/__pycache__/tablegen.cpython-312.pyc,, +pygments/lexers/__pycache__/tact.cpython-312.pyc,, +pygments/lexers/__pycache__/tal.cpython-312.pyc,, +pygments/lexers/__pycache__/tcl.cpython-312.pyc,, +pygments/lexers/__pycache__/teal.cpython-312.pyc,, +pygments/lexers/__pycache__/templates.cpython-312.pyc,, +pygments/lexers/__pycache__/teraterm.cpython-312.pyc,, +pygments/lexers/__pycache__/testing.cpython-312.pyc,, +pygments/lexers/__pycache__/text.cpython-312.pyc,, +pygments/lexers/__pycache__/textedit.cpython-312.pyc,, +pygments/lexers/__pycache__/textfmts.cpython-312.pyc,, +pygments/lexers/__pycache__/theorem.cpython-312.pyc,, +pygments/lexers/__pycache__/thingsdb.cpython-312.pyc,, +pygments/lexers/__pycache__/tlb.cpython-312.pyc,, +pygments/lexers/__pycache__/tls.cpython-312.pyc,, +pygments/lexers/__pycache__/tnt.cpython-312.pyc,, +pygments/lexers/__pycache__/trafficscript.cpython-312.pyc,, +pygments/lexers/__pycache__/typoscript.cpython-312.pyc,, +pygments/lexers/__pycache__/typst.cpython-312.pyc,, +pygments/lexers/__pycache__/ul4.cpython-312.pyc,, +pygments/lexers/__pycache__/unicon.cpython-312.pyc,, +pygments/lexers/__pycache__/urbi.cpython-312.pyc,, +pygments/lexers/__pycache__/usd.cpython-312.pyc,, +pygments/lexers/__pycache__/varnish.cpython-312.pyc,, +pygments/lexers/__pycache__/verification.cpython-312.pyc,, +pygments/lexers/__pycache__/verifpal.cpython-312.pyc,, +pygments/lexers/__pycache__/vip.cpython-312.pyc,, +pygments/lexers/__pycache__/vyper.cpython-312.pyc,, +pygments/lexers/__pycache__/web.cpython-312.pyc,, +pygments/lexers/__pycache__/webassembly.cpython-312.pyc,, +pygments/lexers/__pycache__/webidl.cpython-312.pyc,, +pygments/lexers/__pycache__/webmisc.cpython-312.pyc,, +pygments/lexers/__pycache__/wgsl.cpython-312.pyc,, +pygments/lexers/__pycache__/whiley.cpython-312.pyc,, +pygments/lexers/__pycache__/wowtoc.cpython-312.pyc,, +pygments/lexers/__pycache__/wren.cpython-312.pyc,, +pygments/lexers/__pycache__/x10.cpython-312.pyc,, +pygments/lexers/__pycache__/xorg.cpython-312.pyc,, +pygments/lexers/__pycache__/yang.cpython-312.pyc,, +pygments/lexers/__pycache__/yara.cpython-312.pyc,, +pygments/lexers/__pycache__/zig.cpython-312.pyc,, +pygments/lexers/_ada_builtins.py,sha256=CA_OnShtdc7wWh9oYcRlcrkDAQwYUKl6w7tdSbALQd4,1543 +pygments/lexers/_asy_builtins.py,sha256=cd9M00YH19w5ZL7aqucmC3nwpJGTS04U-01NLy5E2_4,27287 +pygments/lexers/_cl_builtins.py,sha256=kQeUIyZjP4kX0frkICDcKxBYQCLqzIDXa5WV5cevhDo,13994 +pygments/lexers/_cocoa_builtins.py,sha256=Ka1lLJe7JfWtdho4IFIB82X9yBvrbfHCCmEG-peXXhQ,105173 +pygments/lexers/_csound_builtins.py,sha256=qnQYKeI26ZHim316uqy_hDiRiCoHo2RHjD3sYBALyXs,18414 +pygments/lexers/_css_builtins.py,sha256=aD-dhLFXVd1Atn_bZd7gEdQn7Mhe60_VHpvZ340WzDI,12446 +pygments/lexers/_googlesql_builtins.py,sha256=IkrOk-T2v1yzbGzUEEQh5_Cf4uC_cmL_uuhwDpZlTug,16132 +pygments/lexers/_julia_builtins.py,sha256=N2WdSw5zgI2fhDat_i4YeVqurRTC_P8x71ez00SCN6U,11883 +pygments/lexers/_lasso_builtins.py,sha256=8q1gbsrMJeaeUhxIYKhaOxC9j_B-NBpq_XFj2Ze41X0,134510 +pygments/lexers/_lilypond_builtins.py,sha256=XTbGL1z1oKMoqWLEktG33jx5GdGTI9CpeO5NheEi4Y0,108094 +pygments/lexers/_lua_builtins.py,sha256=PhFdZV5-Tzz2j_q4lvG9lr84ELGfL41BhnrSDNNTaG4,8108 +pygments/lexers/_luau_builtins.py,sha256=-IDrU04kUVfjXwSQzMMpXmMYhNsQxZVVZk8cuAA0Lo0,955 +pygments/lexers/_mapping.py,sha256=9fv7xYOUAOr6LzfdFS4MDbPu78o4OQQH-2nsI1bNZf4,70438 +pygments/lexers/_mql_builtins.py,sha256=ybRQjlb7Cul0sDstnzxJl3h0qS6Ieqsr811fqrxyumU,24713 +pygments/lexers/_mysql_builtins.py,sha256=y0kAWZVAs0z2dTFJJV42OZpILgRnd8T3zSlBFv-g_oA,25838 +pygments/lexers/_openedge_builtins.py,sha256=Sz4j9-CPWIaxMa-2fZgY66j7igcu1ob1GR2UtI8zAkg,49398 +pygments/lexers/_php_builtins.py,sha256=Jd4BZpjMDELPi4EVoSxK1-8BFTc63HUwYfm1rLrGj0M,107922 +pygments/lexers/_postgres_builtins.py,sha256=Pqh4z0RBRbnW6rCQtWUdzWCJxNyqpJ7_0HOktxHDxk4,13343 +pygments/lexers/_qlik_builtins.py,sha256=xuJy9c9uZDXv6h8z582P5PrxqkxTZ_nS8gPl9OD9VN8,12595 +pygments/lexers/_scheme_builtins.py,sha256=2hNtJOJmP21lUsikpqMJ2gAmLT3Rwn_KEeqhXwCjgfk,32564 +pygments/lexers/_scilab_builtins.py,sha256=oZYPB1XPdIEz3pII11pFDe6extRRyWGA7pY06X8KZ8w,52411 +pygments/lexers/_sourcemod_builtins.py,sha256=H8AFLsNDdEpymIWOpDwbDJGCP1w-x-1gSlzPDioMF4o,26777 +pygments/lexers/_sql_builtins.py,sha256=oe8F9wWuO2iS6nEsZAdJtCUChBTjgM1Sq_aipu74jXM,6767 +pygments/lexers/_stan_builtins.py,sha256=dwi1hllM_NsaCv-aXJy7lEi57X5Hh5gSD97aCQyT9KM,13445 +pygments/lexers/_stata_builtins.py,sha256=Hqrr6j77zWU3cGGpBPohwexZci43YA4_sVYE4E1sNow,27227 +pygments/lexers/_tsql_builtins.py,sha256=Pi2RhTXcLE3glI9oxNhyVsOMn-fK_1TRxJ-EsYP5LcI,15460 +pygments/lexers/_usd_builtins.py,sha256=c9hbU1cwqBUCFIhNfu_Dob8ywv1rlPhi9w2OTj3kR8s,1658 +pygments/lexers/_vbscript_builtins.py,sha256=MqJ2ABywD21aSRtWYZRG64CCbGstC1kfsiHGJmZzxiw,4225 +pygments/lexers/_vim_builtins.py,sha256=bA4mH8t1mPPQfEiUCKEqRO1O0rL2DUG0Ux1Bt8ZSu0E,57066 +pygments/lexers/actionscript.py,sha256=JBngCe5UhYT_0dLD2j7PnPO0xRRJhmypEuQ-C5in8pY,11727 +pygments/lexers/ada.py,sha256=58k5ra1vGS4iLpW3h1ItY9ftzF3WevaeAAXzAYTiYkQ,5353 +pygments/lexers/agile.py,sha256=DN-7AVIqtG1MshA94rtSGYI_884hVHgzq405wD0_dl8,896 +pygments/lexers/algebra.py,sha256=yGTu9Tt-cQzAISQYIC5MS5a3z4QmL-tGcXnd_pkWGbk,9952 +pygments/lexers/ambient.py,sha256=UnzKpIlfSm3iitHvMd7XTMSY8TjZYYhKOC3AiARS_cE,2605 +pygments/lexers/amdgpu.py,sha256=S8qjn2UMLhBFm3Yn_c06XAGf8cl5x_ZeluelWG_-JAw,1723 +pygments/lexers/ampl.py,sha256=ZBRfDXm760gR1a1gqItnsHuoO3JdUcTBjJ5tFY9UtPA,4176 +pygments/lexers/apdlexer.py,sha256=Zr5-jgjxC8PKzRlEeclakZXPHci7FHBZghQ6wwiuT7A,30800 +pygments/lexers/apl.py,sha256=PTQMp-bxT5P-DbrEvFha10HBTcsDJ5srL3I1s9ljz58,3404 +pygments/lexers/archetype.py,sha256=pQVlP1Fb5OA8nn7QwmFaaaOSvvpoIsQVw43FVCQCve4,11538 +pygments/lexers/arrow.py,sha256=2PKdbWq3xQLF1KoDbWvSxpjwKRrznnDiArTflRGZzBo,3564 +pygments/lexers/arturo.py,sha256=U5MtRNHJtnBn4ZOeWmW6MKlVRG7SX6KhTRamDqzn9tA,11414 +pygments/lexers/asc.py,sha256=-DgZl9jccBDHPlDmjCsrEqx0-Q7ap7XVdNKtxLNWG1w,1693 +pygments/lexers/asm.py,sha256=xm2Y5mcT-sF3oQvair4SWs9EWTyndoaUoSsDy5v6shI,41967 +pygments/lexers/asn1.py,sha256=BlcloIX2bu6Q7BxGcksuhYFHGsXLVKyB4B9mFd4Pj6E,4262 +pygments/lexers/automation.py,sha256=Q61qon8EwpfakMh_2MS2E2zUUT16rG3UNIKPYjITeTs,19831 +pygments/lexers/bare.py,sha256=tWoei86JJX1k-ADhaXd5TgX6ItDTici9yFWpkTPhnfM,3020 +pygments/lexers/basic.py,sha256=qpVe5h8Fa7NJo1EihN-4R_UZpHO6my2Ssgkb-BktkKs,27989 +pygments/lexers/bdd.py,sha256=yysefcOFAEyk9kJ2y4EXmzJTecgLYUHlWixt_3YzPMU,1641 +pygments/lexers/berry.py,sha256=zxGowFb8HMIyN15-m8nmWnW6bPRR4esKtSEVugc9uXM,3209 +pygments/lexers/bibtex.py,sha256=yuNoPxwrJf9DCGUT17hxfDzbq_HtCLkQkRbBtiTVmeQ,4811 +pygments/lexers/blueprint.py,sha256=NzvWHMxCLDWt8hc6gB5jokltxVJgNa7Jwh4c61ng388,6188 +pygments/lexers/boa.py,sha256=dOot1XWNZThPIio2UyAX67K6EpISjSRCFjotD7dcnwE,3921 +pygments/lexers/bqn.py,sha256=nJiwrPKKbRF-qdai5tfqipwBkkko2P3weiZAjHUMimY,3671 +pygments/lexers/business.py,sha256=lRtekOJfsDkb12AGbuz10-G67OJrVJgCBtihTQ8_aoY,28345 +pygments/lexers/c_cpp.py,sha256=D7ZIswaHASlGBgoTlwnSqTQHf8_JyvvSt2L2q1W-F6g,18059 +pygments/lexers/c_like.py,sha256=FTGp17ds6X2rDZOHup2hH6BEn3gKK4nLm9pydNEhm0E,32021 +pygments/lexers/capnproto.py,sha256=XQJAh1WS-0ulqbTn9TdzR6gEgWLcuBqb4sj3jNsrhsY,2174 +pygments/lexers/carbon.py,sha256=av12YuTGZGpOa1Cmxp3lppx3LfSJUWbvOu0ixmUVll0,3211 +pygments/lexers/cddl.py,sha256=MKa70IwABgjBjYu15_Q9v8rsu2sr1a-i2jkiaPTI6sM,5076 +pygments/lexers/chapel.py,sha256=0n_fL3ehLC4pw4YKnmq9jxIXOJcxGPka1Wr1t1zsXPc,5156 +pygments/lexers/clean.py,sha256=dkDPAwF5BTALPeuKFoRKOSD3RfsKcGWbaRo6_G8LHng,6418 +pygments/lexers/codeql.py,sha256=ebvghn2zbrnETV4buVozMDmRCVKSdGiIN8ycLlHpGsE,2576 +pygments/lexers/comal.py,sha256=TC3NzcJ58ew5jw7qwK0kJ-okTA47psZje0yAIS39HR4,3179 +pygments/lexers/compiled.py,sha256=Slfo1sjWqcPawUwf0dIIZLBCL5pkOIoAX2S8Lxs02Mc,1426 +pygments/lexers/configs.py,sha256=wW8pY0Sa5a10pnAeTLGf48HhixQTVageIyHEf1aYMCc,50913 +pygments/lexers/console.py,sha256=-jAG120dupvV3kG3zC70brLJvSLwTFqMubBQuj_GVnU,4180 +pygments/lexers/cplint.py,sha256=DkbyE5EKydLgf6BRr1FhQrK-IeQPL7Zmjk0DVdlRFnQ,1389 +pygments/lexers/crystal.py,sha256=xU-RnpIkpjrquoxtOuOcP8fcesSJl4xhU7kO9m42LZY,15754 +pygments/lexers/csound.py,sha256=ioSw4Q04wdwjUAbnTZ1qLhUq1vxdWFxhh3QtEl5RAJc,16998 +pygments/lexers/css.py,sha256=JN1RBYsee-jrpHWrSmhN3TKc4TkOBn-_BEGpgTCzcqE,25376 +pygments/lexers/d.py,sha256=piOy0EJeiAwPHugiM3gVv0z7HNh3u2gZQoCUSASRbY4,9920 +pygments/lexers/dalvik.py,sha256=deFg2JPBktJ9mEGb9EgxNkmd6vaMjJFQVzUHo8NKIa8,4606 +pygments/lexers/data.py,sha256=o0x0SmB5ms_CPUPljEEEenOON4IQWn86DkwFjkJYCOg,27026 +pygments/lexers/dax.py,sha256=ASi73qmr7OA7cVZXF2GTYGt01Ly1vY8CgD_Pnpm8k-4,8098 +pygments/lexers/devicetree.py,sha256=RecSQCidt8DRE1QFCPUbwwR0hiRlNtsFihdGldeUn3k,4019 +pygments/lexers/diff.py,sha256=F6vxZ64wm5Nag_97de1H_3F700ZwCVnYjKvtT5jilww,5382 +pygments/lexers/dns.py,sha256=Hh5hJ7MXfrq36KgfyIRwK3X8o1LdR98IKERcV4eZ7HY,3891 +pygments/lexers/dotnet.py,sha256=NDE0kOmpe96GLO-zwNLazmj77E9ORGmKpa4ZMCXDXxQ,39441 +pygments/lexers/dsls.py,sha256=GnHKhGL5GxsRFnqC7-65NTPZLOZdmnllNrGP86x_fQE,36746 +pygments/lexers/dylan.py,sha256=7zZ1EbHWXeVHqTD36AqykKqo3fhuIh4sM-whcxUaH_Y,10409 +pygments/lexers/ecl.py,sha256=vhmpa2LBrHxsPkYcf3kPZ1ItVaLRDTebi186wY0xGZA,6371 +pygments/lexers/eiffel.py,sha256=5ydYIEFcgcMoEj4BlK31hZ0aJb8OX0RdAvuCNdlxwqw,2690 +pygments/lexers/elm.py,sha256=uRCddU8jK5vVkH6Y66y8KOsDJprIfrOgeYq3hv1PxAM,3152 +pygments/lexers/elpi.py,sha256=O9j_WKBPyvNFjCRuPciVpW4etVSnILm_T79BhCPZYmo,6877 +pygments/lexers/email.py,sha256=ZZL6yvwCRl1CEQyysuOu0lbabp5tjMutS7f3efFKGR4,4804 +pygments/lexers/erlang.py,sha256=bU11eVHvooLwmVknzN6Xkb2DMk7HbenqdNlYSzhThDM,19147 +pygments/lexers/esoteric.py,sha256=Jfp8UUKyKYsqLaqXRZT3GSM9dzkF65zduwfnH1GoGhU,10500 +pygments/lexers/ezhil.py,sha256=22r-xjvvBVpExTqCI-HycAwunDb1p5gY4tIfDmM0vDw,3272 +pygments/lexers/factor.py,sha256=urZ4En4uKFCLXdEkXLWg9EYUFGHQTTDCwNXtyq-ngok,19530 +pygments/lexers/fantom.py,sha256=JJ13-NwykD-iIESnuzCefCYeQDO95cHMJA8TasF4gHA,10231 +pygments/lexers/felix.py,sha256=F-v0si4zPtRelqzDQWXI1-tarCE-BvawziODxRU7378,9655 +pygments/lexers/fift.py,sha256=rOCwp3v5ocK5YOWvt7Td3Md--97_8e-7Sonx52uS8mA,1644 +pygments/lexers/floscript.py,sha256=aHh82k52jMuDuzl9LatrcSANJiXTCyjGU3SO53bwbb0,2667 +pygments/lexers/forth.py,sha256=ZMtsHdNbnS_0IdSYlfAlfTSPEr0MEsRo-YZriQNueTQ,7193 +pygments/lexers/fortran.py,sha256=1PE5dTxf4Df6LUeXFcmNtyeXWsC8tSiK5dYwPHIJeeQ,10382 +pygments/lexers/foxpro.py,sha256=CBkW62Fuibz3yfyelZCaEO8GGdFJWsuRhqwtsSeBwLM,26295 +pygments/lexers/freefem.py,sha256=LFBQk-m1-nNCgrl-VDH3QwnVWurvb7W29i06LoT207A,26913 +pygments/lexers/func.py,sha256=OR2rkM7gf9fKvad5WcFQln-_U_pb-RUCM9eQatToF4A,3700 +pygments/lexers/functional.py,sha256=fYT2AGZ642cRkIAId0rnXFBsx1c8LLEDRN_VuCEkUyM,693 +pygments/lexers/futhark.py,sha256=Vf1i4t-tR3zqaktVjhTzFNg_ts_9CcyA4ZDfDizbCmk,3743 +pygments/lexers/gcodelexer.py,sha256=4Xs9ax4-JZGupW_qSnHon39wQGpb-tNA3xorMKg841E,874 +pygments/lexers/gdscript.py,sha256=Ws7JKxy0M0IyZ_1iMfRvJPrizEwmeCNLDoeMIFaM-CU,7566 +pygments/lexers/gleam.py,sha256=XIlTcq6cB743pCqbNYo8PocSkjZyDPR6hHgdaJNJ1Vc,2392 +pygments/lexers/go.py,sha256=4LezefgyuqZWHzLZHieUkKTi-ssY6aHJxx7Z-LFaLK0,3783 +pygments/lexers/grammar_notation.py,sha256=LvzhRQHgwZzq9oceukZS_hwnKK58ee7Z5d0cwXOR734,8043 +pygments/lexers/graph.py,sha256=WFqoPA1c_hHYrV0i_F7-eUw3Co4_HmZY3GJ-TyDr670,4108 +pygments/lexers/graphics.py,sha256=tmF9NNALnvPnax8ywYC3pLOla45YXtp9UA0H-5EiTQY,39145 +pygments/lexers/graphql.py,sha256=O_zcrGrBaDaKTlUoJGRruxqk7CJi-NR92Y0Cs-KkCvw,5601 +pygments/lexers/graphviz.py,sha256=mzdXOMpwz9_V-be1eTAMyhkKCBl6UxCIXuq6C2yrtsw,1934 +pygments/lexers/gsql.py,sha256=VPZk9sb26-DumRkWfEaSTeoc0lx5xt5n-6eDDLezMtc,3990 +pygments/lexers/hare.py,sha256=PGCOuILktJsmtTpCZZKkMFtObfJuBpei8HM8HHuq1Tw,2649 +pygments/lexers/haskell.py,sha256=MYr74-PAC8kGJRX-dZmvZsHTc7a2u6yFS2B19LfDD7g,33262 +pygments/lexers/haxe.py,sha256=WHCy_nrXHnfLITfbdp3Ji3lqQU4HAsTUpXsLCp2_4sk,30974 +pygments/lexers/hdl.py,sha256=MOWxhmAuE4Ei0CKDqqaON7T8tl43geancrNYM136Z0U,22738 +pygments/lexers/hexdump.py,sha256=1lj9oJ-KiZXSVYvTMfGmEAQzNEW08WlMcC2I5aYvHK4,3653 +pygments/lexers/html.py,sha256=MxYTI4EeT7QxoGleCAyQq-8n_Sgly6tD95H5zanCNmk,21977 +pygments/lexers/idl.py,sha256=rcihUAGhfuGEaSW6pgFq6NzplT_pv0DagUoefg4zAmk,15449 +pygments/lexers/igor.py,sha256=wVefbUjb3ftaW3LCKGtX1JgLgiY4EmRor5gVOn8vQA8,31633 +pygments/lexers/inferno.py,sha256=ChE_5y5SLH_75Uv7D2dKWQMk2dlN6z1gY1IDjlJZ8rU,3135 +pygments/lexers/installers.py,sha256=ZHliit4Pxz1tYKOIjKkDXI5djTkpzYUMVIPR1xvUrL8,14435 +pygments/lexers/int_fiction.py,sha256=0ZzIa1sZDUQsltd1oHuS-BoNiOF8zKQfcVuDyK1Ttv8,56544 +pygments/lexers/iolang.py,sha256=L6dNDCLH0kxkIUi00fI4Z14QnRu79UcNDrgv02c5Zw8,1905 +pygments/lexers/j.py,sha256=DqNdwQGFLiZW3mCNLRg81gpmsy4Hgcai_9NP3LbWhNU,4853 +pygments/lexers/javascript.py,sha256=TGKQLSrCprCKfhLLGAq_0EOdvqvJKX9pOdKo7tCRurQ,63243 +pygments/lexers/jmespath.py,sha256=R5yA5LJ2nTIaDwnFIpSNGAThd0sAYFccwawA9xBptlg,2082 +pygments/lexers/jslt.py,sha256=OeYQf8O2_9FCaf9W6Q3a7rPdAFLthePCtVSgCrOTcl8,3700 +pygments/lexers/json5.py,sha256=8JZbc8EiTEZdKaIdQg3hXEh0mHWSzPlwd473a0nUuT0,2502 +pygments/lexers/jsonnet.py,sha256=bx2G6J4tJqGrJV1PyZrIWzWHXcoefCX-4lIxxtbn2gw,5636 +pygments/lexers/jsx.py,sha256=wGsoGSB40qAJrVfXwRPtan7OcK0O87RVsHHk0m6gogk,2693 +pygments/lexers/julia.py,sha256=0ZDJ9X83V5GqJzA6T6p0TTN8WHy2JAjvu-FSBXvfXdc,11710 +pygments/lexers/jvm.py,sha256=Yt1iQ3QodXRY-x_HUOGedhyuBBHn5jYH-I8NzOzHTlE,72667 +pygments/lexers/kuin.py,sha256=3dKKJVJlskgrvMKv2tY9NOsFfDjyo-3MLcJ1lFKdXSg,11405 +pygments/lexers/kusto.py,sha256=kaxkoPpEBDsBTCvCOkZZx7oGfv0jk_UNIRIRbfVAsBE,3477 +pygments/lexers/ldap.py,sha256=77vF4t_19x9V522cxRCM5d3HW8Ne3giYsFsMPVYYBw4,6551 +pygments/lexers/lean.py,sha256=7HWRgxFsxS1N9XKqw0vfKwaxl27s5YiVYtZeRUoTHFo,8570 +pygments/lexers/lilypond.py,sha256=yd2Tuv67um6EyCIr-VwBnlPhTHxMaQsBJ4nGgO5fjIk,9752 +pygments/lexers/lisp.py,sha256=EHUy1g4pzEsYPE-zGj2rAXm3YATE1j9dCQOr5-JPSkU,157668 +pygments/lexers/macaulay2.py,sha256=zkV-vxjQYa0Jj9TGfFP1iMgpTZ4ApQuAAIdJVGWb2is,33366 +pygments/lexers/make.py,sha256=YMI5DBCrxWca-pz9cVXcyfuHLcikPx9R_3pW_98Myqo,7831 +pygments/lexers/maple.py,sha256=Rs0dEmOMD3C1YQPd0mntN-vzReq4XfHegH6xV4lvJWo,7960 +pygments/lexers/markup.py,sha256=zWtxsyIx_1OxQzS6wLe8bEqglePv4RqvJjbia8AvV5c,65088 +pygments/lexers/math.py,sha256=P3ZK1ePd8ZnLdlmHezo2irCA8T2-nlHBoSaBoT5mEVI,695 +pygments/lexers/matlab.py,sha256=F9KO4qowIhfP8oVhCRRzE_1sqg4zmQbsB2NZH193PiM,133027 +pygments/lexers/maxima.py,sha256=a0h9Ggs9JEovTrzbJT-BLVbOqI29yPnaMZlkU5f_FeY,2715 +pygments/lexers/meson.py,sha256=BMrsDo6BH2lzTFw7JDwQ9SDNMTrRkXCNRDVf4aFHdsI,4336 +pygments/lexers/mime.py,sha256=yGrf3h37LK4b6ERBpFiL_qzn3JgOfGR5KLagnbWFl6c,7582 +pygments/lexers/minecraft.py,sha256=Nu88snDDPzM0D-742fFdUriczL-EE911pAd4_I4-pAw,13696 +pygments/lexers/mips.py,sha256=STKiZT67b3QERXXn7XKVxlPBu7vwbPC5EyCpuf3Jfbw,4656 +pygments/lexers/ml.py,sha256=t8sCv4BjvuBq6AihKKUwStEONIgdXCC2RMtO0RopNbM,35390 +pygments/lexers/modeling.py,sha256=M7B58bGB-Zwd1EmPxKqtRvg7TgNCyem3MVUHv0_H2SQ,13683 +pygments/lexers/modula2.py,sha256=NtpXBRoUCeHfflgB39LknSkCwhBHBKv2Er_pinjVsNE,53072 +pygments/lexers/mojo.py,sha256=8JRVoftN1E-W2woG0K-4n8PQXTUM9iY6Sl5sWb2uGNg,24233 +pygments/lexers/monte.py,sha256=baWU6zlXloenw9MO1MtEVGE9i3CfiXAYhqU621MIjRk,6289 +pygments/lexers/mosel.py,sha256=gjRdedhA1jTjoYoM1Gpaoog_I9o7TRbYMHk97N1TXwg,9297 +pygments/lexers/ncl.py,sha256=zJ6ahlitit4S0pBXc7Wu96PB7xOn59MwfR2HdY5_C60,63999 +pygments/lexers/nimrod.py,sha256=Q1NSqEkLC5wWt7xJyKC-vzWw_Iw2SfDNP_pyMFBuIfA,6413 +pygments/lexers/nit.py,sha256=p_hVD8GzMRl3CABVKHtYgnXFUQk0i5F2FbWFA6WXm6s,2725 +pygments/lexers/nix.py,sha256=NOrv20gdq-2A7eZ6c2gElPHv1Xx2pvv20-qOymL9GMg,4421 +pygments/lexers/numbair.py,sha256=fxkp2CXeXWKBMewfi1H4JSYkmm4kU58wZ2Sh9BDYAWQ,1758 +pygments/lexers/oberon.py,sha256=jw403qUUs7zpTHAs5CbLjb8qiuwtxLk0spDIYqGZwAw,4210 +pygments/lexers/objective.py,sha256=Fo1WB3JMj8sNeYnvB84H4_qwhOt4WNJtJWjVEOwrJGk,23297 +pygments/lexers/ooc.py,sha256=kD1XaJZaihDF_s-Vyu1Bx68S_9zFt2rhox7NF8LpOZM,3002 +pygments/lexers/openscad.py,sha256=h9I1k8kiuQmhX5vZm6VDSr2fa5Finy0sN8ZDIE-jx1c,3700 +pygments/lexers/other.py,sha256=WLVyqPsvm9oSXIbZwbfyJloS6HGgoFW5nVTaU1uQpTw,1763 +pygments/lexers/parasail.py,sha256=DWMGhtyQgGTXbIgQl_mID6CKqi-Dhbvs_dTkmvrZXfE,2719 +pygments/lexers/parsers.py,sha256=feNgxroPoWRf0NEsON2mtmKDUfslIQppukw6ndEsQ3M,26596 +pygments/lexers/pascal.py,sha256=N2tRAjlXnTxggAzzk2tOOAVzeC2MBzrXy97_HQl5n44,30989 +pygments/lexers/pawn.py,sha256=LWUYQYsebMMt2d5oxX1HYWvBqbakR1h7Av_z8Vw94Wg,8253 +pygments/lexers/pddl.py,sha256=Mk4_BzlROJCd0xR4KKRRSrbj0F7LLQcBRjmsmtWmrCg,2989 +pygments/lexers/perl.py,sha256=9BXn3tyHMA49NvzbM9E2czSCHjeU7bvaPLUcoZrhz-4,39192 +pygments/lexers/phix.py,sha256=hZqychqo5sFMBDESzDPXg1DYHQe_9sn294UfbjihaFk,23249 +pygments/lexers/php.py,sha256=l4hzQrlm0525i5dSw9Vmjcai3TzbPT6DkjzxPg9l6Zc,13061 +pygments/lexers/pointless.py,sha256=WSDjqQyGrNIGmTCdaMxl4zk7OZTlJAMzeUZ02kfgcTI,1974 +pygments/lexers/pony.py,sha256=EXrMkacqMZblI7v4AvBRQe-3Py8__bx5FOgjCLdfXxQ,3279 +pygments/lexers/praat.py,sha256=4UFK-nbC6WkZBhJgcQqEGqq9CocJkW7AmT_OJQbjWzk,12676 +pygments/lexers/procfile.py,sha256=05W2fyofLTP-FbEdSXD1eles-PPqVNfF6RWXjQdW2us,1155 +pygments/lexers/prolog.py,sha256=9Kc5YNUFqkfWu2sYoyzC3RX65abf1bm7oHr86z1s4kQ,12866 +pygments/lexers/promql.py,sha256=n-0vo-o8-ZasqP3Va4ujs562UfZSLfZF-RzT71yL0Tk,4738 +pygments/lexers/prql.py,sha256=PFReuvhbv4K5aeu6lvDfw4m-3hULkB3r43bKAy948os,8747 +pygments/lexers/ptx.py,sha256=KSHAvbiNVUntKilQ6EPYoLFocmJpRsBy_7fW6_Nrs1Y,4501 +pygments/lexers/python.py,sha256=WZe7fBAHKZ_BxPg8qIU26UGhk8qwUYyENJ3IyPW64mc,53805 +pygments/lexers/q.py,sha256=WQFUh3JrpK2j-VGW_Ytn3uJ5frUNmQIFnLtMVGRA9DI,6936 +pygments/lexers/qlik.py,sha256=2wqwdfIjrAz6RNBsP4MyeLX8Z7QpIGzxtf1CvaOlr_g,3693 +pygments/lexers/qvt.py,sha256=XMBnsWRrvCDf989OuDeb-KpszAkeETiACyaghZeL1ns,6103 +pygments/lexers/r.py,sha256=B6WgrD9SY1UTCV1fQBSlZbezPfpYsARn3FQIHcFYOiM,6474 +pygments/lexers/rdf.py,sha256=qUzxLna9v071bHhZAjdsBi8dKaJNk_h9g1ZRUAYCfoo,16056 +pygments/lexers/rebol.py,sha256=4u3N4kzui55HapopXDu3Kt0jczxDZ4buzwR7Mt4tQiM,18259 +pygments/lexers/rego.py,sha256=Rx5Gphbktr9ojg5DbqlyxHeQqqtF7g8W-oF0rmloDNY,1748 +pygments/lexers/resource.py,sha256=ioEzgWksB5HCjoz85XNkQPSd7n5kL0SZiuPkJP1hunQ,2927 +pygments/lexers/ride.py,sha256=kCWdxuR3PclVi4wiA0uUx4CYEFwuTqoMsKjhSW4X3yg,5035 +pygments/lexers/rita.py,sha256=Mj1QNxx1sWAZYC02kw8piVckaiw9B0MqQtiIiDFH0pA,1127 +pygments/lexers/rnc.py,sha256=g7ZD334PMGUqy_Ij64laSN1vJerwHqVkegfMCa3E-y8,1972 +pygments/lexers/roboconf.py,sha256=HbYuK5CqmQdd63SRY2nle01r7-p7mil0SnoauYDmEOY,2074 +pygments/lexers/robotframework.py,sha256=c4U1B9Q9ITBCTohqJTZOvkfyeVbenN4xhzSWIoZh5eU,18448 +pygments/lexers/ruby.py,sha256=uG617E5abBZcECRCqkhIfc-IbZcRb5cGuUZq_xpax90,22753 +pygments/lexers/rust.py,sha256=ZY-9vtsreBP0NfDd0WCouLSp_9MChAL8U8Abe-m9PB8,8260 +pygments/lexers/sas.py,sha256=C1Uz2s9DU6_s2kL-cB_PAGPtpyK5THlmhNmCumC1l48,9456 +pygments/lexers/savi.py,sha256=jrmruK0GnXktgBTWXW3oN3TXtofn3HBbkMlHnR84cko,4878 +pygments/lexers/scdoc.py,sha256=DXRmFDmYuc7h3gPAAVhfcL1OEbNBK5RdPpJqQzF3ZTk,2524 +pygments/lexers/scripting.py,sha256=eaYlkDK-_cAwTcCBHP6QXBCz8n6OzbhzdkRe0uV0xWY,81814 +pygments/lexers/sgf.py,sha256=w6C513ENaO2YCnqrduK7k03NaMDf-pgygvfzq2NaSRk,1985 +pygments/lexers/shell.py,sha256=dCS1zwkf5KwTog4__MnMC7h3Xmwv4_d3fnEV29tSwXI,36381 +pygments/lexers/sieve.py,sha256=eob-L84yf2jmhdNyYZUlbUJozdcd6GXcHW68lmAe8WE,2514 +pygments/lexers/slash.py,sha256=I-cRepmaxhL1SgYvD1hHX3gNBFI8NPszdU7hn1o5JlA,8484 +pygments/lexers/smalltalk.py,sha256=ue2PmqDK2sw0j75WdseiiENJBdZ1OwysH2Op1QN1r24,7204 +pygments/lexers/smithy.py,sha256=VREWoeuz7ANap_Uiopn7rs0Tnsfc-xBisDJKRGQY_y8,2659 +pygments/lexers/smv.py,sha256=He_VBSMbWONMWZmkrB5RYR0cfHVnMyKIXz68IFYl-a8,2805 +pygments/lexers/snobol.py,sha256=qDzb41xQQWMNmjB2MtZs23pFoFgZ2gbRZhK_Ir03r7I,2778 +pygments/lexers/solidity.py,sha256=Tixfnwku4Yezj6nNm8xVaw7EdV1qgAgdwahdTFP0St8,3163 +pygments/lexers/soong.py,sha256=Vm18vV4g6T8UPgjjY2yTRlSXGDpZowmuqQUBFfm4A9A,2339 +pygments/lexers/sophia.py,sha256=2YtYIT8iwAoW0B7TZuuoG_ZILhJV-2A7oBGat-98naE,3376 +pygments/lexers/special.py,sha256=8JuR2Vex8X-RWnC36S0HXTHWp2qmZclc90-TrLUWyaY,3585 +pygments/lexers/spice.py,sha256=m4nK0q4Sq_OFQez7kGWfki0No4ZV24YrONfHVj1Piqs,2790 +pygments/lexers/sql.py,sha256=WSG6vOsR87EEEwSQefP_Z7TauUG_BjqMHUFmPaSOVj4,41476 +pygments/lexers/srcinfo.py,sha256=B8vDs-sJogG3mWa5Hp_7JfHHUMyYRwGvKv6cKbFQXLM,1746 +pygments/lexers/stata.py,sha256=Zr9BC52D5O_3BbdW0N-tzoUmy0NTguL2sC-saXRVM-c,6415 +pygments/lexers/supercollider.py,sha256=_H5wDrn0DiGnlhB_cz6Rt_lo2TvqjSm0o6NPTd9R4Ko,3697 +pygments/lexers/tablegen.py,sha256=1JjedXYY18BNiY9JtNGLOtGfiwduNDZpQLBGTeQ6jAw,3987 +pygments/lexers/tact.py,sha256=X_lsxjFUMaC1TmYysXJq9tmAGifRnil83Bt1zA86Xdo,10809 +pygments/lexers/tal.py,sha256=xS9PlaWQOPj8MVr56fUNq31vUQKRWoLTlyWj9ZHm8AM,2904 +pygments/lexers/tcl.py,sha256=lK97ju4nikkt-oGOzIeyFEM98yq4dZSI8uEmYsq0R6c,5512 +pygments/lexers/teal.py,sha256=t3dqy_Arwv8_yExbX_xiFxv1TqJLPv4vh1MVKjKwS4Y,3522 +pygments/lexers/templates.py,sha256=BVdjYeoacIUuFyHTG39j4PxeNCe5E1oUURjH1rITrI4,75731 +pygments/lexers/teraterm.py,sha256=ciwztagW5Drg2gr17Qykrh6GwMsKy7e4xdQshX95GyQ,9718 +pygments/lexers/testing.py,sha256=YZgDgUEaLEYKSKEqpDsUi3Bn-Db_D42IlyiSsr1oX8U,10810 +pygments/lexers/text.py,sha256=nOCQPssIlKdVWU3PKxZiBPkf_KFM2V48IOssSyqhFY8,1068 +pygments/lexers/textedit.py,sha256=ttT4Ph-hIdgFLG6maRy_GskkziTFK0Wcg28yU0s6lek,7760 +pygments/lexers/textfmts.py,sha256=mi9KLEq4mrzDJbEc8G3VM-mSki_Tylkzodu47yH6z84,15524 +pygments/lexers/theorem.py,sha256=51ppBAEdhJmwU_lC916zMyjEoKLXqf89VAE_Lr0PNCc,17855 +pygments/lexers/thingsdb.py,sha256=x_fHNkLA-hIJyeIs6rg_X8n5OLYvFqaSu1FhI3apI5Y,6017 +pygments/lexers/tlb.py,sha256=ue2gqm45BI512lM13O8skAky9zAb7pLMrxZ8pbt5zRU,1450 +pygments/lexers/tls.py,sha256=_uQUVuMRDOhN-XUyGR5DIlVCk1CUZ1fIOSN4_WQYPKk,1540 +pygments/lexers/tnt.py,sha256=pK4LgoKON7u1xF66JYFncAPSbD8DZaeI_WTZ9HqEFlY,10456 +pygments/lexers/trafficscript.py,sha256=X3B8kgxS54ecuok9ic6Hkp-UMn5DvOmCK0p70Tz27Cw,1506 +pygments/lexers/typoscript.py,sha256=mBuePiVZUoAORPKsHwrx6fBWiy3fAIqG-2O67QmMiFI,8332 +pygments/lexers/typst.py,sha256=zIJBEhUXtWp5OiyAmvFA5m8d1EQG-ocwrJ677dvTUAk,7167 +pygments/lexers/ul4.py,sha256=rCaw0J9j3cdql9lX_HTilg65k9-9S118zOA6TAYfxaM,10499 +pygments/lexers/unicon.py,sha256=RAqoCnAAJBYOAGdR8ng0g6FtB39bGemLRlIqv5mcg9E,18625 +pygments/lexers/urbi.py,sha256=ajNP70NJg32jNnFDZsLvr_-4TToSGqRGkFyAPIJLfCU,6082 +pygments/lexers/usd.py,sha256=2eEGouolodYS402P_gtBrn4lLzpg1z8uHwPCKqjUb_k,3304 +pygments/lexers/varnish.py,sha256=dSh0Ku9SrjmlB29Fi_mWdWavN7M0cMKeepR4a34sOyI,7473 +pygments/lexers/verification.py,sha256=Qu433Q_h3EK3uS4bJoLRFZK0kIVwzX5AFKsa4Z-qnxA,3934 +pygments/lexers/verifpal.py,sha256=buyOOzCo_dGnoC40h0tthylHVVpgDt8qXu4olLvYy_4,2661 +pygments/lexers/vip.py,sha256=2lEV4cLV9p4E37wctBL7zkZ4ZU4p3HVsiLJFzB1bie0,5711 +pygments/lexers/vyper.py,sha256=Zq6sQIUBk6mBdpgOVgu3A6swGoBne0kDlRyjZznm2BY,5615 +pygments/lexers/web.py,sha256=4W9a7vcskrGJnxt4KmoE3SZydWB1qLq7lP2XS85J_m8,913 +pygments/lexers/webassembly.py,sha256=zgcMouzLawcbeFr6w_SOvGoUR68ZtqnnsbOcWEVleLk,5698 +pygments/lexers/webidl.py,sha256=ODtVmw4gVzI8HQWxuEckP6KMwm8WP2G2lSZEjagDXts,10516 +pygments/lexers/webmisc.py,sha256=-_-INDVdk47e2jlj-9bFcuLtntqVorBqIjlnwPfZFdI,40564 +pygments/lexers/wgsl.py,sha256=9igd9dzixGIgNewruv9mPnFms-c9BahkZcCCrZygv84,11880 +pygments/lexers/whiley.py,sha256=lMr750lA4MZsB4xqzVsIRtVMJIC3_dArhFYTHvOPwvA,4017 +pygments/lexers/wowtoc.py,sha256=8xxvf0xGeYtf4PE7KtkHZ_ly9xY_XXHrpCitdKE42Ro,4076 +pygments/lexers/wren.py,sha256=goGXnAMKKa13LLL40ybT3aMGPrk3gCRwZQFYAkKB_w0,3229 +pygments/lexers/x10.py,sha256=Q-AmgdF2E-N7mtOPpZ07CsxrTVnikyqC4uRRv6H75sk,1943 +pygments/lexers/xorg.py,sha256=9ttrBd3_Y2nXANsqtMposSgblYmMYqWXQ-Iz5RH9RsU,925 +pygments/lexers/yang.py,sha256=13CWbSaNr9giOHz4o0SXSklh0bfWt0ah14jJGpTvcn0,4499 +pygments/lexers/yara.py,sha256=jUSv78KTDfguCoAoAZKbYzQERkkyxBBWv5dInVrkDxo,2427 +pygments/lexers/zig.py,sha256=f-80MVOSp1KnczAMokQLVM-_wAEOD16EcGFnaCNlsN0,3976 +pygments/modeline.py,sha256=K5eSkR8GS1r5OkXXTHOcV0aM_6xpk9eWNEIAW-OOJ2g,1005 +pygments/plugin.py,sha256=tPx0rJCTIZ9ioRgLNYG4pifCbAwTRUZddvLw-NfAk2w,1891 +pygments/regexopt.py,sha256=wXaP9Gjp_hKAdnICqoDkRxAOQJSc4v3X6mcxx3z-TNs,3072 +pygments/scanner.py,sha256=nNcETRR1tRuiTaHmHSTTECVYFPcLf6mDZu1e4u91A9E,3092 +pygments/sphinxext.py,sha256=VEe_oHNgLoEGMHc2ROfbee2mF2PPREFyE6_m_JN5FvQ,7898 +pygments/style.py,sha256=Cpw9dCAyW3_JAwFRXOJXmtKb5ZwO2_5KSmlq6q4fZw4,6408 +pygments/styles/__init__.py,sha256=f9KCQXN4uKbe8aI8-L3qTC-_XPfT563FwTg6VTGVfwI,2006 +pygments/styles/__pycache__/__init__.cpython-312.pyc,, +pygments/styles/__pycache__/_mapping.cpython-312.pyc,, +pygments/styles/__pycache__/abap.cpython-312.pyc,, +pygments/styles/__pycache__/algol.cpython-312.pyc,, +pygments/styles/__pycache__/algol_nu.cpython-312.pyc,, +pygments/styles/__pycache__/arduino.cpython-312.pyc,, +pygments/styles/__pycache__/autumn.cpython-312.pyc,, +pygments/styles/__pycache__/borland.cpython-312.pyc,, +pygments/styles/__pycache__/bw.cpython-312.pyc,, +pygments/styles/__pycache__/coffee.cpython-312.pyc,, +pygments/styles/__pycache__/colorful.cpython-312.pyc,, +pygments/styles/__pycache__/default.cpython-312.pyc,, +pygments/styles/__pycache__/dracula.cpython-312.pyc,, +pygments/styles/__pycache__/emacs.cpython-312.pyc,, +pygments/styles/__pycache__/friendly.cpython-312.pyc,, +pygments/styles/__pycache__/friendly_grayscale.cpython-312.pyc,, +pygments/styles/__pycache__/fruity.cpython-312.pyc,, +pygments/styles/__pycache__/gh_dark.cpython-312.pyc,, +pygments/styles/__pycache__/gruvbox.cpython-312.pyc,, +pygments/styles/__pycache__/igor.cpython-312.pyc,, +pygments/styles/__pycache__/inkpot.cpython-312.pyc,, +pygments/styles/__pycache__/lightbulb.cpython-312.pyc,, +pygments/styles/__pycache__/lilypond.cpython-312.pyc,, +pygments/styles/__pycache__/lovelace.cpython-312.pyc,, +pygments/styles/__pycache__/manni.cpython-312.pyc,, +pygments/styles/__pycache__/material.cpython-312.pyc,, +pygments/styles/__pycache__/monokai.cpython-312.pyc,, +pygments/styles/__pycache__/murphy.cpython-312.pyc,, +pygments/styles/__pycache__/native.cpython-312.pyc,, +pygments/styles/__pycache__/nord.cpython-312.pyc,, +pygments/styles/__pycache__/onedark.cpython-312.pyc,, +pygments/styles/__pycache__/paraiso_dark.cpython-312.pyc,, +pygments/styles/__pycache__/paraiso_light.cpython-312.pyc,, +pygments/styles/__pycache__/pastie.cpython-312.pyc,, +pygments/styles/__pycache__/perldoc.cpython-312.pyc,, +pygments/styles/__pycache__/rainbow_dash.cpython-312.pyc,, +pygments/styles/__pycache__/rrt.cpython-312.pyc,, +pygments/styles/__pycache__/sas.cpython-312.pyc,, +pygments/styles/__pycache__/solarized.cpython-312.pyc,, +pygments/styles/__pycache__/staroffice.cpython-312.pyc,, +pygments/styles/__pycache__/stata_dark.cpython-312.pyc,, +pygments/styles/__pycache__/stata_light.cpython-312.pyc,, +pygments/styles/__pycache__/tango.cpython-312.pyc,, +pygments/styles/__pycache__/trac.cpython-312.pyc,, +pygments/styles/__pycache__/vim.cpython-312.pyc,, +pygments/styles/__pycache__/vs.cpython-312.pyc,, +pygments/styles/__pycache__/xcode.cpython-312.pyc,, +pygments/styles/__pycache__/zenburn.cpython-312.pyc,, +pygments/styles/_mapping.py,sha256=6lovFUE29tz6EsV3XYY4hgozJ7q1JL7cfO3UOlgnS8w,3312 +pygments/styles/abap.py,sha256=64Uwr8uPdEdcT-tE-Y2VveTXfH3SkqH9qdMgY49YHQI,749 +pygments/styles/algol.py,sha256=fCuk8ITTehvbJSufiaKlgnFsKbl-xFxxR82xhltc-cQ,2262 +pygments/styles/algol_nu.py,sha256=Gv9WfHJvYegGcUk1zcufQgsdXPNjCUNk8sAHyrSGGh4,2283 +pygments/styles/arduino.py,sha256=NoUB8xk7M1HGPoLfuySOLU0sVwoTuLcZqllXl2EO_iE,4557 +pygments/styles/autumn.py,sha256=fLLfjHXjxCl6crBAxEsBLH372ALMkFacA2bG6KFbJi4,2195 +pygments/styles/borland.py,sha256=_0ySKp4KGCSgtYjPe8uzD6gQhlmAIR4T43i-FoRYNOM,1611 +pygments/styles/bw.py,sha256=vhk8Xoj64fLPdA9IQU6mUVsYMel255jR-FDU7BjIHtI,1406 +pygments/styles/coffee.py,sha256=NqLt-fc7LONma1BGggbceVRY9uDE70WBuZXqK4zwaco,2308 +pygments/styles/colorful.py,sha256=mYcSbehtH7itH_QV9NqJp4Wna1X4lrwl2wkVXS2u-5A,2832 +pygments/styles/default.py,sha256=RTgG2zKWWUxPTDCFxhTnyZI_WZBIVgu5XsUpNvFisCA,2588 +pygments/styles/dracula.py,sha256=vRJmixBoSKV9o8NVQhXGViQqchhIYugfikLmvX0DoBw,2182 +pygments/styles/emacs.py,sha256=TiOG9oc83qToMCRMnJrXtWYqnzAqYycRz_50OoCKtxc,2535 +pygments/styles/friendly.py,sha256=oAi-l9anQTs9STDmUzXGDlOegatEOH4hpD0j6o6dZGM,2604 +pygments/styles/friendly_grayscale.py,sha256=a7Cqkzt6-uTiXvj6GoYBXzRvX5_zviCjjRB04Kf_-Q0,2828 +pygments/styles/fruity.py,sha256=GfSUTG0stlJr5Ow_saCaxbI2IB4-34Dp2TuRTpfUJBs,1324 +pygments/styles/gh_dark.py,sha256=ruNX3d4rf22rx-8HnwvGbNbXRQpXCNcHU1HNq6N4uNg,3590 +pygments/styles/gruvbox.py,sha256=KrFoHEoVnZW6XM9udyXncPomeGyZgIDsNWOH3kCrxFQ,3387 +pygments/styles/igor.py,sha256=fYYPhM0dRCvcDTMVrMVO5oFKnYm-8YVlsuVBoczFLtY,737 +pygments/styles/inkpot.py,sha256=jggSeX9NV15eOL2oJaVmZ6vmV7LWRzXJQRUqcWEqGRs,2404 +pygments/styles/lightbulb.py,sha256=Y8u1qdvlHfBqI2jJex55SkvVatVo_FjEUzE6h-X7m-0,3172 +pygments/styles/lilypond.py,sha256=Y6fp_sEL-zESmxAaMxzjtrKk90cuDC_DalNdC8wj0nw,2066 +pygments/styles/lovelace.py,sha256=cA9uhmbnzY04MccsiYSgMY7fvb4WMRbegWBUrGvXh1M,3178 +pygments/styles/manni.py,sha256=g9FyO7plTwfMm2cU4iiKgdlkMlvQLG6l2Lwkgz5ITS4,2443 +pygments/styles/material.py,sha256=LDmgomAbgtJDZhbv446_zIwgYh50UAqEEtgYNUns1rQ,4201 +pygments/styles/monokai.py,sha256=lrxTJpkBarV9gTLkBQryZ6oNSjekAVheJueKJP5iEYA,5184 +pygments/styles/murphy.py,sha256=-AKZiLkpiWej-otjHMsYCE-I-_IzCOLJY-_GBdKRZRw,2805 +pygments/styles/native.py,sha256=l6tezGSQTB8p_SyOXJ0PWI7KzCeEdtsPmVc4Yn4_CwU,2043 +pygments/styles/nord.py,sha256=GDt3WAaqaWsiCeqpIBPxd8TEUX708fGfwaA7S0w0oy0,5391 +pygments/styles/onedark.py,sha256=k80cZEppCEF-HLoxy_FEA0QmQDZze68nHVMNGyUVa28,1719 +pygments/styles/paraiso_dark.py,sha256=Jkrg4nUKIVNF8U4fPNV_Smq_g9NFbb9eiUrjYpVgQZg,5662 +pygments/styles/paraiso_light.py,sha256=MxN964ZEpze3wF0ss-igaa2I7E684MHe-Zq0rWPH3wo,5668 +pygments/styles/pastie.py,sha256=ZvAs9UpBNYFC-5PFrCRGYnm3FoPKb-eKR-ozbWZP-4g,2525 +pygments/styles/perldoc.py,sha256=HSxB93e4UpQkZspReQ34FeJbZ-59ksGvdaH-hToehi8,2230 +pygments/styles/rainbow_dash.py,sha256=4ugL18Or7aNtaLfPfCLFRiFy0Gu2RA4a9G2LQUE9SrM,2390 +pygments/styles/rrt.py,sha256=fgzfpC0PC_SCcLOMCNEIQTjPUMOncRe7SR10GfSRbXY,1006 +pygments/styles/sas.py,sha256=yzoXmbfQ2ND1WWq93b4vVGYkQSZHPqb4ymes9YYRT3w,1440 +pygments/styles/solarized.py,sha256=qupILFZn02WspnAF5SPYb-W8guo9xnUtjb1HeLw3XgE,4247 +pygments/styles/staroffice.py,sha256=CLbBeMoxay21Xyu3Af2p4xUXyG1_6ydCbvs5RJKYe5w,831 +pygments/styles/stata_dark.py,sha256=vX8SwHV__sG92F4CKribG08MJfSVq98dgs7gEA_n9yc,1257 +pygments/styles/stata_light.py,sha256=uV3GE-ylvffQ0yN3py1YAVqBB5wflIKZbceyK1Lqvrc,1289 +pygments/styles/tango.py,sha256=O2wcM4hHuU1Yt071M9CK7JPtiiSCqyxtT9tbiQICV28,7137 +pygments/styles/trac.py,sha256=9kMv1ZZyMKACWlx2fQVjRP0I2pgcRYCNrd7iGGZg9qk,1981 +pygments/styles/vim.py,sha256=J7_TqvrGkTX_XuTHW0In5wqPLAUPRWyr1122XueZWmM,2019 +pygments/styles/vs.py,sha256=s7YnzbIPuFU3LIke27mc4lAQSn2R3vbbHc1baMGSU_U,1130 +pygments/styles/xcode.py,sha256=PbQdzgGaA4a9LAU1i58alY9kM4IFlQX5jHQwOYmf_Rk,1504 +pygments/styles/zenburn.py,sha256=suZEKzBTCYdhf2cxNwcY7UATJK1tq5eYhGdBcXdf6MU,2203 +pygments/token.py,sha256=WbdWGhYm_Vosb0DDxW9lHNPgITXfWTsQmHt6cy9RbcM,6226 +pygments/unistring.py,sha256=al-_rBemRuGvinsrM6atNsHTmJ6DUbw24q2O2Ru1cBc,63208 +pygments/util.py,sha256=oRtSpiAo5jM9ulntkvVbgXUdiAW57jnuYGB7t9fYuhc,10031 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/WHEEL b/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/WHEEL new file mode 100644 index 00000000..12228d41 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.27.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/entry_points.txt b/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/entry_points.txt new file mode 100644 index 00000000..15498e35 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +pygmentize = pygments.cmdline:main diff --git a/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/licenses/AUTHORS b/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/licenses/AUTHORS new file mode 100644 index 00000000..811c66ae --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/licenses/AUTHORS @@ -0,0 +1,291 @@ +Pygments is written and maintained by Georg Brandl . + +Major developers are Tim Hatch and Armin Ronacher +. + +Other contributors, listed alphabetically, are: + +* Sam Aaron -- Ioke lexer +* Jean Abou Samra -- LilyPond lexer +* João Abecasis -- JSLT lexer +* Ali Afshar -- image formatter +* Thomas Aglassinger -- Easytrieve, JCL, Rexx, Transact-SQL and VBScript + lexers +* Maxence Ahlouche -- PostgreSQL Explain lexer +* Muthiah Annamalai -- Ezhil lexer +* Nikolay Antipov -- OpenSCAD lexer +* Kumar Appaiah -- Debian control lexer +* Andreas Amann -- AppleScript lexer +* Timothy Armstrong -- Dart lexer fixes +* Jeffrey Arnold -- R/S, Rd, BUGS, Jags, and Stan lexers +* Eiríkr Åsheim -- Uxntal lexer +* Jeremy Ashkenas -- CoffeeScript lexer +* José Joaquín Atria -- Praat lexer +* Stefan Matthias Aust -- Smalltalk lexer +* Lucas Bajolet -- Nit lexer +* Ben Bangert -- Mako lexers +* Max Battcher -- Darcs patch lexer +* Thomas Baruchel -- APL lexer +* Tim Baumann -- (Literate) Agda lexer +* Paul Baumgart, 280 North, Inc. -- Objective-J lexer +* Michael Bayer -- Myghty lexers +* Thomas Beale -- Archetype lexers +* John Benediktsson -- Factor lexer +* David Benjamin, Google LLC -- TLS lexer +* Trevor Bergeron -- mIRC formatter +* Vincent Bernat -- LessCSS lexer +* Christopher Bertels -- Fancy lexer +* Sébastien Bigaret -- QVT Operational lexer +* Jarrett Billingsley -- MiniD lexer +* Adam Blinkinsop -- Haskell, Redcode lexers +* Stéphane Blondon -- Procfile, SGF and Sieve lexers +* Frits van Bommel -- assembler lexers +* Pierre Bourdon -- bugfixes +* Martijn Braam -- Kernel log lexer, BARE lexer +* JD Browne, Google LLC -- GoogleSQL lexer +* Matthias Bussonnier -- ANSI style handling for terminal-256 formatter +* chebee7i -- Python traceback lexer improvements +* Hiram Chirino -- Scaml and Jade lexers +* Mauricio Caceres -- SAS and Stata lexers. +* Michael Camilleri, John Gabriele, sogaiu -- Janet lexer +* Daren Chandisingh -- Gleam lexer +* Ian Cooper -- VGL lexer +* David Corbett -- Inform, Jasmin, JSGF, Snowball, and TADS 3 lexers +* Leaf Corcoran -- MoonScript lexer +* Fraser Cormack -- TableGen lexer +* Gabriel Corona -- ASN.1 lexer +* Christopher Creutzig -- MuPAD lexer +* Daniël W. Crompton -- Pike lexer +* Pete Curry -- bugfixes +* Bryan Davis -- EBNF lexer +* Bruno Deferrari -- Shen lexer +* Walter Dörwald -- UL4 lexer +* Luke Drummond -- Meson lexer +* Giedrius Dubinskas -- HTML formatter improvements +* Owen Durni -- Haxe lexer +* Alexander Dutton, Oxford University Computing Services -- SPARQL lexer +* James Edwards -- Terraform lexer +* Nick Efford -- Python 3 lexer +* Sven Efftinge -- Xtend lexer +* Artem Egorkine -- terminal256 formatter +* Matthew Fernandez -- CAmkES lexer +* Paweł Fertyk -- GDScript lexer, HTML formatter improvements +* Michael Ficarra -- CPSA lexer +* James H. Fisher -- PostScript lexer +* Amanda Fitch, Google LLC -- GoogleSQL lexer +* William S. Fulton -- SWIG lexer +* Carlos Galdino -- Elixir and Elixir Console lexers +* Michael Galloy -- IDL lexer +* Naveen Garg -- Autohotkey lexer +* Simon Garnotel -- FreeFem++ lexer +* Laurent Gautier -- R/S lexer +* Alex Gaynor -- PyPy log lexer +* Richard Gerkin -- Igor Pro lexer +* Alain Gilbert -- TypeScript lexer +* Alex Gilding -- BlitzBasic lexer +* GitHub, Inc -- DASM16, Augeas, TOML, and Slash lexers +* Bertrand Goetzmann -- Groovy lexer +* Krzysiek Goj -- Scala lexer +* Rostyslav Golda -- FloScript lexer +* Andrey Golovizin -- BibTeX lexers +* Matt Good -- Genshi, Cheetah lexers +* Michał Górny -- vim modeline support +* Alex Gosse -- TrafficScript lexer +* Patrick Gotthardt -- PHP namespaces support +* Hubert Gruniaux -- C and C++ lexer improvements +* Olivier Guibe -- Asymptote lexer +* Phil Hagelberg -- Fennel lexer +* Florian Hahn -- Boogie lexer +* Martin Harriman -- SNOBOL lexer +* Matthew Harrison -- SVG formatter +* Steven Hazel -- Tcl lexer +* Dan Michael Heggø -- Turtle lexer +* Aslak Hellesøy -- Gherkin lexer +* Greg Hendershott -- Racket lexer +* Justin Hendrick -- ParaSail lexer +* Jordi Gutiérrez Hermoso -- Octave lexer +* David Hess, Fish Software, Inc. -- Objective-J lexer +* Ken Hilton -- Typographic Number Theory and Arrow lexers +* Varun Hiremath -- Debian control lexer +* Rob Hoelz -- Perl 6 lexer +* Doug Hogan -- Mscgen lexer +* Ben Hollis -- Mason lexer +* Max Horn -- GAP lexer +* Fred Hornsey -- OMG IDL Lexer +* Alastair Houghton -- Lexer inheritance facility +* Tim Howard -- BlitzMax lexer +* Dustin Howett -- Logos lexer +* Ivan Inozemtsev -- Fantom lexer +* Hiroaki Itoh -- Shell console rewrite, Lexers for PowerShell session, + MSDOS session, BC, WDiff +* Brian R. Jackson -- Tea lexer +* Christian Jann -- ShellSession lexer +* Jonas Camillus Jeppesen -- Line numbers and line highlighting for + RTF-formatter +* Dennis Kaarsemaker -- sources.list lexer +* Dmitri Kabak -- Inferno Limbo lexer +* Igor Kalnitsky -- vhdl lexer +* Colin Kennedy - USD lexer +* Alexander Kit -- MaskJS lexer +* Pekka Klärck -- Robot Framework lexer +* Gerwin Klein -- Isabelle lexer +* Eric Knibbe -- Lasso lexer +* Stepan Koltsov -- Clay lexer +* Oliver Kopp - Friendly grayscale style +* Adam Koprowski -- Opa lexer +* Benjamin Kowarsch -- Modula-2 lexer +* Domen Kožar -- Nix lexer +* Oleh Krekel -- Emacs Lisp lexer +* Alexander Kriegisch -- Kconfig and AspectJ lexers +* Marek Kubica -- Scheme lexer +* Jochen Kupperschmidt -- Markdown processor +* Gerd Kurzbach -- Modelica lexer +* Jon Larimer, Google Inc. -- Smali lexer +* Olov Lassus -- Dart lexer +* Matt Layman -- TAP lexer +* Dan Lazin, Google LLC -- GoogleSQL lexer +* Kristian Lyngstøl -- Varnish lexers +* Sylvestre Ledru -- Scilab lexer +* Chee Sing Lee -- Flatline lexer +* Mark Lee -- Vala lexer +* Thomas Linder Puls -- Visual Prolog lexer +* Pete Lomax -- Phix lexer +* Valentin Lorentz -- C++ lexer improvements +* Ben Mabey -- Gherkin lexer +* Angus MacArthur -- QML lexer +* Louis Mandel -- X10 lexer +* Louis Marchand -- Eiffel lexer +* Simone Margaritelli -- Hybris lexer +* Tim Martin - World of Warcraft TOC lexer +* Kirk McDonald -- D lexer +* Gordon McGregor -- SystemVerilog lexer +* Stephen McKamey -- Duel/JBST lexer +* Brian McKenna -- F# lexer +* Charles McLaughlin -- Puppet lexer +* Kurt McKee -- Tera Term macro lexer, PostgreSQL updates, MySQL overhaul, JSON lexer +* Joe Eli McIlvain -- Savi lexer +* Lukas Meuser -- BBCode formatter, Lua lexer +* Cat Miller -- Pig lexer +* Paul Miller -- LiveScript lexer +* Hong Minhee -- HTTP lexer +* Michael Mior -- Awk lexer +* Bruce Mitchener -- Dylan lexer rewrite +* Reuben Morais -- SourcePawn lexer +* Jon Morton -- Rust lexer +* Paulo Moura -- Logtalk lexer +* Mher Movsisyan -- DTD lexer +* Dejan Muhamedagic -- Crmsh lexer +* Adrien Nayrat -- PostgreSQL Explain lexer +* Ana Nelson -- Ragel, ANTLR, R console lexers +* David Neto, Google LLC -- WebGPU Shading Language lexer +* Kurt Neufeld -- Markdown lexer +* Nam T. Nguyen -- Monokai style +* Jesper Noehr -- HTML formatter "anchorlinenos" +* Mike Nolta -- Julia lexer +* Avery Nortonsmith -- Pointless lexer +* Jonas Obrist -- BBCode lexer +* Edward O'Callaghan -- Cryptol lexer +* David Oliva -- Rebol lexer +* Pat Pannuto -- nesC lexer +* Jon Parise -- Protocol buffers and Thrift lexers +* Benjamin Peterson -- Test suite refactoring +* Ronny Pfannschmidt -- BBCode lexer +* Dominik Picheta -- Nimrod lexer +* Andrew Pinkham -- RTF Formatter Refactoring +* Clément Prévost -- UrbiScript lexer +* Tanner Prynn -- cmdline -x option and loading lexers from files +* Oleh Prypin -- Crystal lexer (based on Ruby lexer) +* Nick Psaris -- K and Q lexers +* Xidorn Quan -- Web IDL lexer +* Elias Rabel -- Fortran fixed form lexer +* raichoo -- Idris lexer +* Daniel Ramirez -- GDScript lexer +* Kashif Rasul -- CUDA lexer +* Nathan Reed -- HLSL lexer +* Justin Reidy -- MXML lexer +* Jonathon Reinhart, Google LLC -- Soong lexer +* Norman Richards -- JSON lexer +* Corey Richardson -- Rust lexer updates +* Fabrizio Riguzzi -- cplint leder +* Lubomir Rintel -- GoodData MAQL and CL lexers +* Andre Roberge -- Tango style +* Georg Rollinger -- HSAIL lexer +* Michiel Roos -- TypoScript lexer +* Konrad Rudolph -- LaTeX formatter enhancements +* Mario Ruggier -- Evoque lexers +* Miikka Salminen -- Lovelace style, Hexdump lexer, lexer enhancements +* Stou Sandalski -- NumPy, FORTRAN, tcsh and XSLT lexers +* Matteo Sasso -- Common Lisp lexer +* Joe Schafer -- Ada lexer +* Max Schillinger -- TiddlyWiki5 lexer +* Andrew Schmidt -- X++ lexer +* Ken Schutte -- Matlab lexers +* René Schwaiger -- Rainbow Dash style +* Sebastian Schweizer -- Whiley lexer +* Tassilo Schweyer -- Io, MOOCode lexers +* Pablo Seminario -- PromQL lexer +* Ted Shaw -- AutoIt lexer +* Joerg Sieker -- ABAP lexer +* Robert Simmons -- Standard ML lexer +* Kirill Simonov -- YAML lexer +* Corbin Simpson -- Monte lexer +* Ville Skyttä -- ASCII armored lexer +* Alexander Smishlajev -- Visual FoxPro lexer +* Steve Spigarelli -- XQuery lexer +* Jerome St-Louis -- eC lexer +* Camil Staps -- Clean and NuSMV lexers; Solarized style +* James Strachan -- Kotlin lexer +* Tom Stuart -- Treetop lexer +* Colin Sullivan -- SuperCollider lexer +* Ben Swift -- Extempore lexer +* tatt61880 -- Kuin lexer +* Edoardo Tenani -- Arduino lexer +* Tiberius Teng -- default style overhaul +* Jeremy Thurgood -- Erlang, Squid config lexers +* Brian Tiffin -- OpenCOBOL lexer +* Bob Tolbert -- Hy lexer +* Doug Torrance -- Macaulay2 lexer +* Matthias Trute -- Forth lexer +* Tuoa Spi T4 -- Bdd lexer +* Erick Tryzelaar -- Felix lexer +* Alexander Udalov -- Kotlin lexer improvements +* Thomas Van Doren -- Chapel lexer +* Dave Van Ee -- Uxntal lexer updates +* Daniele Varrazzo -- PostgreSQL lexers +* Abe Voelker -- OpenEdge ABL lexer +* Pepijn de Vos -- HTML formatter CTags support +* Matthias Vallentin -- Bro lexer +* Benoît Vinot -- AMPL lexer +* Linh Vu Hong -- RSL lexer +* Taavi Väänänen -- Debian control lexer +* Immanuel Washington -- Smithy lexer +* Nathan Weizenbaum -- Haml and Sass lexers +* Nathan Whetsell -- Csound lexers +* Dietmar Winkler -- Modelica lexer +* Nils Winter -- Smalltalk lexer +* Davy Wybiral -- Clojure lexer +* Whitney Young -- ObjectiveC lexer +* Diego Zamboni -- CFengine3 lexer +* Enrique Zamudio -- Ceylon lexer +* Alex Zimin -- Nemerle lexer +* Rob Zimmerman -- Kal lexer +* Evgenii Zheltonozhskii -- Maple lexer +* Vincent Zurczak -- Roboconf lexer +* Hubert Gruniaux -- C and C++ lexer improvements +* Thomas Symalla -- AMDGPU Lexer +* 15b3 -- Image Formatter improvements +* Fabian Neumann -- CDDL lexer +* Thomas Duboucher -- CDDL lexer +* Philipp Imhof -- Pango Markup formatter +* Thomas Voss -- Sed lexer +* Martin Fischer -- WCAG contrast testing +* Marc Auberer -- Spice lexer +* Amr Hesham -- Carbon lexer +* diskdance -- Wikitext lexer +* vanillajonathan -- PRQL lexer +* Nikolay Antipov -- OpenSCAD lexer +* Markus Meyer, Nextron Systems -- YARA lexer +* Hannes Römer -- Mojo lexer +* Jan Frederik Schaefer -- PDDL lexer + +Many thanks for all contributions! diff --git a/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/licenses/LICENSE b/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/licenses/LICENSE new file mode 100644 index 00000000..446a1a80 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pygments-2.19.2.dist-info/licenses/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2006-2022 by the respective authors (see AUTHORS file). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/__init__.py b/Backend/venv/lib/python3.12/site-packages/pygments/__init__.py new file mode 100644 index 00000000..2a391c3e --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pygments/__init__.py @@ -0,0 +1,82 @@ +""" + Pygments + ~~~~~~~~ + + Pygments is a syntax highlighting package written in Python. + + It is a generic syntax highlighter for general use in all kinds of software + such as forum systems, wikis or other applications that need to prettify + source code. Highlights are: + + * a wide range of common languages and markup formats is supported + * special attention is paid to details, increasing quality by a fair amount + * support for new languages and formats are added easily + * a number of output formats, presently HTML, LaTeX, RTF, SVG, all image + formats that PIL supports, and ANSI sequences + * it is usable as a command-line tool and as a library + * ... and it highlights even Brainfuck! + + The `Pygments master branch`_ is installable with ``easy_install Pygments==dev``. + + .. _Pygments master branch: + https://github.com/pygments/pygments/archive/master.zip#egg=Pygments-dev + + :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" +from io import StringIO, BytesIO + +__version__ = '2.19.2' +__docformat__ = 'restructuredtext' + +__all__ = ['lex', 'format', 'highlight'] + + +def lex(code, lexer): + """ + Lex `code` with the `lexer` (must be a `Lexer` instance) + and return an iterable of tokens. Currently, this only calls + `lexer.get_tokens()`. + """ + try: + return lexer.get_tokens(code) + except TypeError: + # Heuristic to catch a common mistake. + from pygments.lexer import RegexLexer + if isinstance(lexer, type) and issubclass(lexer, RegexLexer): + raise TypeError('lex() argument must be a lexer instance, ' + 'not a class') + raise + + +def format(tokens, formatter, outfile=None): # pylint: disable=redefined-builtin + """ + Format ``tokens`` (an iterable of tokens) with the formatter ``formatter`` + (a `Formatter` instance). + + If ``outfile`` is given and a valid file object (an object with a + ``write`` method), the result will be written to it, otherwise it + is returned as a string. + """ + try: + if not outfile: + realoutfile = getattr(formatter, 'encoding', None) and BytesIO() or StringIO() + formatter.format(tokens, realoutfile) + return realoutfile.getvalue() + else: + formatter.format(tokens, outfile) + except TypeError: + # Heuristic to catch a common mistake. + from pygments.formatter import Formatter + if isinstance(formatter, type) and issubclass(formatter, Formatter): + raise TypeError('format() argument must be a formatter instance, ' + 'not a class') + raise + + +def highlight(code, lexer, formatter, outfile=None): + """ + This is the most high-level highlighting function. It combines `lex` and + `format` in one function. + """ + return format(lex(code, lexer), formatter, outfile) diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/__main__.py b/Backend/venv/lib/python3.12/site-packages/pygments/__main__.py new file mode 100644 index 00000000..4890a6c7 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pygments/__main__.py @@ -0,0 +1,17 @@ +""" + pygments.__main__ + ~~~~~~~~~~~~~~~~~ + + Main entry point for ``python -m pygments``. + + :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import sys +import pygments.cmdline + +try: + sys.exit(pygments.cmdline.main(sys.argv)) +except KeyboardInterrupt: + sys.exit(1) diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..acda9a5082ff0d881f84686424533c1027091b6c GIT binary patch literal 3464 zcmaJ@O>7&-72YK&i6ZsmO16q9u059P*t8{5R^8fBYXg?;#6lF?u+%hVfs(7;5xLfK zmzkNBL_sbLBZuCaq6M5o3K&HWDcnPjIW|adz1Wcoy9*zT{v3L9qW~>@>i1^3q?@`O zNW!7-E(N%F&v|8IHr^G%tor5YNX3) z({R$|jFTz%)C{NR*Jiob>3!EI_c=Ky`>t8e;AAM0+`yMi)h)$*&K;%#))ZVRccm2@3qoOYW(N*0vdc+W2vzXe*!|-O zd);+76PAxC;f) zM1IkvrsZm<9Ih^q_5xvIsMTX12;auHM`2z`~YKAIteUcI+;i1=JmzUmLm}Cpq5`TM=U0-@_k}baV z`lLP^w}H^LCoEYRr}^6aLJ~T{q12qevN+Eqe<$R=&0~xmXRby?D6M4=4idH~t(ZRL zxjx4^1;`W@>mkow7M4hSFBXe>2qro}l#H(ddvn5a{gu#OJrp0_Qk}DEf^M7 z!{s7^D>d6J>ozQ5*WEQ_T^U$}H-@sdVS8-&H& z-$`Z)sVKXs1dup?HR_vdDK6))ZuZU;Prh25iS_^_N`$rw1$PwRP=$1q_V`BB8zV33 z@0^XDs1{l6!G9Y6yC0ysWAqza<_#0guaqt0Ub}bEIBckng>M?71dSujgWlatnTG05 zmTs9!D<^&5Yp)PrSA*U0tyHH!ugD8soOH6+ZS%IVZfvA~Xsnxs%;v9j(iZpztCB!e zp#wk&CAx}8xu~*ZjZiAK35~#dZ z0_njNx=6lNJULU6NZwTYu8~~|RX|l&RVqdBqZj+=>M$DeI;va7(}8Ty(4*nK4~Gvv z7(RG6vpsy|e)h<}9_ROdVf6M4{VhB7IKS^b{=w2te&WN;kB@vZzH{{AGw1W^D>lWJln%CMfL)F4lzd%v(7k06Ztt~M zY?vYk^mf+YYg^ecCH0dnU6THrX)vWP>Fxe*8C&TD)h60EI(+EPNo*_<+g@y=!wl1C zrNFojliG|+I~lLH(RVWg-@cYu2IEGOcH#pVw=e12GPg3F{=C9#CN56T3mMmI?MJdS zxL?!vDAYIxeHB!DA%6;8PR6=J0$;6m+SRHycMSZDO=A2xjwVl(6|BKExZ-*oTY-f& z$TCqD7Z6*+HS91NW5MzbZY%9eyQw9?(wkJP^aLUU4G!2k1!6Sb4P1tvf}eQNKq-=E z5?&&%nv7p7>n=Td;)@-d$AWW0G*Wl z4-v>meGmaWPK3a-&=3SnL0E~h=tmAhE(Yjxq$3I)lVXe}4^cHt)gDxZo~SoIyr{o3 zG3sk)Im+XeK`a!bjy#_HK`# z`+R)i_oqL5_4D!a&b7C*TX%o7GjQ_J=z)i$M<0wH-5xF6 zA1FL|Zukd(9GH4e3#Lg6+z5N zmbNK>Ng++5RPIirx?^(dFny?6U-)w4VEz!+@Kf+TZz?DOn>y(E# zkJo(z0TpZ|^lf@u78#!1cyuh|F(vhNRV6gXIE{~kXn@;>ch~6ALMl!@IbO&J;<}(X z#miLfMX;Z+8&v>JO9!8cK-DJ%;{Z>BC0Ee zT+~;oIDuWML@76j!A54Ie5JC+g+vG#8q{-QckziOxECvxSm-rCXy4x$;vjxBY@TJm z*9e@@dP%P1F2G>LoMt&r^nZ=*iI7t^A`s6SoHcoI7@_?~C3cb7*&B O%pCf1rpG*^&HWpQxWYpK literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/__main__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/__main__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..554ff0e9b8714db4f775701d269af0bb8206a6a9 GIT binary patch literal 783 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 JK9?D*|1Vp{2EG6Q literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/console.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/console.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b96ba0984880baaad37a00b54af49d67411c73c9 GIT binary patch literal 2633 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 aQ~zNZ`qEBNn9{!QRdE&X5hM*Ic< literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/formatter.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/formatter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..36a6b748cbe44e861ad00a5dbdb0efa8747daef6 GIT binary patch literal 4696 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/lexer.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/lexer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..643111c4f738d09b8d0689084227e8ad3fcaccbf GIT binary patch literal 38760 zcmch=33waVohMj@8w5!3zDa=;FGv(GNtQ%QvMB049ZODP#%>ZKKnfBG&;?Ku5ztl~ zcZYPGH5n%nG_qGv+w+-PJ7c<&9;>HkHnNk>(fv&iAj(Y$t)5|eGGBMT-IuxHxPI1A?`fPCg#Stp%4LvmmOrl%gbRX8 z=o4I`OEV(&iR@R?r(wU^J}rK=Bf3$2pMKQPXAmi!Zp1ih>NBx>{fK!qr!QyJ(q|d9 z_E|@B`*KI~`tsO2!-#D(zb}8Zps!%Gu&;2msIO?WxUYD$q_2d%H;$B!mi3j1!m!{n z{ju0rt`XegsV=3F$zPZGZIM6qRU&N;(pDjDwafCNpuVZm2zv#W^+myz>#luUE2m$@ zV)GDdbFX?QUyZl3_yWWiy6tLg9g8hOY_Ypuja|)ROAuT7HpYvtz6R9a@HT%C`Wjh^ zGNdSXH>vrWS!@MjE7g20uBsP>J_mbKjW;#wn>8qLjauSbS1r=DvUIDE&aS3g$KvV` zSMM%X>uO`Ms}bAaUZuviv)D$&Ho5KYcJyDzo7~sI-ZbM)i&}Cgi*q1ujk`lFqsz4x zEm_arwc_16cf0y-1B-1#Y`eR|y&G!++ZTv;|pl@tK@(iC0blZECTYFbW$0O^yI=VL6PfXYYC*AhP zvwnCcOKit-yhoFyZgwo-S#1$WOum( zPR|JH{5cKL5r-y`>l_;!ndl$#j0D`0KVjIzZX9BwaM&G?qx(-x^n0D7Zihah-z`Z# zDWN;=J{L&nkD^IZBIk1_Jps3W%sJ>zq{Vw;Q&*es|%#wT9eU3_S ze~{yas{+PJJm;qj_WNIGKj|BFw-0;IweNEK zPX~Ns?fZNI_sF`PKHq8dt$n9+5EJHVKjZeEX&>>NXdjyhob-9uw{>>4`!TQUFt4Yb z!)`xb@Fm_pLc`uRHgVsyb;LP(!sXm9HKMY5{4G;Qz9cLo6b30!E~WX)rHHv8xCKCA zfgmuTuLgThH=%Xt5@t3thp|e7FR=A|90PA3#WUM2cISA&H|iYr3_3?fChUVg?-{og z@Y@Fl_|kFr(~J%bP>z#6m*0-n=40jf1DO9oHU8{L&)`YBbL6aZ!f%({SZZGT(1_0& zu%B^`V8!vxG$;cS)(1i-Y|Ni#9dL;u?HNHT2q+yBLO>%Gc=xs(L(g0B9FW1OAe_?U zG2;peAu*&0X%(P#Ou$?wtf~ERXr(;l!YahBN*Me5z3#L9{TMCn=%7ELqn(-1p8#O- zCqx%paRiX02IP^}AqWUHH z{06Oc`3C#@SwkdRAuKy@oP!=Ipw;8IB6v}_tI_IB%Q;$oCr?PP(-x94n#%)tlev)k zbi+cQ9)AY>8S!UwY5UABO^3_n>D8t7^pQ9UjLPkwN(}0A=Th-)HYe*SiPU zZgIKKxJP_rgdzr7?N0kiK>Bqf6lotG_qg2lfX{vwum?Mm)x$C=xPezH;|u#q=NXhA z2uPk2;{dz%alhNeS~Pgl=Ob{%5XGp^AFuYJ*1F{Jjr;9` zs2G6HDYw6^jqNkFitcVZ%_b`W3#;4fTgP@uH;c)V+BxE()%eLX0Q zJH0MDR_`FtKUS+u{?O9353p_ya01Hil-%+V4-B9(&zN^)U_col`!R|^kxmzF3@S4q zfdZljKv6>Ae*2K*8)eP)c*n*A_7)k1#;PF-vfp7HfN~-NJb`$GOAEp zm#k_J07kMdP;elA(mHY&1d^R%(Ndr(8eQI8;BLSDtS10;rgZ6nF4!^n9(4=Yfl?*+ zv*R8~1{{7+2+FXt(rMnt5yxwwQ9A4KE8vZg3s$qdAlZ5Zgy?Kr&lmc@K)`vzk4a4# zsQp9sVGjrlHUYzu8}LY?et7L2t#sI%bqf)K>)WhuPXJ~|GV zcLKoCYgf`S$VF=peOBYnda%rpjzLbDK%id9^h8P%Xh9J`_1T~FdWZyVwR?u_o&XJ@ z*9QPQg3h?GZk6}y)XF)NeHa*MX0&Yc01yDDKTto`x!v!f{bhgh_?~r}?d|qMK2SGq zd%IfHAtGHniFS7n()Mbz+k4SPhGNhQ6g+s+Dd9DWrlz7;)&)6MEeFJ%GhK zRr1$m0&6a}2Q4!xZyu65=WUt(J16?Bq7V{JA?$Az)%c`_< zm+(xHAWUjdUYU|2`5V$GwTi;aB|2du*OBL(MWfZpXFDJT9@_%|C|G^c2@fEmG@$}z zr{8|sJwY4}UJe5a5L2rbvm0w+0&wUAr^nfj8}dllcQUNT<^UQv>rIwNF!v<(KGE!I zIcy~ z#ubzZK+NwOmj)Sc2@u8^9}dOnJuaXEdp~$qPoTfQ#qS;&YPGLfBiHWW^bb$SLQ2p! z=o=lAfVAw}lp$yX-`mox#5cFvo0}bKSLEz$C_tS*q^e}2de|<+4p+v2O#$H`fC8B! zb)EqHz&vso=MUc>9J|yDI zq{k@8rMMj6F_XWV zN*^y@b=h*!5;-t$Tqxg6DU_H)3+0_R%6HBmfB%{Ho?$P_Dkz6iqh_J((R+euZk*a1 zx8}ccboyv`??={#yIMps_UZvxO)z!&H3Wdw!YTY_UQ#nSU*VJrpOf!TsjxN~cS&#& z#7ah}cb7CS?d$qu5Vd})(zLH@!6EC81Pit*;s`Er^S1>Jd5_s#mE6*)9Z;v%^ zk2XDa?aX!8k48QiiSB+f`s8P$MW2gWKezN?s%N7`JyB~9AMn(HQ<0;DdM*elq69H1 zG*D2F-x^TG2O)7t^avr%o8tc_PSOCXC=3P3*C`dHC@}&M+}kp85}D8_xLJh%tuX5{C|!Iz#0Su0JcPPFH!s0o=OeI*0ag_ignt$ zXsU{ts<5g}4G%78l)*(d)H*IEbAFJD6noIw6uFR0mZ7(Vt7PJsbsS7OKsrdl6dbcZ zV3!#{Sy;o!BN9L{GFO%=Ic2$$Oxa2h6(oT$<2(iiI@&ro;sT8LaPD8m5ukc>-| z!aSp+ZWoCVMkdfOMn_RTR`L`SN`T*nc zlB}sG(9EaFk7m)7ftb!L5u6=f$kU%hqDL6YgP2!G%Pj?-NhH_{7RpkiL%y0xQKZY0OS`hYWAakj*c?Tpkhw1xoJZ8<(z9I0Yg) z^9mVV1_jDa>P5jT>V%N~{FadRluC>z^MEYZxr~*nn0L~I7G*2dl#Nh#nUr>dJ$oeW0SjbjUQ~8OYD*mzml{u_f?8Mo-u!tVmmj%F_MLGt21Z_M ztcBUq_4YVay zq2|>OzC~?W@}!m7@L(>f6s6Gy%(#-R@$0d^tU=JEG+xJq)k1)2xG6&e#$i&IWMo}> zrM{5v1r7F$?m69A@vLx8`;>52bmRp;+e5lNaAu%zm>`X08UAt7eE7lIaLSs&946># z1uz@+ouNcKj~-?~((98(A!!X#oFXr4leVET38O4tV1)XS;OXN&2v``RkzXX`Eei5% zwL{K95xI@?5GUheIhnVg=C7;o5`Z&!lGU8roGHO}Z}o$-Q_3pKMf5yy>! zwRbf7g5%=f7T4dg3RSCSjJF%R?g`q0+S$W1z2WkBS=EK-XP=MwKPp=ruWx$OKezsk zbCHQi`(pj(SpDW}<=3|?)E^ETZs=COEX-Q~86ZF9wQ?zzBR)k5XQ zncjG9{pHgaPtUD>xA|)G{F;R|+itAccfBmWX8p2OT(xm#?^0RqtZoSvesT7TkqvKc zyRvPetaC;eFDkq6*z9AG%{Pl$<5jEP)JFEbksHa2R<+OWT0$3}pME}k?xw8;O?|C5 zV!CuVd?Z@7c1CvxLs87MTzewB7Hp0w9SsO+y>>+!7HrLU2K~S7rEOF}_iXp8Tc>x$ za|>R1`lYAkn6PW1X!X=C#wJW}4cFbUR4I+w@YdEVTNjEt_*)bbDSoTwO3hs0X59vG zWHY;_4^2_)rYyf&*)Iy^t7%M+i?=Hq?+BXaL*lZasX8n!J#p-gUQ=>h{LLLLy}s`! z5zzN^driWROy<4y+8-5I_tt2CRHMiJAZ-HWB8r@zm(L;iitrWjf-o(dk|Cdxh<>M4 z&fq0+N|+LdpqYKqG^y1H0}`wB}X7x zoq#gL9YJwk@h52CZrM} zu@Bq-NwG}J00TwP9kj}lF(evbz#JM$pBGXdCT%B)+=RzH0_6df!n&L+g)=5(ufbpR zk9(E&xG>TXV3Wdu&A@;M<`(BLLN2Hp$4Hc{_6Q{Mz_^z)l5W=kO}5_+!WeWqsa8-7 zmB8NngcOUZq={-Gzwp? zZxq3dyKpNJJYZZpn%PVD3=`!fCrUbv*o1-C%o>f^Rd7#EvXKhwo{af_kzLw5^TV*3JR0c24Pjq0^YF?iz%`%2z9=47Y3*w@d0HkIZd&ck9)y*NPWPwomPe z+e)T#A7oKzYN*-EWCsBj1tGs)5FMiP7+$N(B^9{H?D@w70r0%)=2uGr5Maw1D>Mr% z2#jN=f;D5((gAe?$gqr&4p`1=VPnwBvn>s%#sCo&$#zhhx{IJn>gJI_82EY_hPaRm zn_dyV!%9VljRX|Hq?6fGDyz!WN(4}dtp~;Dv=6(z=qt3bv=RBj()7jlaL!?;3 z_JWKf$z)=E7aj2hKqYaoOS8^)kfJLO>Cvp}9NTFb> zkDBV(Tsy=)Ak;`gC((A5Hc+sU0uloyjIupJ!i=rY)%^)uhW-XrW?H zL@8k<*73YtF!eCDzGz!k|LlTSad@mwPD$XCqQ-1OJefMqk`w*?a7g4>BkD zm6sbN0AtIBFbeA4!!$zx+!Acc2*MKvfUy8THUvl%LHj|;#WoBp5;$rIyYeiwnjAe1 z3@}qez#)#081Sd0spU|Ul4ZzS={QwDvda`bKUCCitZIm~P$2JvY~&{EPSH$eX~5p` z9g$`WMu6Oczh#MyGKeJTFCmcCkw6yOJjC(~Z$L<;ydg-&u8~mjQml(w~yc)}N8h2>UXY zZwFeS;4Ed6)1C>IXBbqfdaJdx!HFR{FWjnZj8%5b84*AuVY^kkE>^pF-iTmIchgiA zFReke<+dsJmAvV^nLy0c96q~XYJL#*t9Ex>gY1GJX;_%l;!g)A9Cc-0rwCJITuEUR z&^xFBhTip)Bu+A97fOvoTo%$vwGaZSBCTYh0adu3)VzjtLn&HV);>+h$XI#J%a4Y1 z*~$s&QNM9W`?kC;Kw>-&;ge3P{^!=46c5k}JEaQIl5NjAK5EMqQ4F_36dXqBIb`L~ z@OipBOu=2bsYxMj)|DgH4%>ElF;q;+9TYdjBVi0BuxMGG&UFluhOEpsZ@KY)!0e&0<+k ztgPpH!$R5KdxFutZh9}w9G{)3i`&46T{t*! zrkk%fmghxwEFF{L01rx>$Z)q%-22D~LEg%5S}6L!o!`1#4;CTDoYh zfdS=f{>bJ_lXIt{wOejlyKmd7!+r6Zy373+`xk3^V>P|k_bt>M249qdcAZdBfL!9~ zI7B^iRhPAw_0$g%lJP}AwX&-BQMYJ4Q@1qemO;>)c|uB)y(=GUZEV8wWftGT-UoUG ziF{_KSW?acl97Xde9SE|^`eX+l<`Aj*hO$?S%*Nj(3WUUI7zV|cd24~qC$cNN^<~D zxe}5nTLyu#sBPu)+R$emHAcr|4BgkLJ_?Rz4BfIp)K~DKqu1Got{QLBR{=NFX4He8 zsE33Jbms|Tz=GTsBDpM-!BZ0OVH<_@ya(hYA6do9I(6J=xCq`Zx3SujlzvvIMG8$B zhaMB8VPwIH20bIsnZYrhv`_*ibRb>)X~gO}qz&dPty9dR|A5AcWclZX+qPnoiiM9c zAy8DoOsQ=}I^I@!j(H2%1S>@HH;^Q)UWPvlQtIVq1Wu}$sEYH#lF2Ghv?1$6Q_DFJ z$e8UJdO^WcR2b<$}S+Tg^ZAY(gu zGtuTzE@~g!Om^JBFaah7NfL{hl&RA)kz5AsEI`80&-gJ}6HS6z2zD9E>!%`tZ8(@= z6H}7fe3hy>P2)*05xcO2>??BOrBze9uUYs!r76>>A*`vS#dw$vg|b#f>0cq$RnAZ) zw6sX6t4fnD;XYWH)+E+?AGL=7Awh+rSH3v?#i*&0wI!`7v?^Is_Tc7WO-YgFWtHVA zgh>!(D9zTgW-}TV4WbI5Seb3#-HZ3x(^V)^#)&!RcVs#3Ab-Ed}MGd)5sORI?MVL(undASUuw zWnhVffuBkW7Nw1>97)Y0@dyStzxYDVY|hkV)Ktw!G;33cu)ZL8lxbyfOUkU$%t{Bc z4+Kjetd-&?sbT`zG^+(uGDc>wI43Irqn#$v+Lj^|yg|Xgrr<3KGJx6zilbnNs%gZH z3e+_E>SaK-Dne%KmJM2c<-HtHUqg?DtlKQ#AhM_zO#wA^xfsD$(0i9=3ME6-ZRYAW zhcITIIk_zyJ9Xi90;sW3QmmI5oXY~na!@G~NB&#%<(ncG zMrbHvN>g=s-nt(4kfY++)5`@i`2J_i%ucY=2gm`)Q9v? zkyC1OMp8UxrVi;s`XQ|>{5<%{Ye0)ngfvpcC#*%&uUFavjv0JiAXj<*geB+|Isp6_ z%`z^GB6>J!2pP~{gmNz|I^pGc@K_qAjJ5H!siC6o2&J@YLKgetY}U_dGdWBNv)c>f zSV+^{QY&I4nq9t_X>BL8kovI&$W}Sh>zObip=YZDWJ%zhG))N&d@&_-E@!}*&_NnG zDlxJlg&$G`#-s$HRT->QPADb?#mc)sq>b`lP%;-3->#^SY`C=Qo?tUKPwxlAbB1KB z&w-J-T~IPJ^lJ5+m9c`g@v>^BPV0<3du8K7nPWzmC@PCruDZPT;#%-GbywU_3eFA9 zyP}od3&q__4@ncy!_9RF85m`-1e6$!rt}>I3`Y&f z2%iv*(3yn4~h)AbMrDWUHt0b20$41ye^(_uIX zL}IQHcK{k4W->*ma3NYI?n#k<(aeE0N?|3Ql#jd&3@|AfEW%ikN2IT!_Dm4@rxZuQ zCF%%)fER^(bc&7xL3)#hi)gDc;nknuhIE2o(GePWN#_!$MI33|FfC@&kR*}qq$ULz zX_7!FKp2Hx5QNAyrDOXUaE-mR&l;>$7Kx>>`0AQdgr*us7r~^3GhG6K4 z7=guUL3he=4x{r!bd(K9I+Rv4R9vOVsZIG{jLX9$5fFvTB0ZL!WP=abx--!tW{HH4 zOrFkVlq5o=vnqq`u>dL7NO;KY1zAy$7(#UgX`2fwF0wRZ=5q3h7&*0Uoz0kLnFZqL zg@mAFdC5hQ%m9xOdRQo2TL+OTs|C6McD9899y0P)j5%7GNuo!lK-6Ta)Ff&TF3D%7&CHZm>ji6prI!_xDtKX1hmy2e<+S1{ z+E^Kvl8z|7ePWZaxNkI8G=em=o04PK>um% zi(&W);J-|p10IT9vR;#c+F%)jTo0nBe@8%S7Ikp+1a)A3=~8~_h1}WPu>11p#nGGj zt?`nEnI|vw&-TYk+9GEV%xacODrYqD{E`cnS<9=rQ$6vj4dJfEstvKK4f9XF|AqIy z@SSI3RgYiyARuLEeX26FZq^d64F0Gx>e$xnql3(;| zKY_0=@66v>BxYZRIL#w`Q!8tcbN+ctaN5L{TMl1tSZFHYgUaPgnOu3HsZdpUPj=kd zh&Lr4TWD-Sf69esN!Fm;DP>}zsqIK#Xh!MJk(jTthb}Nn#sZW6J>~fi6c7=Vvc?P? zjz}cRmA;3-Ppy(ynEm`GT4O(BHg1%j+(#yJ>B?RZtcys0%kC znA*AYFR`Y?ojOjEKxPKQaEu$J>B#+s^mXcJ6#{5yKA?N*G#?tGS=d0RO86aISV8Qk zM8TWXQ#WqzL*ZV@)QoXs-b_=>SRO~O4@@7JIlEw8H6`A*l*~9^Yj{&1uDNMxj@Q*s z?VlOGVXa-NZ<(u$RMArmPsZGD6o2Wt|Eb9}H{I!CRM0O=%(yT9eH78h$yudpG%y%r zxaUC$AutKUk9JN6+gsdi!)^A}7$pZ`B<7xklRt!Xlv7=@@k~oIB?OKG{-qPGu;eCv zH}bVK0|y=_O*>E_PsAEP_=}@1UaPFjr_<3uevCwes`IRSWJE@I4<2>xYU^M{rAb=| z^$_w}E<`|ric}euB9#)Q_wiN{4$?g%Nfm+5ZbSt0<*upXr~gcIOuQNx?2<~zopKmq zhZ7hNXRkB3Xqo^v3x7jpDKc~e94+L5f#!4~r5VJZdMU_sxfW3wQb8QDg#1sZih>PN z4H5;v`M@005+_;$hseMyjS#T_m9AnMkCTU9KWHtYKFC!EQ8#4iL}m$Gr_9{F@brwJ z1M%ea;;K%;< z3DnEO#g{8DR?h8u_u$oo*L6R#d|!_{O&f%p-exRtHtZDq zJyl3ski2>HZ`LH?5_5F}|9fZ>@%NZ2z* z{Z5fcB;&%m5`#M*S1r<6Kd{NiN1O!Qv8iy(3f+T-WDEzE0L!t}odD=$GR3U@m?>2o z7RVtKEfL?&RzyMrZ&60d1{)zp$zsL6inj?pgCPlnG&JHI_A>~azAqDbFqmfc@ldIY4J9#Y>vkH43o? zX*fj54oz@l#z6C7{C#5^?BMCh0#@~@AUi2waahG9vmlGvR>tZ9JM{VWGkNHsnQw2>@(*zqKBLPMsY5RiaB zBvC!yXxSUYvnQQPUkQ-~i?gN9l}k^DY?K+ZK;n)$!$GF>IZH}{anMEu&T31BTqXc* zYJJ8PL|U?iIVWa&>oSuC%AlrS1qrZB!66f{2H>qpIvNsBZ^HP}I?4$d(%Tf$XX9wm zjx0PWd9lJ_D;ir|N^fD&vJsrD9`AGnVPPeM(Ms!U9?L*^Xo=v0^$WER1`VuzH0I*V zrjWs9VRyM~^bEu?Cs#b`K)HBh%^X)pZg5-1K9EU2qCU7~rc8Tghc)+gL_hxgcew8B zSC5VRPagR>v28&-_{O?}h2!4SUUGTBPtO9P+k?jb!01?iqf7cfC}Gi{M;c@xj@Jy* z!H(T=i~9Pvl%Vv%A^_~{FsETq?n$IcXh9({-7QQQQ8@WaN*G`>3e8%|eqt3crUFJJ zZ2OtfEK-6()M807<4%|rd6_F=rpEFlMi1Yu2?HoMLQW)KAQA=_qhb<;DmYXN4%R&k zVya0XG!gut!ItAt)AMH0O-thvXo5XAZ1wT7ig-a~_;jqm5ihTs2}Sc))AR6+g2s56 zJ(_Q)r>=0%OiiTuMuFpYc~y9S#2GmeKDVP^`RTYJc2nd*#qehr&Iu&-vP+cxgEf6Iar~i`+fp zj3HiD6REgCMIMT_^;~bdUKwpYw19`QHSzqi3wg77VgKT)&e*EX`L*$`N3VSr@W#Gd z{DmME>=EzisSwz7Hmn6l<%wDC@#319tx@YL-k6Oya~(^(t}awnUK?3^qkJ8HqxI`= zl&`;|)f99s6}QYi5-aXp*5d)dtzrWXedKnb(Bhh?wR*|ABU;V-S-E4ux?^S|M=a|0 z#&i*Z&p_OB2-85E_CXLQBM@!I#)nrv5{?7JDZoJ&dLRu0hkDxhl=c{WoSqW_itx;~ zdRB280Uw@zmoOtf^B8UN6mVe30{R{WkV>`#pd_QMx@^)A04b6jWLyNoQ>54GsnIN8 z>u6f5XiV9q0x9w3fwUA4g0Z7$0};&`SIBURDAbILTBfuDW{J9NXHNrVN~Tj}OO$cp zBq{b6UD?i10^y_^8h$T81T(vmXJiA!?6C_32cZOj-Sp8mlNhy5NtOHscxEexK)u=z zLRrnpG|C;)=F*B$+B=yOa3tR;v93IZO0_QIq|saYu(5RIQAwX<^g^akPWl+kh%fI1 z;=matoU^P~TF@X!_DOS~QF(TmmA=7gMGmb#mn9@1w1&)=gxA#*y&*GBVHzjR&zVzB zVcLR?DagkutAk{bM+!k3EI2FqU=YoKF7sMtXD2J~i>rf09xntN9;g-wBJ-EQiU)BD zY%R%o4&$JX1Z})PGQwkiY=TAb;lk)+cGQWHy7piWX{~v=K^+m>K~1-vVXdDNHg=Sx zU?4rGplK0Gq+yhnDEJx$Bt4M+fr9rbI84ERq997azoCFph+n6>1q2E6Zl+5Cv7gBK zEIHle<`|2V;3)(S3qxPh-&4eYrGQFK7&v;8ZqS1+fDx2_O1E0ceGYafk|g~N9@B(| z3g%+XeK@Jf1_ADj1^+)`8xg5l5pD#UvNc9cyP}TnYa6dQqRqP&@L+3<+lpsCGkq?+ z{-x)mrpKc78|U5gPetvIEtnpgdOmK+pV=R?)WnOb!%spj8nv#DH#9{}O;d)Mitt9r z9peSX7b<2erVR1oRS|uxxM^9io7Ye6x{W#;-ZbKvA`Uo)$G<)q%|94z>ABu`y&~Fl za3TNT%w${^mJcr0cEoBs=BuuGZg*~&KmMHxV4_u9SAdANEkV1>N;2KFIe<9|Dv|}q zt5+>ncg281HoV{cUiY;x{&@dF^=D_ya=EWbmxC9BbM=>=TU@n1wrc(S@oQV6wYzWT z?}5trXGLXU-K*Vimc)vhqSl>r?bjNvl}B54E?9R)i<<7}w7HTPFRfTCZHd8KNAaBV zYT0}f{M?l8n9-scCFP67P3UYSFxPYC>_Tz-)b5|z3THNabt2sJ(sS`@`>c5>*}7}u zdz<3xw_i7-Uo{8C88Zw%bBmZHcYQ=3nTS?){*ATkPLGI@yR%b-9$_l?S1T1jlcWKV z#M`Cz=xUsgy4LgF+4FDSCqy5h433t>v6s;RxC_NeVJ{!${ z7RM_o<9()D{(+-(Pm$(_Mdm$Q^*^kx+OtXj!%ceJA4yXV5S`9YEh(8vjS2)vcGq~y zyh6+yY)FHH|2F0qjP_T?6=EJ_p|oO|mtT;gWiZzMHu#mb^n-(wMG?~fKpoQeDOf@Pp(7EM{9ulm<;E8D zCpT!ufq_K6!dfe7gR3%V#modx&=x#~N?;4wN}ThQd8YTm(b=PsU9n;Z2&x;!osl9k z8=Kn|tLvOH-!7>LJ6`Q4M@L(yc7wcIwAMnH9iEtT%s=^&^-;{Kc`HcGsEM1vMwLFob6><32^ zvddvYdIdXzMlhbQBlaz%Cu$3L*^rG6O8O5_bDDq&H9XhL-62gjr*|Q8qbDy=c(^dZ zpyw#+G9o8j>JsZ!^=WD}BcGXasr8;v(7dJZYv4K^t)0;U|tlBkm$?KKI>1W*917R#vH6U~;?2 za%QTj2O?;WGZjfTDZCjrha{U0=BlUkRDU|Msd>_QYr40YHamQk62HZT-D}{4ZB1&4 z$y0x<#8%XjY$Nwe-l`lFOjB^kXAG0WfSUG`Oid{f<@rxiy-9C7a$8dbZi*Q{aw%5t z%v%t7eudJ6iSd|y17!TnosQbLdbzNA#p@-rZy0o{xzqxio%JLN)QF~|pSuNEREs%ES139HP#B2?o= z#pI{p!uHwik(Rlg3q|cyyO!(?Z{=LcdBb{7sLt&bXRR~(nelj4-R16!-4XXfRqKo? zUQ`t=T8)#7jcaE1gg4F}zN68W)Ww_DzIFP_>F+i#Gr9ygNqfNfM_!gz6?h_ma9><#qt`lDgll)C%SGh$^VK zRa}m!f{Oe8228^Tt4el0s{63Kq&G+R;iKkWqw)I&k-{8vZ>jP7B_hH!@K1P_WXQ|^ zhyeDWWJip<%<^5Mh7l4T*c5S z7X}bh0YMw~5{IwcMPnX&=qF#|PPhu?>YE(tR~RN~nS%d}AYnY|#D^W=ghhuDlzu_+ zBHaPlbIyP^CJBz=*j~%G9Z^&X-RZL~f4ZeUzRw~A%QE(flKHE&MNT3ohzCn?xcBm* zi-%&CmUyjws`uri@v3V49J*~Qo$*JjTW{Ld&97eU+I6FA*HV7*Qf=L0ZC9+eYrbpV zzqn~%Y}3AlO$Qcg56)=6VS(J49|wurIwBix*gEDm#FF0dxz)BMt3Gyt`pUQ(#vU!v zW3vC$nM{Me|42zFxQCZa(?H5B81&uC(dpaoS#|o7d)7LAk9cp7=)l?F&x!_p#l8G? z+@26EisO7LU=V-mg^^*`@&^2*E4tjeY`U60EE$~ffb||JyKufkFBKtc@I3~QcakF? zQfSGV0S11D#z%lvGqpMEP9QB@jk06796}$X!|8CIFDt+BiqNo33c~Rl$AaWegE`Xh zc}F<9Ly9xxE+1!&Wz90Hm$bsJEHx`fjI_M#EF_YBHU5^n5zDQ4Tn{DM;crRJnV~%^&IY0vL{*Yyd|o2{f(w zOFv36yEX?NlxKZDWCo4;1o~?USzLN1ov|muPm(s}zO&HNGAIye&176ktzc7HP!d;} zlpt+MMnXI)ov>{7-^S{K1`ck(3oPHWR z(tws|UAjNiY!kd|AmzfDD0bg~yHD8zFG<#nkRGKg?K0LBJkf2^5tS{=OI;>5;*~D* zut+P=W#(%yqzzOm{cxGECN$)$1g892cKBEeDc9pjLn0b>DdlBN>DmOT?8_CPx>ZS_ zLX%JE6sns%pg3}Z EzF>Gd-#d6O$aP&g99I0+}$hU=q`_M@tqv5TQOg{&hXBZ;k z=rA0!nWpFzFZKfiU*HC=&kPJCT_-dABPGx$M&%tz>QnBg3DZDWL8Ntz()}3%M?S|o z-=_#Blp;MfH{FlW-60B?pz0~Q`;dae6#N|m9MghJFEYoG2#`pBi~wH=>81Ol6kMa= z9ST+uFi&Ej>Y0hFg~2M0JQF4ete8oQG*5|tN{Pi$uAU}#2qdg}aq|&EYpI$R@++Oi zKhM7hu<_r>oQ*DB70H?Fx{?xJ)mKWf;$q{g^EYS?<$hO$=jh1!-m^LN+Ztl6|;66~uZTP{8K z-J@mRYC7Z9O^em*V%6*5mbvh%_oM1< zOVtOXt=q17u00c7b8w;hU}R&wseL}@TGxAdv8KnumRrRYcX~w(*PR2RrJxdC#lpR_ z6Ommv%37A1S|gti=TLe>tZ7r&60fa$vwJQOttBN-9LlR_FPg$y5VP^hn%Bl7uDSJB zJkiSb8^!JM;<9k(?7o@ayPz~}`EVdov+4fIumH6iy z5q!`g?lBua=-iI`4?A|&;pZ<3?R%`kUvz2cenS!Ne`wU={)blco^tIEODO(_Wg^{| zQ~Z&u@TJ2yh&d}EL*m?o`S6p6j_*IT|Hy9X|H3nu4$;EQBw`5aGjib~Ek}YSM8xQD zY&&1bg#vE+(D~hWI)uFZ^LtiwkLvmPMV4U@Y1Ipv*_GW0P0NhXbg;kCuM;r3M-mJJBhGyn?Q7L}F|CQb-TK(GUu#F>)n%APtHAk zt?=5e?;ZHF1J}0yxFBjf7&RSa8df-6Ir4F4X$MeZ<|d%^$QI>&8udav$)^{WO*(z^ zvQ^X{lFia6rijzekgNMBcom_nfxtm3Qi_t9@W>$8<36~e_;xl42v0s#Dd6ERwPgA zz4nlXo*@LyGUqO)LS_UFNSkcNiIXBoAw46q|5_%p;lE|l1k#s;pt@|FHew06XI!d! z(i}2_NOgpaUm8gv71eW?NfH?3_{N9S05UF{l2;*d6%rk(EE&zfqmnA5O9BIL6Jr~M zkU1&O2RX0LAm>f~!cR1orWx@|8v`m49x?@J$*@bACF!M*355NNAnd`Q$i(c1q_P=3 z&oX0g%LKVgM~-qaj&<47WpfOy^c%$dR@7)@gstNt`N=pwbF zs5(SOzCQx}LzmoyjnxK{tOYV->SvtjIWPOY*`AEzofv7}%SvNgsBuTrG!gr^wgl9dsYasVV4 z>*XloTo*%$jBrU}@CcRBNDi5zH3 z_-3<^wbKWe3M=?y?_2w??4LVv<#4RBYw93Ku?w4LH^XVL%$0RL$d$dd>B^?L z-goz2-Mdh{;oo4&IpTEF#1_11V(^IPsK?l*?Ry|3?;8Mb-< zLi3}G)sIH2AHB7@b*XLBHSPDze`da3|7W?ewmlKcQkgxLg?rnY#=Wh7cgxi+Z#;KR zTxi}BZ&~}U`KtNd+^f0Ywk4UkX6oo-!-iPHhWTUfKlR>I*QEEJ`KY0HsUh{vlOHua zN(|lBREF+ntLkNTj%EB#?TuBB#d$Uu8er_?GUhy2PsJLxBw4#vm-k=XzgXQGt8Se; z_U=g`t+$>65+-wBo!PiR zA~hI^YNoxBizk@?UNP+u6{e38r5VPnq-O-1iIbiwmUc<2G04VJ$WAq$eJMhNuSmca z(WQ}%ogrjl2W}b37}A~BgbaAAoivB^(z%c!d8#3a%(=drA*W%|UxVi}1OcJ&y#$a+ zQVC?h#!!w*;OSDzP>Bg9fdkPC@(M%;Yk_6bI+=^S)=+NNytxVi!}Gpe11-5+^Q8IX zdyKvm{@%WbFAs;zDQ#7Wm(v(+ka6E){X&0TVx^F^g=!Ay^f4ZCJO2dj)Jt0!FHB#y zAlD_S{mvs|otORbr6&9RdwVOsym>;L5P>RH0u`tdP(CDrWEQ`O*l%e`0=`Kg8TeDe zg!+Aij6C@62Yqz}mItpWDE46~l4USCsNOk@O~LT+K70lZA_vlnFuX}Vg6Tuk>`Xjt z@5omg(^cb`3tXY$LwQ4R7XbIDPX1*)sN{J*Sx8F0g%!t%96cxX9!VF)vcBIu+r;e$ZPHf=`jGZsx=+D>LqMO7aE?Is zLtlIMkGO{dnFo+{kP&%cW;i+qJBEY_U;bmZ0qll>E!3wSP!e zQt%l9eSeK5!pMS^YN4=n=F~z#-T4Ffrh?vfyP_t1YN5h0oj0YQI!of(qVs#>d4=#I z43qE5dqRo6aoRW~PIXQ>mn?--kHNn;EY=~^)!oV~3D>tA?g^S)M=T%u z|8PrWAEiAvR~{|xypi9@)1JGuEtcOhYs3c#avjj$7gs<)xn*kC9gQxx^mbii7VR|Gst!AOIWy~SSXZe9z?vVK z^+~fTh3Qwp3KHXsU>rDsGwb@I?x} z?KdkMgyK3Hj#Bsrm@(eUFQG1%{$|B4As1@w}^D_&{E1)%dT$y4>qy*Eh61_v-oY6-4^X%m?-`)azwhfQ2Zd8q+VEY z>{=$0J41rR4mLugt4owUF8SbA8`{^T@Q9sV+@5}HF-tO>U-ZFq%^=_ybiVF$b~?BV zEw0Bq)bGO@as$IU*G@F2rWx*OEtD zUoybUhxn2q=_xmxm)vPMu2FdXm-I819F^jB!x zAojm{(MXUiX8E@Wz6r-W)5cTk%jAuV*@0&Bc{!akZN|qrAR!%A+c&9Aj#O4jja=}g zZrYU101=Ag)WZk*PgoibWwG8BJT88+DTnFf`sw>~!R{;zKFeXyq=S=W-q#4= zOQ_{nkT8RlBSWnESW)9*QAez(W4`fP(R?+WV6DG)ZlP!&Y`*c89oT}uS~ayZURZgd zZMH2^9P!WXStwjT)tmYf--{{u>fV`Wrw@IVKIh3Er0~w!voLl=48-^cn4}tC7-HNr zI~2_hZU311{SylQ69rKO_$1N9?;5cppvB9QKHd5c9QRzd5-{34bJ2(5}P0Q-rg;n9*mk(Y%IIn--`koc4K>A|N zidHP@oF*UnMdcWSq7pJ_3lByfStwdFW%`+;_1&tgRlkG}=dwF2$j8@=6u*lP^NT=j z&6LkKQ`ab%rN)u);YHz=Zr`#lU*S#Y_Vucu9qe zTtlKL%jg96?83KF*1>KO8VUN275hj-`r&X8KOo9nJ_zt>Gw@H=gIpw;J%hIR{KLK{!~XBfLs|X=SUbh_qOIko=)JRL&DT z5PCz7rf|M4*;)&D*tFVShNG#J_bD|_2%ChL|BmrE)+2-RB4C}NWwsuF%Woo3bVJYV z1A~iY$JlRp>1V&h1d1ulyXt2?8*X!{gOF_@y72W11tXIoP9>SRkmoGmROL6M4PxmG z>17{U2k^NbE$e_Gs2Oh#YMy@jzWDT!pZB2Io`1xir*A!zR#18xG13zV;Gsv;*1`Pm zBt$78dYNyc3h5GIj@n*C1*@HkN~V-7%OmhBLKTDA7# z9`aaqWl!WYb2wBgR8+pUAtGMdba~swZF40HRqcyan_^X)t{JZD7OHk%AG|I_s}97= zE5aKhbr&~Bj^lXE=jX*MPXmsYufNL*5q>oA zB4VZY=*f&sFxLt=N2E?XE7TMT4N~m8)~HjRA7m zsczBK5;L{T72Y(p-Y%+HEZPt&!WqG98?Kiu6zyF!?TwoD#tTb{*jy}J8!KG9SlAvb zY+oqsoHE|Fm2i-;)c(l3lUFC-ettzyU}@PPRBgl88CT4@{Dvvr3iKQ0Sf&xz&9b!% z#cQX`KQraNY{VB`qIo;X7xr`0&n?=TVz#Dtv~#=OHqUQ~=550#$!yzkMA7^h5>I(< zn#x)F4be^eV|n{;nhxNRMdQVI%ExSCTG$j3zDki+vx)Ad**-}x2>3Br!lz;r zy`bPN8e_6bCBpFWC49%}K)kFxsw-a66)T@mpmdH$xDcGt*BdBzD}sbg{YX9kY90Uh z8j)={7&kFYbf3SCzw)!srzi5%XeMmp`Ypn@l8FLLNq|qlN1nv!%qa4)7{r8n4#bT1q5rEt6Jvpj(M=-Cx;oU@*n9f+jdMz z$aDvPNO6ozXH$|sP4`V(M}01wZ{99FiP6S?!G8!L7Ktd{)tE%xaj+u%5%lT;?$lMfqqx)E)81s)C&BWqghAve;|sLGbNF> zUkG&f%bi-Qcu2e}P`FaN8&ToP6@h;5J+9d%7Ow2j6d}&LBH(AGwNk7IuZec-{htEv I*rxdZ0A|YE1^@s6 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/modeline.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/modeline.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..80b50db49cd4141f628fe1d2c26f9b0c8b430441 GIT binary patch literal 1569 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/sphinxext.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/sphinxext.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45a20358316223845fe95c43cf39968633c141a1 GIT binary patch literal 12111 zcmb_iZEO=~o}aPD?{@6ij`N;O65b5KAp|G{NP)aSAV6Da*=9>!lbIwoj-Ad-2=R`q zXoarHF1IAx+e@OhhuK}JLEY2l_H`Vm9FK~C$##t~D$3DSmsGh^(xFs6PhWA3+U zh~orfxkfNn*6^uT>8HPhv0Wqj?GqYrNq`~=!c0RLofsNnBLeT^M~8!vtL#-l&VKKy zV3zaG$+c*x#zy#HG(rnedWem%TtHyyz?cvn2?)Ufnu!jKAz65VI?+Ko%wALp7oChg<-!C6v8YY zD>wUm^hlH$3$yL*^k&{{hClpb?_j||IKcC{d|dJNbHhXvpz z2VxOM>)pN zu%m2*VIu=UmY1{vh5>rR!7#(I5wA|7cs4vJX`^F;M4=6kG*`lsW-!8|@T2adANyN| zqa$p~P~>V$7t3E3qN6P*q5>Pe3|+Mx3k+O_E?Tazkt;3X;H8$)31K)IY4f%2 zZsCIhyK@vO1cq213KVnOk~g=$(FsY*jYVSibqdf28ONc29_&0ZNj#{m`LJw?x?YmC z*l%=9cg#1WEHw$^UCXwt)01&-NjbNqo!h26v$oP3Z%@BHe>7#QOEllL`Q+m5Dd+aI zvw1_Y_pWV+T)Z{q+?sZJpDa!`w%xV0$>l96XG_}II^FpPXLYjXWZHQuX+8D8?!NKf z^m}Q0{S@`URyH#*7n}_ys1Krxm+skGr!)_Ybi(~f)vc<<*OuJ(jQfDnoNv~*N|2^* za_U&t>bP-o`eephpR(3xtlLu7Z5ivKl=V=?dL(5%a>se!+O?v^x)1HH8}aFQ(pa+w z>+z&|M#Uqm=U+kme#!fGaH7zGB1XtKDLY8uM8!2jpbp?qG1N68t`qVK7!=BL)F_&) zU+7hZ0;=F23F0kK5^;Up0HP_obvU9z2o+I;qJc4>CQ?N^ZWN87l0q9b1-TA2U3kVI z88?Yk$W_QwQyN0lsnnshTg@*#)v+_iVv18VsbdP|Rb`!CI5@dXFn9d7Z}zZ(VHpcpf{@Lg3O^IKa`W#X{InaP@NV0QK>C@c+RV920tmFVU|&lLy%WoM>7(_uno{aag8U-x&qUpseZV7>c*mIEbr4LyW8LL@m8LyEZG8j*xp z`?2mExEx^0sWXqscVZg)P)ytLj{i2{)qoKK&s)-tabX^5(loaf9+E{drG6Yx?7(_c zp<2G2_03y+-=T1HAQ0v|eCwLBK#Q-3Xp;EC?#xzJ&F`7tlF%gD5}OlzvV2FJCyvk!6 zWpcv$A5eJH>W>4J4wisfj+gD z*$H_dk2TG-L~ehqd272w`X7^TNfffcS2)MVq|ZxnyC5Z*3tS`_a(p4_WCj?3@_ax? z1YOg$I}rVZoCEz8CktR<8XQPfSAyIg%GnW3Tq|n12CzbC!!`tvKjfkf0bJJHLaR@3 z6}`nN0LN)!Kx19a4hlA{7yuDTwH9tx%-JI&bUJ`C8D?oNI{uiXy_BSl1xF=)U=;l+ z*vilt4^C4k8bm+JkP}I+2?mY6X=F$kmb8#7l_*8PpqR(!J*!EDwN2w3_(*=}CHB&v zw)^>p6VNB$57GZk5)Ym3sm=%Wjfsw>m(ukIR|(3tW7aaGpVxlj@?_oRb0=p{&hs}X z7A6)w>H1yw-K`JR%*4Kh*nRi*6%#aBu@aEIe|GuUM`(I7x0H5eMW70v$V9o=;Xh|V^Ubee52y)n zSsQo#&e=Nu>P*|ryC3XLI$NhYA^O5qGe0o%a?;xP?W&e=?Uu*$SJp}jBntFc%-XG< z6m8?ZF(NhG|wJsbvB8w7N0VT0bf6Shvu<%LHCf`mH z-qg5HuC{3LuU{If`^{oK9!^Q(l(WS>4e)jK0( zE9jf{1$(Aqf2v~tvU@r3>+*EP(W%~SWn-qYIaS%bIFV^Nk%C|4iK&wht)(~mru)9A zXi9V~mfkwP_~tJ!-oBWwIGC+#yct{wE}GMIZJD}*sk(#9op;L8b;mPxXH#`&ldqgh zzW!#quK#WwlYE=WR@dJ=uy7!8ezD=!g>?1KYz>_#T{u2}AaOJiOnf(4+4{9fS5@*2 zp|janOfc3}4^eT9oU%ULw&#DfWYxZG-KIxcqPlK*@9*kfS?avg`MLhHQy=kQ|E}CR zc)z~oZrv;Qt6p6rpz2h~BfZJg@Qp`nYFdF6vfeOHn`d86yEff3Zq6Dl^6P>5Hxh3z z`R|ORUFYu^Usto$TCR#!LTB<#b!M%01U$17S%+(;XV!v`zLcW@pS`msS!eZpXTq@1 zH{Y>X3O0DnL{;qk1~iv%MGNgg7d_cMqN0iEiFwx#-%A>6zg;~{)W1r8D-WB;BZ$vh zN{+kqpB*+IFLC^<(SdP=337g0qJ#JeyN*Cc3;(rdFk?>vC;|Z#fGQ%ajces66{o4E@+;BOF6n&bdxs0aX3y#PiFaEdCJ!5PiPnw%jt0>l9a3B8H9xo}z}ni)6~7mY)t z>Y~Oia1v7lCKYqcMN3gglrx_sA>_}JM2l!t$Au6gghI3-@b&`iV05ApoH#wW&UtFZ zTg7ebK!{DqpAD(K!Fix9)Ucr|crBc10foXit&EW&)HADiiCDsY2+$DC?S=r(!{!ZH z@cOj#5Zdx|tzvnMDF(aDm@yVhFpk?rdk77~hQL^Ukq1?Ahv?u-g&k@Y#wM1)x|QJS zeVhk;-zQ=3?YQ^c9yOKY1y~)%5d)qA@MU94#ZqYP{8*dIafAR4ZVaF1GQiEkQ97ZW zOLRgzx7zNy4Zw{xgyv*J;2ca@@e&xXChm%87*E_K>O*J^HU!Z;ly~t$Z5s;VRWvf? zp}j@&b8}ZYDF>x1o>^OlaBA()OHWf|s+ek(zlxUEFtuv=3wx`>eNlT;Yl}9~HJCri z1+Z%t4Oid@V1~T@-yn5Gq$loPCkO7La^QY}9JocdXb{clY0YS^*Hgqgy`z7w-l>05 z?_8YCX&rOC3@PFaCrj3GDJ`0Dztvd-f97gEkKY)1r9uau-Vc={LvWJ3F)$5kFL95k zE5QQM+_IjD_+_EvLXPs%_7sz3rv&!BrGPsj9R%j-21as2}T z3ZA0;s#E&_pwOh8LvtwMPneWERJ1}#0V~Z^%mgz!(i-23$PRFX@N)kYQBr3ppz!h|~0XFjj2cD$j5Y0Y3F``utqpZjxx|N=I$OD6(-;^RBnbBnJm3E zX5A{EtjpdV%^ksk)G0T1a2Xh2Mxv1*o@~R7V7JoH3>S!sFNe2c5+L`>P$8x< zn`5~$tRXwVF(*`mQ+>RCg~M!?fr=bjMXntqpmWG^95N7h7^4n|zCyV0$L$pt z#72la{t_GYA4ms5x(uUYp3^~|hb1iRg1lzjQK<2l-05xL-oU!(Q*(ZZBttO5kHY1j zWEc$y!*ImTeFw|l#4@wrAB5`xPVoC>C!V7**XWnMc)wpV48d)$AaIhw?}uH79Nj2v zydar_Fa}--LK+k#Tm4E2;qWADcc$$-vz0YBs~4(g^bcIM^MgNU z@41?@wM{oKE?ivPm#*DC)05rNnsm?!Puj6<%@YRoA;p z=uOSjXQq0fy1hQ3PuaI7?JW;H^~uKl%bsOpl0KUD97{TmWj$LGS5h8d#&aO$Igs`o zTt1rebYwhTDNk40)06R>PI*p$K9KgjHg!A)3IApy(X)7b>D<4#mM{EsO=@pq*;0WpHFeBh7fU~L;%-Y4-L;5dzdr&w|T{Fc8OQ}e*5$R#)8w~Jrq{3tElk5f61 zT_@l%uN*|3jNY{C(3>968;)i}DCWI^mtC@<8oa8$2cWn7*8`$n)T;h0T+A7YeN7NI zuKS9>D8N;b64Z8p!;D2Qx$@o)T-zCneMJENl_r$GXj17CM8U@eOrsAK!FaG{Ph1S< zyldk#Yn#y8s4Gwm^)Y6~qRvRACK?B+4gTHl!MC#(9Z}xr1>er5IBMYTn0wB?cKYb~ z^Ki@YmvJ0gi)#MDmjX|FdBAD}kDN0}d`^De@Nd-QIxmmA3BiZ!fR13CKF`CG`QOi|Le%T`qQg~#nkebu#+z~hgnbMTzEEIPymh!9@& zYzW0ZA`tR=E`%6pLx5A8BH9h*yV&WLD)WHqsG{0Uw@$n1ifT7qQSFvLzl!TXi&bwJ zB)keWY991lr=|-}_=ZZi5qVx;0R2L}P`~K%`o+-G+lR_Ukz-j>OC?7^Qk9Q|xW9vL zWp}?IoNyH~!E@74fDk}&aX}Piku4&vij10+?odcSM3N|}UcDluav!pg?!uzHRPwm% zia;vDSPt*8n&QH!jQ1<3!7o6BlGv+wjqi=6JA>Y8bK!W;dFzxlm| z?=80dvg39~s=hr_-tr-P0OZa)3H?fvHRA}6$|zUXUI=` z3TM1kAWHSE3$6wB<{GC=Hye_X2 z?pD@i_{WFg;)REApjgSGRQDs8LcX%#5E5{x;y7Gd1$BBcg?HbaQH>DH@<$-L zMtrT&>Wtv*x+*45!8LZ0^5kNAbCRkBlrVW}l`7TMu5M}9)vaD7U)I&E9@98k8a|Jh&)!ug#DGo?*yAwYMZOfw$a}Mde$7MR1pI103kjf5R&G z2NqQemsRV|Jpc=`!q_PH_dxM~;Lqdtkds7~GGwT-6je4e^7HTBr&{G9!)24q*$Jqk zeDfy2XUxh+a%Irx7{2-d(!xbo06ubq53_Mjk~?1}f4vM}TO;s9#V3_2U!wW2sZah0 zTK=$(dmjq9DU3eC=%*OT8-e58utRWvuZUj<_bVtx_me*b5g-PV{90oosWlHlTKul7dX{E$sr6!FlRYv*YHelu>_qc|vnejd%@VO@FNOHG4{r?AVNZ059 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/style.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/style.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3a25edb00159c8ee232d8c10b8528a1b82d0ef40 GIT binary patch literal 6705 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 Cr=BqY literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/token.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/token.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..50b426dfc064d86a6cf8acdc09f75d8185a8d8a5 GIT binary patch literal 8194 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 ML+i=1d6QcI1N7uj{{R30 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/util.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/__pycache__/util.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a02e7512cc9001ccf1557c202d7190cfd784be85 GIT binary patch literal 14083 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 L%D-~>VVL is one of "lexer", "formatter" or "filter".') + special_modes.add_argument( + '-V', action='store_true', + help='Print the package version.') + special_modes.add_argument( + '-h', '--help', action='store_true', + help='Print this help.') + special_modes_group.add_argument( + '-a', metavar='ARG', + help='Formatter-specific additional argument for the -S (print ' + 'style sheet) mode.') + + argns = parser.parse_args(args[1:]) + + try: + return main_inner(parser, argns) + except BrokenPipeError: + # someone closed our stdout, e.g. by quitting a pager. + return 0 + except Exception: + if argns.v: + print(file=sys.stderr) + print('*' * 65, file=sys.stderr) + print('An unhandled exception occurred while highlighting.', + file=sys.stderr) + print('Please report the whole traceback to the issue tracker at', + file=sys.stderr) + print('.', + file=sys.stderr) + print('*' * 65, file=sys.stderr) + print(file=sys.stderr) + raise + import traceback + info = traceback.format_exception(*sys.exc_info()) + msg = info[-1].strip() + if len(info) >= 3: + # extract relevant file and position info + msg += '\n (f{})'.format(info[-2].split('\n')[0].strip()[1:]) + print(file=sys.stderr) + print('*** Error while highlighting:', file=sys.stderr) + print(msg, file=sys.stderr) + print('*** If this is a bug you want to report, please rerun with -v.', + file=sys.stderr) + return 1 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/console.py b/Backend/venv/lib/python3.12/site-packages/pygments/console.py new file mode 100644 index 00000000..ee1ac27a --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pygments/console.py @@ -0,0 +1,70 @@ +""" + pygments.console + ~~~~~~~~~~~~~~~~ + + Format colored console output. + + :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +esc = "\x1b[" + +codes = {} +codes[""] = "" +codes["reset"] = esc + "39;49;00m" + +codes["bold"] = esc + "01m" +codes["faint"] = esc + "02m" +codes["standout"] = esc + "03m" +codes["underline"] = esc + "04m" +codes["blink"] = esc + "05m" +codes["overline"] = esc + "06m" + +dark_colors = ["black", "red", "green", "yellow", "blue", + "magenta", "cyan", "gray"] +light_colors = ["brightblack", "brightred", "brightgreen", "brightyellow", "brightblue", + "brightmagenta", "brightcyan", "white"] + +x = 30 +for dark, light in zip(dark_colors, light_colors): + codes[dark] = esc + "%im" % x + codes[light] = esc + "%im" % (60 + x) + x += 1 + +del dark, light, x + +codes["white"] = codes["bold"] + + +def reset_color(): + return codes["reset"] + + +def colorize(color_key, text): + return codes[color_key] + text + codes["reset"] + + +def ansiformat(attr, text): + """ + Format ``text`` with a color and/or some attributes:: + + color normal color + *color* bold color + _color_ underlined color + +color+ blinking color + """ + result = [] + if attr[:1] == attr[-1:] == '+': + result.append(codes['blink']) + attr = attr[1:-1] + if attr[:1] == attr[-1:] == '*': + result.append(codes['bold']) + attr = attr[1:-1] + if attr[:1] == attr[-1:] == '_': + result.append(codes['underline']) + attr = attr[1:-1] + result.append(codes[attr]) + result.append(text) + result.append(codes['reset']) + return ''.join(result) diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/filter.py b/Backend/venv/lib/python3.12/site-packages/pygments/filter.py new file mode 100644 index 00000000..5efff438 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pygments/filter.py @@ -0,0 +1,70 @@ +""" + pygments.filter + ~~~~~~~~~~~~~~~ + + Module that implements the default filter. + + :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + + +def apply_filters(stream, filters, lexer=None): + """ + Use this method to apply an iterable of filters to + a stream. If lexer is given it's forwarded to the + filter, otherwise the filter receives `None`. + """ + def _apply(filter_, stream): + yield from filter_.filter(lexer, stream) + for filter_ in filters: + stream = _apply(filter_, stream) + return stream + + +def simplefilter(f): + """ + Decorator that converts a function into a filter:: + + @simplefilter + def lowercase(self, lexer, stream, options): + for ttype, value in stream: + yield ttype, value.lower() + """ + return type(f.__name__, (FunctionFilter,), { + '__module__': getattr(f, '__module__'), + '__doc__': f.__doc__, + 'function': f, + }) + + +class Filter: + """ + Default filter. Subclass this class or use the `simplefilter` + decorator to create own filters. + """ + + def __init__(self, **options): + self.options = options + + def filter(self, lexer, stream): + raise NotImplementedError() + + +class FunctionFilter(Filter): + """ + Abstract class used by `simplefilter` to create simple + function filters on the fly. The `simplefilter` decorator + automatically creates subclasses of this class for + functions passed to it. + """ + function = None + + def __init__(self, **options): + if not hasattr(self, 'function'): + raise TypeError(f'{self.__class__.__name__!r} used without bound function') + Filter.__init__(self, **options) + + def filter(self, lexer, stream): + # pylint: disable=not-callable + yield from self.function(lexer, stream, self.options) diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/filters/__init__.py b/Backend/venv/lib/python3.12/site-packages/pygments/filters/__init__.py new file mode 100644 index 00000000..2fed761a --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pygments/filters/__init__.py @@ -0,0 +1,940 @@ +""" + pygments.filters + ~~~~~~~~~~~~~~~~ + + Module containing filter lookup functions and default + filters. + + :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import re + +from pygments.token import String, Comment, Keyword, Name, Error, Whitespace, \ + string_to_tokentype +from pygments.filter import Filter +from pygments.util import get_list_opt, get_int_opt, get_bool_opt, \ + get_choice_opt, ClassNotFound, OptionError +from pygments.plugin import find_plugin_filters + + +def find_filter_class(filtername): + """Lookup a filter by name. Return None if not found.""" + if filtername in FILTERS: + return FILTERS[filtername] + for name, cls in find_plugin_filters(): + if name == filtername: + return cls + return None + + +def get_filter_by_name(filtername, **options): + """Return an instantiated filter. + + Options are passed to the filter initializer if wanted. + Raise a ClassNotFound if not found. + """ + cls = find_filter_class(filtername) + if cls: + return cls(**options) + else: + raise ClassNotFound(f'filter {filtername!r} not found') + + +def get_all_filters(): + """Return a generator of all filter names.""" + yield from FILTERS + for name, _ in find_plugin_filters(): + yield name + + +def _replace_special(ttype, value, regex, specialttype, + replacefunc=lambda x: x): + last = 0 + for match in regex.finditer(value): + start, end = match.start(), match.end() + if start != last: + yield ttype, value[last:start] + yield specialttype, replacefunc(value[start:end]) + last = end + if last != len(value): + yield ttype, value[last:] + + +class CodeTagFilter(Filter): + """Highlight special code tags in comments and docstrings. + + Options accepted: + + `codetags` : list of strings + A list of strings that are flagged as code tags. The default is to + highlight ``XXX``, ``TODO``, ``FIXME``, ``BUG`` and ``NOTE``. + + .. versionchanged:: 2.13 + Now recognizes ``FIXME`` by default. + """ + + def __init__(self, **options): + Filter.__init__(self, **options) + tags = get_list_opt(options, 'codetags', + ['XXX', 'TODO', 'FIXME', 'BUG', 'NOTE']) + self.tag_re = re.compile(r'\b({})\b'.format('|'.join([ + re.escape(tag) for tag in tags if tag + ]))) + + def filter(self, lexer, stream): + regex = self.tag_re + for ttype, value in stream: + if ttype in String.Doc or \ + ttype in Comment and \ + ttype not in Comment.Preproc: + yield from _replace_special(ttype, value, regex, Comment.Special) + else: + yield ttype, value + + +class SymbolFilter(Filter): + """Convert mathematical symbols such as \\ in Isabelle + or \\longrightarrow in LaTeX into Unicode characters. + + This is mostly useful for HTML or console output when you want to + approximate the source rendering you'd see in an IDE. + + Options accepted: + + `lang` : string + The symbol language. Must be one of ``'isabelle'`` or + ``'latex'``. The default is ``'isabelle'``. + """ + + latex_symbols = { + '\\alpha' : '\U000003b1', + '\\beta' : '\U000003b2', + '\\gamma' : '\U000003b3', + '\\delta' : '\U000003b4', + '\\varepsilon' : '\U000003b5', + '\\zeta' : '\U000003b6', + '\\eta' : '\U000003b7', + '\\vartheta' : '\U000003b8', + '\\iota' : '\U000003b9', + '\\kappa' : '\U000003ba', + '\\lambda' : '\U000003bb', + '\\mu' : '\U000003bc', + '\\nu' : '\U000003bd', + '\\xi' : '\U000003be', + '\\pi' : '\U000003c0', + '\\varrho' : '\U000003c1', + '\\sigma' : '\U000003c3', + '\\tau' : '\U000003c4', + '\\upsilon' : '\U000003c5', + '\\varphi' : '\U000003c6', + '\\chi' : '\U000003c7', + '\\psi' : '\U000003c8', + '\\omega' : '\U000003c9', + '\\Gamma' : '\U00000393', + '\\Delta' : '\U00000394', + '\\Theta' : '\U00000398', + '\\Lambda' : '\U0000039b', + '\\Xi' : '\U0000039e', + '\\Pi' : '\U000003a0', + '\\Sigma' : '\U000003a3', + '\\Upsilon' : '\U000003a5', + '\\Phi' : '\U000003a6', + '\\Psi' : '\U000003a8', + '\\Omega' : '\U000003a9', + '\\leftarrow' : '\U00002190', + '\\longleftarrow' : '\U000027f5', + '\\rightarrow' : '\U00002192', + '\\longrightarrow' : '\U000027f6', + '\\Leftarrow' : '\U000021d0', + '\\Longleftarrow' : '\U000027f8', + '\\Rightarrow' : '\U000021d2', + '\\Longrightarrow' : '\U000027f9', + '\\leftrightarrow' : '\U00002194', + '\\longleftrightarrow' : '\U000027f7', + '\\Leftrightarrow' : '\U000021d4', + '\\Longleftrightarrow' : '\U000027fa', + '\\mapsto' : '\U000021a6', + '\\longmapsto' : '\U000027fc', + '\\relbar' : '\U00002500', + '\\Relbar' : '\U00002550', + '\\hookleftarrow' : '\U000021a9', + '\\hookrightarrow' : '\U000021aa', + '\\leftharpoondown' : '\U000021bd', + '\\rightharpoondown' : '\U000021c1', + '\\leftharpoonup' : '\U000021bc', + '\\rightharpoonup' : '\U000021c0', + '\\rightleftharpoons' : '\U000021cc', + '\\leadsto' : '\U0000219d', + '\\downharpoonleft' : '\U000021c3', + '\\downharpoonright' : '\U000021c2', + '\\upharpoonleft' : '\U000021bf', + '\\upharpoonright' : '\U000021be', + '\\restriction' : '\U000021be', + '\\uparrow' : '\U00002191', + '\\Uparrow' : '\U000021d1', + '\\downarrow' : '\U00002193', + '\\Downarrow' : '\U000021d3', + '\\updownarrow' : '\U00002195', + '\\Updownarrow' : '\U000021d5', + '\\langle' : '\U000027e8', + '\\rangle' : '\U000027e9', + '\\lceil' : '\U00002308', + '\\rceil' : '\U00002309', + '\\lfloor' : '\U0000230a', + '\\rfloor' : '\U0000230b', + '\\flqq' : '\U000000ab', + '\\frqq' : '\U000000bb', + '\\bot' : '\U000022a5', + '\\top' : '\U000022a4', + '\\wedge' : '\U00002227', + '\\bigwedge' : '\U000022c0', + '\\vee' : '\U00002228', + '\\bigvee' : '\U000022c1', + '\\forall' : '\U00002200', + '\\exists' : '\U00002203', + '\\nexists' : '\U00002204', + '\\neg' : '\U000000ac', + '\\Box' : '\U000025a1', + '\\Diamond' : '\U000025c7', + '\\vdash' : '\U000022a2', + '\\models' : '\U000022a8', + '\\dashv' : '\U000022a3', + '\\surd' : '\U0000221a', + '\\le' : '\U00002264', + '\\ge' : '\U00002265', + '\\ll' : '\U0000226a', + '\\gg' : '\U0000226b', + '\\lesssim' : '\U00002272', + '\\gtrsim' : '\U00002273', + '\\lessapprox' : '\U00002a85', + '\\gtrapprox' : '\U00002a86', + '\\in' : '\U00002208', + '\\notin' : '\U00002209', + '\\subset' : '\U00002282', + '\\supset' : '\U00002283', + '\\subseteq' : '\U00002286', + '\\supseteq' : '\U00002287', + '\\sqsubset' : '\U0000228f', + '\\sqsupset' : '\U00002290', + '\\sqsubseteq' : '\U00002291', + '\\sqsupseteq' : '\U00002292', + '\\cap' : '\U00002229', + '\\bigcap' : '\U000022c2', + '\\cup' : '\U0000222a', + '\\bigcup' : '\U000022c3', + '\\sqcup' : '\U00002294', + '\\bigsqcup' : '\U00002a06', + '\\sqcap' : '\U00002293', + '\\Bigsqcap' : '\U00002a05', + '\\setminus' : '\U00002216', + '\\propto' : '\U0000221d', + '\\uplus' : '\U0000228e', + '\\bigplus' : '\U00002a04', + '\\sim' : '\U0000223c', + '\\doteq' : '\U00002250', + '\\simeq' : '\U00002243', + '\\approx' : '\U00002248', + '\\asymp' : '\U0000224d', + '\\cong' : '\U00002245', + '\\equiv' : '\U00002261', + '\\Join' : '\U000022c8', + '\\bowtie' : '\U00002a1d', + '\\prec' : '\U0000227a', + '\\succ' : '\U0000227b', + '\\preceq' : '\U0000227c', + '\\succeq' : '\U0000227d', + '\\parallel' : '\U00002225', + '\\mid' : '\U000000a6', + '\\pm' : '\U000000b1', + '\\mp' : '\U00002213', + '\\times' : '\U000000d7', + '\\div' : '\U000000f7', + '\\cdot' : '\U000022c5', + '\\star' : '\U000022c6', + '\\circ' : '\U00002218', + '\\dagger' : '\U00002020', + '\\ddagger' : '\U00002021', + '\\lhd' : '\U000022b2', + '\\rhd' : '\U000022b3', + '\\unlhd' : '\U000022b4', + '\\unrhd' : '\U000022b5', + '\\triangleleft' : '\U000025c3', + '\\triangleright' : '\U000025b9', + '\\triangle' : '\U000025b3', + '\\triangleq' : '\U0000225c', + '\\oplus' : '\U00002295', + '\\bigoplus' : '\U00002a01', + '\\otimes' : '\U00002297', + '\\bigotimes' : '\U00002a02', + '\\odot' : '\U00002299', + '\\bigodot' : '\U00002a00', + '\\ominus' : '\U00002296', + '\\oslash' : '\U00002298', + '\\dots' : '\U00002026', + '\\cdots' : '\U000022ef', + '\\sum' : '\U00002211', + '\\prod' : '\U0000220f', + '\\coprod' : '\U00002210', + '\\infty' : '\U0000221e', + '\\int' : '\U0000222b', + '\\oint' : '\U0000222e', + '\\clubsuit' : '\U00002663', + '\\diamondsuit' : '\U00002662', + '\\heartsuit' : '\U00002661', + '\\spadesuit' : '\U00002660', + '\\aleph' : '\U00002135', + '\\emptyset' : '\U00002205', + '\\nabla' : '\U00002207', + '\\partial' : '\U00002202', + '\\flat' : '\U0000266d', + '\\natural' : '\U0000266e', + '\\sharp' : '\U0000266f', + '\\angle' : '\U00002220', + '\\copyright' : '\U000000a9', + '\\textregistered' : '\U000000ae', + '\\textonequarter' : '\U000000bc', + '\\textonehalf' : '\U000000bd', + '\\textthreequarters' : '\U000000be', + '\\textordfeminine' : '\U000000aa', + '\\textordmasculine' : '\U000000ba', + '\\euro' : '\U000020ac', + '\\pounds' : '\U000000a3', + '\\yen' : '\U000000a5', + '\\textcent' : '\U000000a2', + '\\textcurrency' : '\U000000a4', + '\\textdegree' : '\U000000b0', + } + + isabelle_symbols = { + '\\' : '\U0001d7ec', + '\\' : '\U0001d7ed', + '\\' : '\U0001d7ee', + '\\' : '\U0001d7ef', + '\\' : '\U0001d7f0', + '\\' : '\U0001d7f1', + '\\' : '\U0001d7f2', + '\\' : '\U0001d7f3', + '\\' : '\U0001d7f4', + '\\' : '\U0001d7f5', + '\\' : '\U0001d49c', + '\\' : '\U0000212c', + '\\' : '\U0001d49e', + '\\' : '\U0001d49f', + '\\' : '\U00002130', + '\\' : '\U00002131', + '\\' : '\U0001d4a2', + '\\' : '\U0000210b', + '\\' : '\U00002110', + '\\' : '\U0001d4a5', + '\\' : '\U0001d4a6', + '\\' : '\U00002112', + '\\' : '\U00002133', + '\\' : '\U0001d4a9', + '\\' : '\U0001d4aa', + '\\

' : '\U0001d5c9', + '\\' : '\U0001d5ca', + '\\' : '\U0001d5cb', + '\\' : '\U0001d5cc', + '\\' : '\U0001d5cd', + '\\' : '\U0001d5ce', + '\\' : '\U0001d5cf', + '\\' : '\U0001d5d0', + '\\' : '\U0001d5d1', + '\\' : '\U0001d5d2', + '\\' : '\U0001d5d3', + '\\' : '\U0001d504', + '\\' : '\U0001d505', + '\\' : '\U0000212d', + '\\

' : '\U0001d507', + '\\' : '\U0001d508', + '\\' : '\U0001d509', + '\\' : '\U0001d50a', + '\\' : '\U0000210c', + '\\' : '\U00002111', + '\\' : '\U0001d50d', + '\\' : '\U0001d50e', + '\\' : '\U0001d50f', + '\\' : '\U0001d510', + '\\' : '\U0001d511', + '\\' : '\U0001d512', + '\\' : '\U0001d513', + '\\' : '\U0001d514', + '\\' : '\U0000211c', + '\\' : '\U0001d516', + '\\' : '\U0001d517', + '\\' : '\U0001d518', + '\\' : '\U0001d519', + '\\' : '\U0001d51a', + '\\' : '\U0001d51b', + '\\' : '\U0001d51c', + '\\' : '\U00002128', + '\\' : '\U0001d51e', + '\\' : '\U0001d51f', + '\\' : '\U0001d520', + '\\
' : '\U0001d521', + '\\' : '\U0001d522', + '\\' : '\U0001d523', + '\\' : '\U0001d524', + '\\' : '\U0001d525', + '\\' : '\U0001d526', + '\\' : '\U0001d527', + '\\' : '\U0001d528', + '\\' : '\U0001d529', + '\\' : '\U0001d52a', + '\\' : '\U0001d52b', + '\\' : '\U0001d52c', + '\\' : '\U0001d52d', + '\\' : '\U0001d52e', + '\\' : '\U0001d52f', + '\\' : '\U0001d530', + '\\' : '\U0001d531', + '\\' : '\U0001d532', + '\\' : '\U0001d533', + '\\' : '\U0001d534', + '\\' : '\U0001d535', + '\\' : '\U0001d536', + '\\' : '\U0001d537', + '\\' : '\U000003b1', + '\\' : '\U000003b2', + '\\' : '\U000003b3', + '\\' : '\U000003b4', + '\\' : '\U000003b5', + '\\' : '\U000003b6', + '\\' : '\U000003b7', + '\\' : '\U000003b8', + '\\' : '\U000003b9', + '\\' : '\U000003ba', + '\\' : '\U000003bb', + '\\' : '\U000003bc', + '\\' : '\U000003bd', + '\\' : '\U000003be', + '\\' : '\U000003c0', + '\\' : '\U000003c1', + '\\' : '\U000003c3', + '\\' : '\U000003c4', + '\\' : '\U000003c5', + '\\' : '\U000003c6', + '\\' : '\U000003c7', + '\\' : '\U000003c8', + '\\' : '\U000003c9', + '\\' : '\U00000393', + '\\' : '\U00000394', + '\\' : '\U00000398', + '\\' : '\U0000039b', + '\\' : '\U0000039e', + '\\' : '\U000003a0', + '\\' : '\U000003a3', + '\\' : '\U000003a5', + '\\' : '\U000003a6', + '\\' : '\U000003a8', + '\\' : '\U000003a9', + '\\' : '\U0001d539', + '\\' : '\U00002102', + '\\' : '\U00002115', + '\\' : '\U0000211a', + '\\' : '\U0000211d', + '\\' : '\U00002124', + '\\' : '\U00002190', + '\\' : '\U000027f5', + '\\' : '\U00002192', + '\\' : '\U000027f6', + '\\' : '\U000021d0', + '\\' : '\U000027f8', + '\\' : '\U000021d2', + '\\' : '\U000027f9', + '\\' : '\U00002194', + '\\' : '\U000027f7', + '\\' : '\U000021d4', + '\\' : '\U000027fa', + '\\' : '\U000021a6', + '\\' : '\U000027fc', + '\\' : '\U00002500', + '\\' : '\U00002550', + '\\' : '\U000021a9', + '\\' : '\U000021aa', + '\\' : '\U000021bd', + '\\' : '\U000021c1', + '\\' : '\U000021bc', + '\\' : '\U000021c0', + '\\' : '\U000021cc', + '\\' : '\U0000219d', + '\\' : '\U000021c3', + '\\' : '\U000021c2', + '\\' : '\U000021bf', + '\\' : '\U000021be', + '\\' : '\U000021be', + '\\' : '\U00002237', + '\\' : '\U00002191', + '\\' : '\U000021d1', + '\\' : '\U00002193', + '\\' : '\U000021d3', + '\\' : '\U00002195', + '\\' : '\U000021d5', + '\\' : '\U000027e8', + '\\' : '\U000027e9', + '\\' : '\U00002308', + '\\' : '\U00002309', + '\\' : '\U0000230a', + '\\' : '\U0000230b', + '\\' : '\U00002987', + '\\' : '\U00002988', + '\\' : '\U000027e6', + '\\' : '\U000027e7', + '\\' : '\U00002983', + '\\' : '\U00002984', + '\\' : '\U000000ab', + '\\' : '\U000000bb', + '\\' : '\U000022a5', + '\\' : '\U000022a4', + '\\' : '\U00002227', + '\\' : '\U000022c0', + '\\' : '\U00002228', + '\\' : '\U000022c1', + '\\' : '\U00002200', + '\\' : '\U00002203', + '\\' : '\U00002204', + '\\' : '\U000000ac', + '\\' : '\U000025a1', + '\\' : '\U000025c7', + '\\' : '\U000022a2', + '\\' : '\U000022a8', + '\\' : '\U000022a9', + '\\' : '\U000022ab', + '\\' : '\U000022a3', + '\\' : '\U0000221a', + '\\' : '\U00002264', + '\\' : '\U00002265', + '\\' : '\U0000226a', + '\\' : '\U0000226b', + '\\' : '\U00002272', + '\\' : '\U00002273', + '\\' : '\U00002a85', + '\\' : '\U00002a86', + '\\' : '\U00002208', + '\\' : '\U00002209', + '\\' : '\U00002282', + '\\' : '\U00002283', + '\\' : '\U00002286', + '\\' : '\U00002287', + '\\' : '\U0000228f', + '\\' : '\U00002290', + '\\' : '\U00002291', + '\\' : '\U00002292', + '\\' : '\U00002229', + '\\' : '\U000022c2', + '\\' : '\U0000222a', + '\\' : '\U000022c3', + '\\' : '\U00002294', + '\\' : '\U00002a06', + '\\' : '\U00002293', + '\\' : '\U00002a05', + '\\' : '\U00002216', + '\\' : '\U0000221d', + '\\' : '\U0000228e', + '\\' : '\U00002a04', + '\\' : '\U00002260', + '\\' : '\U0000223c', + '\\' : '\U00002250', + '\\' : '\U00002243', + '\\' : '\U00002248', + '\\' : '\U0000224d', + '\\' : '\U00002245', + '\\' : '\U00002323', + '\\' : '\U00002261', + '\\' : '\U00002322', + '\\' : '\U000022c8', + '\\' : '\U00002a1d', + '\\' : '\U0000227a', + '\\' : '\U0000227b', + '\\' : '\U0000227c', + '\\' : '\U0000227d', + '\\' : '\U00002225', + '\\' : '\U000000a6', + '\\' : '\U000000b1', + '\\' : '\U00002213', + '\\' : '\U000000d7', + '\\
' : '\U000000f7', + '\\' : '\U000022c5', + '\\' : '\U000022c6', + '\\' : '\U00002219', + '\\' : '\U00002218', + '\\' : '\U00002020', + '\\' : '\U00002021', + '\\' : '\U000022b2', + '\\' : '\U000022b3', + '\\' : '\U000022b4', + '\\' : '\U000022b5', + '\\' : '\U000025c3', + '\\' : '\U000025b9', + '\\' : '\U000025b3', + '\\' : '\U0000225c', + '\\' : '\U00002295', + '\\' : '\U00002a01', + '\\' : '\U00002297', + '\\' : '\U00002a02', + '\\' : '\U00002299', + '\\' : '\U00002a00', + '\\' : '\U00002296', + '\\' : '\U00002298', + '\\' : '\U00002026', + '\\' : '\U000022ef', + '\\' : '\U00002211', + '\\' : '\U0000220f', + '\\' : '\U00002210', + '\\' : '\U0000221e', + '\\' : '\U0000222b', + '\\' : '\U0000222e', + '\\' : '\U00002663', + '\\' : '\U00002662', + '\\' : '\U00002661', + '\\' : '\U00002660', + '\\' : '\U00002135', + '\\' : '\U00002205', + '\\' : '\U00002207', + '\\' : '\U00002202', + '\\' : '\U0000266d', + '\\' : '\U0000266e', + '\\' : '\U0000266f', + '\\' : '\U00002220', + '\\' : '\U000000a9', + '\\' : '\U000000ae', + '\\' : '\U000000ad', + '\\' : '\U000000af', + '\\' : '\U000000bc', + '\\' : '\U000000bd', + '\\' : '\U000000be', + '\\' : '\U000000aa', + '\\' : '\U000000ba', + '\\
' : '\U000000a7', + '\\' : '\U000000b6', + '\\' : '\U000000a1', + '\\' : '\U000000bf', + '\\' : '\U000020ac', + '\\' : '\U000000a3', + '\\' : '\U000000a5', + '\\' : '\U000000a2', + '\\' : '\U000000a4', + '\\' : '\U000000b0', + '\\' : '\U00002a3f', + '\\' : '\U00002127', + '\\' : '\U000025ca', + '\\' : '\U00002118', + '\\' : '\U00002240', + '\\' : '\U000022c4', + '\\' : '\U000000b4', + '\\' : '\U00000131', + '\\' : '\U000000a8', + '\\' : '\U000000b8', + '\\' : '\U000002dd', + '\\' : '\U000003f5', + '\\' : '\U000023ce', + '\\' : '\U00002039', + '\\' : '\U0000203a', + '\\' : '\U00002302', + '\\<^sub>' : '\U000021e9', + '\\<^sup>' : '\U000021e7', + '\\<^bold>' : '\U00002759', + '\\<^bsub>' : '\U000021d8', + '\\<^esub>' : '\U000021d9', + '\\<^bsup>' : '\U000021d7', + '\\<^esup>' : '\U000021d6', + } + + lang_map = {'isabelle' : isabelle_symbols, 'latex' : latex_symbols} + + def __init__(self, **options): + Filter.__init__(self, **options) + lang = get_choice_opt(options, 'lang', + ['isabelle', 'latex'], 'isabelle') + self.symbols = self.lang_map[lang] + + def filter(self, lexer, stream): + for ttype, value in stream: + if value in self.symbols: + yield ttype, self.symbols[value] + else: + yield ttype, value + + +class KeywordCaseFilter(Filter): + """Convert keywords to lowercase or uppercase or capitalize them, which + means first letter uppercase, rest lowercase. + + This can be useful e.g. if you highlight Pascal code and want to adapt the + code to your styleguide. + + Options accepted: + + `case` : string + The casing to convert keywords to. Must be one of ``'lower'``, + ``'upper'`` or ``'capitalize'``. The default is ``'lower'``. + """ + + def __init__(self, **options): + Filter.__init__(self, **options) + case = get_choice_opt(options, 'case', + ['lower', 'upper', 'capitalize'], 'lower') + self.convert = getattr(str, case) + + def filter(self, lexer, stream): + for ttype, value in stream: + if ttype in Keyword: + yield ttype, self.convert(value) + else: + yield ttype, value + + +class NameHighlightFilter(Filter): + """Highlight a normal Name (and Name.*) token with a different token type. + + Example:: + + filter = NameHighlightFilter( + names=['foo', 'bar', 'baz'], + tokentype=Name.Function, + ) + + This would highlight the names "foo", "bar" and "baz" + as functions. `Name.Function` is the default token type. + + Options accepted: + + `names` : list of strings + A list of names that should be given the different token type. + There is no default. + `tokentype` : TokenType or string + A token type or a string containing a token type name that is + used for highlighting the strings in `names`. The default is + `Name.Function`. + """ + + def __init__(self, **options): + Filter.__init__(self, **options) + self.names = set(get_list_opt(options, 'names', [])) + tokentype = options.get('tokentype') + if tokentype: + self.tokentype = string_to_tokentype(tokentype) + else: + self.tokentype = Name.Function + + def filter(self, lexer, stream): + for ttype, value in stream: + if ttype in Name and value in self.names: + yield self.tokentype, value + else: + yield ttype, value + + +class ErrorToken(Exception): + pass + + +class RaiseOnErrorTokenFilter(Filter): + """Raise an exception when the lexer generates an error token. + + Options accepted: + + `excclass` : Exception class + The exception class to raise. + The default is `pygments.filters.ErrorToken`. + + .. versionadded:: 0.8 + """ + + def __init__(self, **options): + Filter.__init__(self, **options) + self.exception = options.get('excclass', ErrorToken) + try: + # issubclass() will raise TypeError if first argument is not a class + if not issubclass(self.exception, Exception): + raise TypeError + except TypeError: + raise OptionError('excclass option is not an exception class') + + def filter(self, lexer, stream): + for ttype, value in stream: + if ttype is Error: + raise self.exception(value) + yield ttype, value + + +class VisibleWhitespaceFilter(Filter): + """Convert tabs, newlines and/or spaces to visible characters. + + Options accepted: + + `spaces` : string or bool + If this is a one-character string, spaces will be replaces by this string. + If it is another true value, spaces will be replaced by ``·`` (unicode + MIDDLE DOT). If it is a false value, spaces will not be replaced. The + default is ``False``. + `tabs` : string or bool + The same as for `spaces`, but the default replacement character is ``»`` + (unicode RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK). The default value + is ``False``. Note: this will not work if the `tabsize` option for the + lexer is nonzero, as tabs will already have been expanded then. + `tabsize` : int + If tabs are to be replaced by this filter (see the `tabs` option), this + is the total number of characters that a tab should be expanded to. + The default is ``8``. + `newlines` : string or bool + The same as for `spaces`, but the default replacement character is ``¶`` + (unicode PILCROW SIGN). The default value is ``False``. + `wstokentype` : bool + If true, give whitespace the special `Whitespace` token type. This allows + styling the visible whitespace differently (e.g. greyed out), but it can + disrupt background colors. The default is ``True``. + + .. versionadded:: 0.8 + """ + + def __init__(self, **options): + Filter.__init__(self, **options) + for name, default in [('spaces', '·'), + ('tabs', '»'), + ('newlines', '¶')]: + opt = options.get(name, False) + if isinstance(opt, str) and len(opt) == 1: + setattr(self, name, opt) + else: + setattr(self, name, (opt and default or '')) + tabsize = get_int_opt(options, 'tabsize', 8) + if self.tabs: + self.tabs += ' ' * (tabsize - 1) + if self.newlines: + self.newlines += '\n' + self.wstt = get_bool_opt(options, 'wstokentype', True) + + def filter(self, lexer, stream): + if self.wstt: + spaces = self.spaces or ' ' + tabs = self.tabs or '\t' + newlines = self.newlines or '\n' + regex = re.compile(r'\s') + + def replacefunc(wschar): + if wschar == ' ': + return spaces + elif wschar == '\t': + return tabs + elif wschar == '\n': + return newlines + return wschar + + for ttype, value in stream: + yield from _replace_special(ttype, value, regex, Whitespace, + replacefunc) + else: + spaces, tabs, newlines = self.spaces, self.tabs, self.newlines + # simpler processing + for ttype, value in stream: + if spaces: + value = value.replace(' ', spaces) + if tabs: + value = value.replace('\t', tabs) + if newlines: + value = value.replace('\n', newlines) + yield ttype, value + + +class GobbleFilter(Filter): + """Gobbles source code lines (eats initial characters). + + This filter drops the first ``n`` characters off every line of code. This + may be useful when the source code fed to the lexer is indented by a fixed + amount of space that isn't desired in the output. + + Options accepted: + + `n` : int + The number of characters to gobble. + + .. versionadded:: 1.2 + """ + def __init__(self, **options): + Filter.__init__(self, **options) + self.n = get_int_opt(options, 'n', 0) + + def gobble(self, value, left): + if left < len(value): + return value[left:], 0 + else: + return '', left - len(value) + + def filter(self, lexer, stream): + n = self.n + left = n # How many characters left to gobble. + for ttype, value in stream: + # Remove ``left`` tokens from first line, ``n`` from all others. + parts = value.split('\n') + (parts[0], left) = self.gobble(parts[0], left) + for i in range(1, len(parts)): + (parts[i], left) = self.gobble(parts[i], n) + value = '\n'.join(parts) + + if value != '': + yield ttype, value + + +class TokenMergeFilter(Filter): + """Merges consecutive tokens with the same token type in the output + stream of a lexer. + + .. versionadded:: 1.2 + """ + def __init__(self, **options): + Filter.__init__(self, **options) + + def filter(self, lexer, stream): + current_type = None + current_value = None + for ttype, value in stream: + if ttype is current_type: + current_value += value + else: + if current_type is not None: + yield current_type, current_value + current_type = ttype + current_value = value + if current_type is not None: + yield current_type, current_value + + +FILTERS = { + 'codetagify': CodeTagFilter, + 'keywordcase': KeywordCaseFilter, + 'highlight': NameHighlightFilter, + 'raiseonerror': RaiseOnErrorTokenFilter, + 'whitespace': VisibleWhitespaceFilter, + 'gobble': GobbleFilter, + 'tokenmerge': TokenMergeFilter, + 'symbols': SymbolFilter, +} diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/filters/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/filters/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a6864bf16a9c611db5de507d69713e8cacaaf1c0 GIT binary patch literal 37931 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�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, + the `arg` is then given by the ``-a`` option. + """ + return '' + + def format(self, tokensource, outfile): + """ + This method must format the tokens from the `tokensource` iterable and + write the formatted version to the file object `outfile`. + + Formatter options can control how exactly the tokens are converted. + """ + if self.encoding: + # wrap the outfile in a StreamWriter + outfile = codecs.lookup(self.encoding)[3](outfile) + return self.format_unencoded(tokensource, outfile) + + # Allow writing Formatter[str] or Formatter[bytes]. That's equivalent to + # Formatter. This helps when using third-party type stubs from typeshed. + def __class_getitem__(cls, name): + return cls diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__init__.py b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__init__.py new file mode 100644 index 00000000..b24931cd --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__init__.py @@ -0,0 +1,157 @@ +""" + pygments.formatters + ~~~~~~~~~~~~~~~~~~~ + + Pygments formatters. + + :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import re +import sys +import types +import fnmatch +from os.path import basename + +from pygments.formatters._mapping import FORMATTERS +from pygments.plugin import find_plugin_formatters +from pygments.util import ClassNotFound + +__all__ = ['get_formatter_by_name', 'get_formatter_for_filename', + 'get_all_formatters', 'load_formatter_from_file'] + list(FORMATTERS) + +_formatter_cache = {} # classes by name +_pattern_cache = {} + + +def _fn_matches(fn, glob): + """Return whether the supplied file name fn matches pattern filename.""" + if glob not in _pattern_cache: + pattern = _pattern_cache[glob] = re.compile(fnmatch.translate(glob)) + return pattern.match(fn) + return _pattern_cache[glob].match(fn) + + +def _load_formatters(module_name): + """Load a formatter (and all others in the module too).""" + mod = __import__(module_name, None, None, ['__all__']) + for formatter_name in mod.__all__: + cls = getattr(mod, formatter_name) + _formatter_cache[cls.name] = cls + + +def get_all_formatters(): + """Return a generator for all formatter classes.""" + # NB: this returns formatter classes, not info like get_all_lexers(). + for info in FORMATTERS.values(): + if info[1] not in _formatter_cache: + _load_formatters(info[0]) + yield _formatter_cache[info[1]] + for _, formatter in find_plugin_formatters(): + yield formatter + + +def find_formatter_class(alias): + """Lookup a formatter by alias. + + Returns None if not found. + """ + for module_name, name, aliases, _, _ in FORMATTERS.values(): + if alias in aliases: + if name not in _formatter_cache: + _load_formatters(module_name) + return _formatter_cache[name] + for _, cls in find_plugin_formatters(): + if alias in cls.aliases: + return cls + + +def get_formatter_by_name(_alias, **options): + """ + Return an instance of a :class:`.Formatter` subclass that has `alias` in its + aliases list. The formatter is given the `options` at its instantiation. + + Will raise :exc:`pygments.util.ClassNotFound` if no formatter with that + alias is found. + """ + cls = find_formatter_class(_alias) + if cls is None: + raise ClassNotFound(f"no formatter found for name {_alias!r}") + return cls(**options) + + +def load_formatter_from_file(filename, formattername="CustomFormatter", **options): + """ + Return a `Formatter` subclass instance loaded from the provided file, relative + to the current directory. + + The file is expected to contain a Formatter class named ``formattername`` + (by default, CustomFormatter). Users should be very careful with the input, because + this method is equivalent to running ``eval()`` on the input file. The formatter is + given the `options` at its instantiation. + + :exc:`pygments.util.ClassNotFound` is raised if there are any errors loading + the formatter. + + .. versionadded:: 2.2 + """ + try: + # This empty dict will contain the namespace for the exec'd file + custom_namespace = {} + with open(filename, 'rb') as f: + exec(f.read(), custom_namespace) + # Retrieve the class `formattername` from that namespace + if formattername not in custom_namespace: + raise ClassNotFound(f'no valid {formattername} class found in {filename}') + formatter_class = custom_namespace[formattername] + # And finally instantiate it with the options + return formatter_class(**options) + except OSError as err: + raise ClassNotFound(f'cannot read {filename}: {err}') + except ClassNotFound: + raise + except Exception as err: + raise ClassNotFound(f'error when loading custom formatter: {err}') + + +def get_formatter_for_filename(fn, **options): + """ + Return a :class:`.Formatter` subclass instance that has a filename pattern + matching `fn`. The formatter is given the `options` at its instantiation. + + Will raise :exc:`pygments.util.ClassNotFound` if no formatter for that filename + is found. + """ + fn = basename(fn) + for modname, name, _, filenames, _ in FORMATTERS.values(): + for filename in filenames: + if _fn_matches(fn, filename): + if name not in _formatter_cache: + _load_formatters(modname) + return _formatter_cache[name](**options) + for _name, cls in find_plugin_formatters(): + for filename in cls.filenames: + if _fn_matches(fn, filename): + return cls(**options) + raise ClassNotFound(f"no formatter found for file name {fn!r}") + + +class _automodule(types.ModuleType): + """Automatically import formatters.""" + + def __getattr__(self, name): + info = FORMATTERS.get(name) + if info: + _load_formatters(info[0]) + cls = _formatter_cache[info[1]] + setattr(self, name, cls) + return cls + raise AttributeError(name) + + +oldmod = sys.modules[__name__] +newmod = _automodule(__name__) +newmod.__dict__.update(oldmod.__dict__) +sys.modules[__name__] = newmod +del newmod.newmod, newmod.oldmod, newmod.sys, newmod.types diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/__init__.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..501787572e6f050e0b26fb04d233946a1ce3b548 GIT binary patch literal 6917 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 Bw&VZ+ literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/_mapping.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/_mapping.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef792eb3f4b878d0c1b8d5f22484cd91f8e20e61 GIT binary patch literal 4215 zcmd5<&2JmW6(_l*EbGfwEL*Z{$74HA!@wrvwDBnj8Y$L?At{2yd?1%_xI0`?uI{2W*j>PWio^C9#Tm3Q(g0G&^tRy*Kau=6wu* zSX`Vd;qO21J(P#nOQoNP82>r>#_QfP#y7kh7+1WT7;ky27}vbp7}vc!81H)bFuv*C$H=?~7$16%WchF9 zuS&zW>W`Ca8ylO-6I)96xiLc5YoqmO==Viv;zoD88aFx}7i`IlWv^F~nS7HNJk z`m0sIj5-lv%+93gv5gH9i5sld6R}{qG8y_TTW4GgHi(7CIz!gy;gEF(K_H9_Sx0g0 z#ZOqQ*ia3a%R|_X*j^&E3%D$gm!SHpbAHh)vA9yQGpGWobsP=9H3!}45Ew~YkGrPki>1JK6`nhqL zm*l*R$<3GyVTVK0Qz0Wm$hPwclW%t*9=vjo2 zwb_^5dO2~j57~;z$V_bA-rag_hAwSu)$Q82uz_`Z8rnW_CAI8RcQmr3l+;YNSfx?V zU}dy5CB8BdCSsRIgsx(4kB7JeKqfWN3Y2C%^bmNVVX+woh=Ro((+`R^txdD32d1RD z)7swymQ|wamC?+*4Vn?C_>O+OY!Qzc@v}d$cKgFP;^9Z_HZ$Bu_-_oEC%SwPm?v2b zxUdK5#Sj-FbOS{xfS~6j8EKIVK0)lpBIvR+&7-Km;>pvTg}u`mz%!A#%-W$M);3d- zkt(cbc}F^}!0%$Vd3?;=fX8u`Q5^)9FqC{0V{JE%v%Rr=BU#uz+AKmfJz2DyIAEki zk=oU(xGj5SZ(jx|D+yrCFD4d$8o+xICri_#*+B*(9@Z%yoZbEH#EC-R<{u`7m_9t% zer++m{4(;Vo0}DkZz7*?z$dAG6N%hhkfXsQ1z#S%czyOR?V+Na7cX&Y4hF=P)b@Bw zdP|b$z;>35Z6qP`5v83)+&w|L zjFFHBDEb8hFQyu(5Tbf?wI$9CQ(-WKkyRLFl4=O?+{kk?diLqSM92w>Xxe9k&=v(R zYZD4v)M&c4ub~1rl9fXg4Ykkp$sj7&F}gpkFd(me)z5fG3z;CZl7w2ename>rq(VX zSCq^F+%;KJ!f>=d!L$fHv@MUy8oJ-{=iBIDNBpcs{k34Gew(V}9Cq^7OYx`qSu$fg zv4sW;qd!`yoQ=T>&ajS?P;+9`BTiL=pusgMP2DL|0ScY2kE>cZGMCEv)Ow&zH*t8sQ0C-Rjj(^oufMY zLTQh_lhXjf@u_e9upMnG1o5e#IJhx_0CNigCZnHTfxuoZ^HH~Z(uIc-O^W>*4M2m} z415>okLTf^@y6pHuUn05f4JM#r<~{bU>+G3dz zUo`#jy!nZUPmGG1JIaXQ>4s7#C>PBQv~)DB=BWrzn}O^!qwJ^7?;FqFZ^qJyr{kWF z4g2%5*&WyWxcN1H@I=&)qv6HuhkfM@0`ZZ4iIdCtj~`=9opQOH&fZw6{JVNHEtS?r zPU?XAvAPOs{VOM}fl6mewKbv-ob(FOv!xnwulkkrD$#SL+6w4f-&WIkq8CcF8~<7{ z=^_zJIdjWNuMvGcSN6G+E))GmzHch&4Wd_aWyh8DCegP_wL4IL@4S+(61|qo_bTaa zqStX4_HVYT=^di)=CrA%_lSNom*1(S_lah?{3q4)0nrajwFi)Y^jR%^MD$zv{>#dL NAA|n$oZbOkzW_}kuz3Ig literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/bbcode.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/bbcode.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b88976d70308bf5c9b816d8ea3bdbef245b96a2e GIT binary patch literal 4208 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 z zIiv5>o0}dTHMGL#Yx*p6^`lZNoI)D{QFQsEI-|LQvMIar>V~;AVlG`NxMyyBXfT+c zr-GU`>FtsAq~2Wr=yih`Y3&8(nn!Ip=HkcIjb{0HiE7$;sgpX%`4`kle+mb?;rvdv z&ClSOV#fOM{OP%Ix@+CyHaU&6#+Z^a4k*M8Isi^1PP>%;3&kuZr-_{HyrA ztUVmid!>D79RBP4{|*kcOnUu8gH>;OTnJ;rpX)MzuFLwl&i>cBlK)HB@;AEYBD&}P zM%Ne7^=+D9!u@lT11rb2if~rNm5hg0o!Poz{GFo<{b2{2aS!vs{-^@~|SuUb0zkDK|Z=D6pHZ zWNo(Plp{*`?ZdKf3pc?q_qGTK%WQo(mMmxuDmDAVD*np2eWiP P$;vobX#7cm9`64K2DK(9 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/img.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/img.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79b661bef28039f4eaba7598d3bb115698704a92 GIT binary patch literal 28689 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/irc.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/irc.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d80740d5b6a2d6b3f22757ac679f91d1cc3ae394 GIT binary patch literal 6029 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 XhMoBPasT{3uO+`V literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/latex.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/latex.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6cf2d3adb82d8ed296e45afec85f5ecea6861122 GIT binary patch literal 20123 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�ilx!7q&Dn!6){EUD8n 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 literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/other.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/other.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f7ee4b82d977ba4d603d69d3306a92fa1b25206 GIT binary patch literal 6851 zcmbt3TTC3;mA9&^x~pH%V1}oS%QhZp8#fP+Ga3BY7}NH6JjRL5thAlXHbqy_G%l*x zs=}C76EQ1Gfzf1*wHkqyL}rsn%g7&#@|Djd%HRHgvE`nMNAm3MPxgy3p4F_s_MBT) zP1|mEHrgw3`qsJUo_p`P=Xv?pXf#Bi{YUq~Tz(HB|BfGCqsXb971AU zQ)g@r=h>VF*OTXuB&?fO#?>IYJ3m1D-d+ZW&1`X<5$znnQ#gkklL~vuOuWaELOECSdm&F`D7W zXheX@NdfpfVzvReMZRKLUfA-JNWC_LQ(Qka=J!6 zglU19Ygva>G%W-C4EPJmWe6OQb6sN26o-c?JJg?3LudQXAhQl<1q)fkf20i$Cq$eC zDu>pZku*`2z@buZBZ`>OwHu0Ifz?_%Y>7^G*Q9n#?R(hsFYhZi6=S#)oS2z0aw9M+ zry&L9)(02XH>@IwsOC&dJVpEFnPGSHSHS2+Yg!k-RYa9M0$y zC=qj*@xSrf#^>?j+avb6m12_EFfF({gRQn`<#m-X}3pU{y47q zN|Sk++;_PFU)&dM5CtM$f9F^BPl4sjfp4e$s4+;aYH#K)+&FJ#20Z)>IYg|OH=_+4 z4nr*TFto`+Wgb6c<=`($k>&51X>!x|H)NWLhX!q4GBddx26RVniFo9s`P9~}k5*XijaA(x(fdTmCzyj-;%*QOO=M9$q&T@dK zseTy}y0g4?ahLT4y8c1BzJJzV@Q;@aVuyzu@XqmZXg)O0j)Fqp5@tEKAA&xwHU2Mq zDw)gTIwOu|H zg((4TmDec*J;V0S2C3dDoCzbPTl*x*5v_sl2=+?TO4t!nmG7#IzK$a>%HMa_MyCNT zqvQ&~WkkSbl=Mkvy}4y>Au>qwko<8B*5H=Cc;N(l3{RS($e1;ZLGpb2>#oE(Z{n1yXC&1;lc4Ez8mD8P_YGvBZdy71 zxV2}awP&quv-RyqvF}ppWq_pA(DLY6W$4heGL`IgfmAxHcb7XStt=Cd?o~db zc$D6Y9UOubk@|Nh5^&jkZqgPm06dR{2%k}PQ=$4|NBdK~=iJBySBE=wf(9y&%{yE$ zfW*TzR2V2^dP3tg_NXGzS*U&nnyb+Oz~ zSKQP1^W?otcQ0-1Y5%;g>E|6QZ#_PAeB;pZwV}=CzDIQ@zpQIoZu|7oS7BhVRYeYT zV!(RKVfA_Jjm7ILAFL)<1~=*^Ta(=+P}7pkGia->Mm?C4a1aW1!tP2nhm~bH-i-`w+Au*pEO&fI4PJr?eatvx<{Q z8yNFxsbw6*-cGfJ$}R;mNxg@l(msQUae zPv1%W@qmHuH~#_MgOetC{u~Rx$w@<>G;&ae(Rxn;*AD*PeXnuT`qxwX-b6R$XA9ob zjxBhTUUTwjVB0^YJN`>@_{s^xM=h1{GIRZH(1Gwyu3sM7FmsOk24C>-*m3N;`6IOY z8-yem-i2h5BAo(IRrP78B)#&>OvE{w$U3Trsfp2x;5dK^>8K}-*VECDal&5#sF;pI z8i^ibJ>0fh_b>ZC+xOe{2l2B{YWJ@1ySQ2V-g@M{Egw13&%n47TQGci4X+o!w^?xN z@PNw9`m*r&1b;lu8@k=R!9N=)aFwD?p_E-gmdID6@coR)Y{3uER|r5k!TrL2-&ID) zT6(#%3`4H`u*G@wqYTZXgk9)(k8U<-p<}0QHUyWB#`#K{YlNpURIgWBK_~?CfGdO$ zdK?*|4ev%j9({!ce9EOW?r_*;e(?V}KlB&OMjZRp(DYsqwf^8A&nNt+wjbDCXMLZ3Z9l)UN`a5SLS zzhTpRA>mN}dxq_!jc?#c#pbEuT4>uH-HeVi! zU$-nLJG%{gF~0-=*;sw+e7IP%XWs7sd>Lee=NRW{uo$VOA^Pyci0JiyxO{oFX7$8s z`&#WHp^o|8!yX{I9O8A3Cv7bAs|-YPi#|T>p>z z=lxG&t@C`zc!95z2E4iQox5GD$&KcoN0HvA9`fF?yT?`oo6X%1BRwU`O31_aI`4L_ zOm8-KJ&bf0h3ebUh3HS6d#)BNUkk0AnNvF)~CV^%x_Y=GD0i z&p01{iP;X-@QqGT#3_UEY8s#ITJQCC=iebtp&u;`Oj$XV7W#Ho2}dKGL6f#v2?@`A zrVbSYnz;>ctdgq2I{|$-rSbfIz#D@I=+<8j`7eZ1&TH)%BLyVrjLfS5AT42-uULWM zzOExo{0q|Y1rfg>hyN{*)_1LT^sztE{Me(&}Cxw$!vVEp4;pY}T$p}*6Q`jBdi-Cx3D9jPdVR8Hk}Zk|u^98!fW zmlAkHL{)ss`Le3Sz5|W#pvI>pBF%8Be4k4Ncytx1q5H6x@J|JQPbydl$C`@&3}YC% zLUx`Qjvb#dt$D?9h-Jrfijg(v6>Bb^V>0*t_XsngEA{;tG~t_61@?7dGww? ztZNx!*kl-Ani})@pIpB@eq(B!*;k39Xu2H_|BIeE#(ANJe_oV1H_6V|puXn%i&DQb zpBs72jTXgz-5JU7e!SRBV@IFew_#dGF1l6A0OQssmwQ^HPJzuL9=9_}j^KW*7%Cp! z6ByrkociThiec-DLt?xqI^>SyNscnD6PsGc__dyxHRnkpYurhU5qr)tbBSxFL-ezk zOmj{%vWZJdW{wzYVu2V730<2`Qb9EUvs?(qWJ`oC#koC@!j1{&v$0~= zP~`^?XH1pE@pT7h6oW#>z|#a<5D8)t6*fW8@ziHmYp{#Q@d995GYv&mi8?%t2jdr+ zm14vPtFP9RSVX+0J%P#WO?3eLYl3I@3{cmR)8O5zpBk9;;i4H%LoW9P_gjubutMpE z6hMgT+Be3~FJ=+?IY=$?i-O86ittD-#H|Y?tzH+RcPq4S6MWVs>HsV@g9d06u2cjr z(;zVoVbBn$af5rfa)TSHal?DKO$~0O#%*qJ!Nv>^r=gkKH(Y)cC@a7!pxB`s9`dA& zshd_YI%-3J@l2L|e$aRF?qXlE7#vMda?%s0O3C34JFY{zmSu$lc{1 zR^)+=_crB$t&Yx;xYD%Miyu!sns|j;Lh{+s?mtyfm1YRB=7lWXRxKqWtWun3anBWs5!qql6Y=|1D9 z4}N_x<7b;KCl28BoqE!~(OVjO`0?_`Woa|=*3NNo&H^N%42^1i(0hvY!Y^0}06+y5 zz~=G?^+c7gLz06+0C)o)8N8*%fGYuP8U;~TkI({V^|;bOfC3=@_dVPS>~h7qcOgwJ zuHXdg8r{bI$iOSCTy_O3cCc+%q!3^p1{dXn&*TPOftHmk)A`N$4zjo?xYDAy;)+nB z{|}o7d#DO;yn@mbZ&+>MDHN0AB#Y8$0$wrElWtpZL0Fx@@&Y*pny15heJo`4P+F7U zdd<9*Ay6rKXGYUup)0ntpy-}V7fvBZpm_WjQ1iqKvr?NTSOJ)eg+nK5 zh1GVn>^#2v=x(L$bhY!?-xXC0dp@TX41Y=*ynOGwDUoP^EQf zE%>+g?&?6|k0W1>JiAjF_^=XvcXeVzu10&-C)Xx7PCvO_=^Og0v(op`%jl)=y5mps zFS;+R2CF@IP2N5N4(xQINPG3Fn z2)$CVq3MNa!YIjReajkvn0<+uYQLM&p3Sb@UK&!`xbCAJUX3GdO*-F43udV)T@#Z(u@5A7jqU+$8U|OozoUZ*|us<9I zHD=s8Pu-w>hI#c`9`(Wx#Q!w|4A=}%c>Wi z2T-`niQFV7@}eoq&zmMqJeAE+^Sou!!pfGYb-rq{ij}R=>UrCwZQed==Y()1Dk(m#>+w*0VNs6E%&NzQGlilsIy`)2 zaCCU|urR#{2$Jx&PKuyP!TA9}ktE^Nk1oId*1MN{4CC==BqYTY>9}zE(wNr%=7lq7 zCoY{OQUp;_gOMn3eofQt=kv91$)Y4n;#ef4;?;hxFhji9{K1$KnGY`XTk?(>N%c?1 z<57QnLCx3FTO_8k7uFPwDB7D-Nyu`126J2pM&!KXT{UdL3~9X8?k4@^`9~<+bEQuSHD{(!3!?iIgasCqgznpSHMvrsIA$2P`;8_@Clb^ zBTC8Y1`}!=q7exNqtQhno=_JOD%um#D6KRDC&ohwCPu=wh&n5T;!!L}B!*?7f$^9S zjEO=Vi!2KXMZ!=)Q4`^?Pk39B!%|3P3rM0t9T~*8gNiBvgc=tvMnZC2iHFsF@AeBn zj>{rF4UD>sy&aW;iX_D17{$af5RhV_xEP7e1Og0wAOO-!>af_L43n3EXC@Yhq-b(> z8M4Skm{Bi)7pE?rxo|;(7} zyj_ck>TD@}N@S^ozDsx$;$fy>7>PuzPhiyqbU_M5!V$oU!~iuK78c?VI)yfYWKgAZ zf~Xih#JUY?-3El2$W>x0xJ!seMj6pMIkX<%UEERMu)cKBNKA^wm6F9v$T0!Cg&Krd zVt!g;D`uP*n>nqTvH)RIAinA~Ns6&W+!u)vvHjafW*Z#p#Q>qi6LJWfl6GbZ zk%7_QnRSX9l+_|_w7*K2#UvewA&j!3YAvPsM*bi0Nee}E7r+BaC6N#60wKhN6T-*< z#01J%g5p>dVo>T{WY55E1ch1Yx&Y0Bt*#CT zl5fTbDeXHf(LbmT2m{FhT_yXStt3-Zeabe{b;}VJA}Z~+GUx^KS(3RhI%Ck{F@vLO`U1GWbCI}Q^Eg^oa12G@^a~A$flzbRT93X>P!f>b&cm(A8 zi11|$A~oSH^5md!RCtN8q)do38zf6nW?%>(NmL{TJ6q)5zKBoqX=4+R33?hvp0eDtWs|62Ti{u?v7kgCx(S8y(=mwqL=FdyNP{GaiE z$n$v46=I1(2RudoD;>slUZHrxl;r$?tz9Y{L+wf;&Jc}Q0euWusLfSE;+;aP_AD7^E3|AIXA|uueGJOfri#65#M(md!VEzX@s3?PjB->QDUMS58g;McZ3T=kNTYaHT?JbnB zdafC+z>hKRgYz7hGTAw`&*)()a47`|m}@Q67oT!Nl3JRJHHFd=R#U89!YdoQr%+#f z&h;0|g;LVM$aA+ZSay<6_cp#6*g?ra0aEd)oY{eIh^oskvHr?Fj+0&LaG^zPXh9Ji zG_b^WT89eoO2>hUg9WI?XCZh)PlLaUjoqBOztE?Llg&K$K6g8Fjk|7spS#BQdxC$2 z4QgndQ$OhXlM)F8$nRnl$5))(bwd;hzJ_RCpc^mmD*UTp>Oris;f6Ye)+U5qm`D;r>(hAP^`F zrf>}**`9Oc-VZKBRJi{N9APadp}`TD!>7RbYfa4SR`&RW*W(DVG_Qz4xClic6qsrV z)!7I_vF)>i9aD%`!D)ntf=0j3S7I;owgQXuoK_~US*9(Xuhvb$xx7m&8IE?oPOs3t z+pk&w+7i?8HL2r#O`%3w$k%PRV0jzfnGIwm>Zs%$I#{i~IugUWMAW>C{2SqqaLKm^JX}m`KsW;f)o?;HSYzZ3F#~{CbCSLFJDij`?at}o0m+sCEw%^ zAvjbetJw>jd5%Pj~NM zJNoF*+1!DlU%mR}tIy56aD3UaVQc?QXa8E?ql0H2zO&wWo@&~jwsrpU#@f;Q7axwS z?>@WUc8+SjPg^^Fd1bBd{wojb*E`Rww~hg->uD#0I`&Xn?|hRD-YgJz-?Z*MNR;GU zji01GPUYH#&&KbKXWNft+K()c-M+Z#ZDoWu?@1qB8-HM0Z+`h1S8Z=vKDTA6ayC6} zXuBNx%2R zJ+M^^AX^PwN6%+(+{ki)E&r3FMh zQol(iyOOu4(sfm#k%Ax(QcbmAOF1ohwhOFQ`zg98)bA!oYn{B6TpdgAU3c~Wy{GNY z`PDP&rgcwWX?e?PvhRD20o*pOkUC*p34j~K6@_2qVHhj$CTTdKDSn2W2p%qq0(Zi! z5(IW7&0pywQrV_nkYRREJwfhUcX2~*zsIDwV&dbPd%;oti1D!=H=5lSN+Q6 zk8fWm^5E*vK0Wfpy(ibuygIs3-tT0Q-X4&bO~ z3gS`Qfj^&raC+T$GShbQiTky!U4&$-m2)?Ia_!c&73B|-Syy+))tw$&>sxn?KC+F< zhe7PN2#hQm6YC`uAl6tsvV{;x@+Pf#H;Y^=m*PZAF*>7OqO}$0Erli9)@Bu}cWkQ` zZKZ8tGl>rrfPI4b8ZY>6c^B|6HBTW^-78Mb$ViFdISnh@E50mKbo4b74N_BIlWFhg zs#W8D*fyc8(}?C)4sfwUQ&zySiaag70nLff1!p%T1kePpp@;rv^cdfU{T&OpX!;8?TzkvX z-_)>8gxjTsC~{l+!Al6vdl3E^5-Kt28aXtj=LqcdV;*UFO0H=`3@}m_+7Fkbihwd@g{!ujCTOE_0xUcZr4HUQBeg+{T|9mA&45ueu%292KbOPKk@*Z&6> ztdCWpiS$=#wx?E`5X-vet*W}XAmy!B$pOq;8HT)tJxD4wPG}aY#m~S%2N+C&RD#gt#Wl=R00>%_C z@+`D2m46JPOZGsz$ga}%F1udAdlKi1ORyhoJn^~gGRBi)E4#ikzCzjtI|FV8+i_-X zzpx-$-vihS7J3hx(GPJ1c90djq$+7m@>k&z+^%KRTeoAn-E84*4^T<~iq5)J4Drse zhyXIJM*}3FEc{^UW)>N+Beh6Ol&%lO!(qyjM`DJoQhJgCK-N=GgVWdtMkCXsc`Iwn zS5MDqg{o;-NJ)l7(-LV>Q9G4bd;{~($$N0qC0?dPN4`qKoagocuBNV%B~&ZRFHM_tsv!^`9@m> z>0ScHLfY&SO#2QL{QtwH&mMAbnOO4NxG@!0Zlr{%P#E`K6jP}eys%VAH^vw zc+Xd9(J7Ne7Gbma{Y#!{S6sO*O^DQGvA$_5mEgwwu2BU@jJzum8W=}mpio-{|<6ofh}JF7hUA@=tl(1 z1ufLGg9W`0>!{xl&FZ!2!Od0JA#)ULlwAdj2W!z=;>T5tr~&nRqoyMM+kGs(T!~6+ zvDXf~tSafj%^61lMZ)FI9Y)eYM}_{|Iy42zZO?bQFc*Lb7x!ydeKcY}rP?VmhSr!eElwj=#9|;-PB)4gH7jRDa%^6a&rt=X4f`!xuy~Ow$mGdC z!c8H+gnT>Av#VuM#jF&BOy13S~v8}X9M>J(&`tf&r|EIhrcoN2aZtNE4gDY!!9+y`YmU+pMYJm*Aa}B zpKjQCa^Ci=cYnsaKkFUNc!$@$qZna73OjgqWhCdQ|K!bEZ?1Z>Ext^PZ!NLja`=hk z2;4pUQJs*LOUOSVuGB&PWW&~#)2*#O*qa{v;^OBQGc6+GCW)wB1e0 zs&E;*`_HjgD6p)gy09;hgGFRxnkyZv!=QMF_GwXeZhIm5T*cj9l5;_i3j0(3sc1@B zpk?5z0q>B$AFEQ;No%FOo2*J!hmD*Cc71iF{aK8z;Qbw-Sc!S3mfIs1u6UHFlB}^S zX#sTJciDxgka9|u zc+1Vfe=WZXI5FEA`Qsh<_#wkqX%3(sa{y3`G!C7LI7n0IiG{%(VW7Llhg59|*J=xt z&5>OzyMAtHmy{z`FIs=7&yE~vEdz|#q@zr~RXpIFql9_cl{7T1@>;TLPAF@}Dj$tnD?^j{+lO|5*IkAHuSKd;PVAMsB#woN<#JE5uF@_M$tv_zfTNG2 zv1J@|ini~CNU~fRA{$o`Vhw{>zXOei^gyniqT_q}It!A<`qq7KU)Q$2Zq|1vKGp9y zD>jbCZs!x^xUT|V>k2qiuB1z*{BDKaEjA=waQ+(q#K=I z>)$oOazHE}$%5$1%*&A91(V=Ugr<8%<_}Og+$-iS)A6W?(~9YQRYVQq^KFzPd3ypu zyG-ezCG%7wDqo_}^;4nQX=JGJ2!$7dDj*G`UUOy0aXGhSO4al?$$pgc)@wNQlw@Q8 z^+286mb?w8dkTGkoHs|Mn0%aC*+C|Y;HT)7jx_UDc`gCxeUd8nP=$k~78ETDm9Ik9 z967bTi`C*pSJoP|px|X1brLsxQ@x+c;{>4^hsTs`lPBnn9qJ;ZnxNxQEwv@TL4Dt( zRwqpz-w*lOr>pV+)zz`DwJ7<;g0XyE2%nmR2t70_91~N_p=BB<@Szh~e4MC|!6C=1 z^wOL#w-zG$6F^AcLW*UPUwLygJZHs;ZG$5rk2mY-$#{CmVd`5R%Q+f9xp)itg|z41 zfhUf=&>T@Yf+*Q6Y1i_(oN$0r3q5xnE7p}n z&fQE0f2)bKv>MxR59S)ZcTe0ok?wiY*q8Hkrn~OEb!U7FX@;I}k;@bQ%$y!yQ?o-S zGx+tKe8^{dPOS^4*W1r5U)*dJmfrxvhQ{TpTuaAtU9P?FIp?#tYq`H)yE@W)*T#Nz z@ym;uJ;!qHT@0_7?HtN<4sE!H{{ihiUkv=|0A)80W!ny9@bes4JC*SauBpHJ`IkS> zG@f|a^|b1*1L0e=CaL$ndZSYX`}hjgQow|{-4?@ zDe1TeE^V2?)(TzzbNi;pdw2ZKc-G^~c*tS(99}+~Yw#?eL85cz{l~7|IqlqJ^(8uT zeJQi|r3Xz97Bj~#WcH3{+Qv8BZ&1e{fBa#ke%JoGGqd;gOxx=l?hDlIJtR(baEEb@ zN>%^v*tuI|mometFjeQ@uCOymBXr|s^=I~UX48=k$J1zFf|cW-v}uGB#& zSG(@i{!!J|2R!6wE5LJh_b1L<&K2p2p5mJ5Z={bYV)#}BA5+x${rFTQ5v8)j@BdjM z7}Z;<{eJvy0;(-kk*|i@!yhInc{`~Gs#4fD1X_4SdW5tOK2HhFOZXZ=lv$L;6dUCY z^R@*U$p%?n)YKJIVx$6PoSJf9axA)Fd!7A(2fLB?s;uDl5%-PhE#7L|3Ya)Y{mn6~ z%<|05^SOHOiv8w=XO>~>@n<4$w(fgY(`-GsHOy5td}Fd%o1dFe+M?1kkK5Y&tf$%9 zL9KP)n5wNK)LOSiB^=gM9d8}u^-3yY38>a z6gAQ@ALG{x+bh}%o{;}N>L_KP(CH`i8qb3byyc%g9N+VI+`hl#y8oVgd!2h5Gjp^) Xs%gDh{cY6+E8nql?4LP$GNAt#9-FJZ literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/svg.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/svg.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c836b0c714bdff083f1bdc372ef05b3d5c45209 GIT binary patch literal 9115 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 H);an=Fpm|< literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/terminal.cpython-312.pyc b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/__pycache__/terminal.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f174d1d1fd086baeaed09ee064ad24be11dfad91 GIT binary patch literal 5781 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 Q_#S>?^X^}9lwfQ754{DDY5)KL literal 0 HcmV?d00001 diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/formatters/_mapping.py b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/_mapping.py new file mode 100644 index 00000000..72ca8404 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/_mapping.py @@ -0,0 +1,23 @@ +# Automatically generated by scripts/gen_mapfiles.py. +# DO NOT EDIT BY HAND; run `tox -e mapfiles` instead. + +FORMATTERS = { + 'BBCodeFormatter': ('pygments.formatters.bbcode', 'BBCode', ('bbcode', 'bb'), (), 'Format tokens with BBcodes. These formatting codes are used by many bulletin boards, so you can highlight your sourcecode with pygments before posting it there.'), + 'BmpImageFormatter': ('pygments.formatters.img', 'img_bmp', ('bmp', 'bitmap'), ('*.bmp',), 'Create a bitmap image from source code. This uses the Python Imaging Library to generate a pixmap from the source code.'), + 'GifImageFormatter': ('pygments.formatters.img', 'img_gif', ('gif',), ('*.gif',), 'Create a GIF image from source code. This uses the Python Imaging Library to generate a pixmap from the source code.'), + 'GroffFormatter': ('pygments.formatters.groff', 'groff', ('groff', 'troff', 'roff'), (), 'Format tokens with groff escapes to change their color and font style.'), + 'HtmlFormatter': ('pygments.formatters.html', 'HTML', ('html',), ('*.html', '*.htm'), "Format tokens as HTML 4 ```` tags. By default, the content is enclosed in a ``
`` tag, itself wrapped in a ``
`` tag (but see the `nowrap` option). The ``
``'s CSS class can be set by the `cssclass` option."), + 'IRCFormatter': ('pygments.formatters.irc', 'IRC', ('irc', 'IRC'), (), 'Format tokens with IRC color sequences'), + 'ImageFormatter': ('pygments.formatters.img', 'img', ('img', 'IMG', 'png'), ('*.png',), 'Create a PNG image from source code. This uses the Python Imaging Library to generate a pixmap from the source code.'), + 'JpgImageFormatter': ('pygments.formatters.img', 'img_jpg', ('jpg', 'jpeg'), ('*.jpg',), 'Create a JPEG image from source code. This uses the Python Imaging Library to generate a pixmap from the source code.'), + 'LatexFormatter': ('pygments.formatters.latex', 'LaTeX', ('latex', 'tex'), ('*.tex',), 'Format tokens as LaTeX code. This needs the `fancyvrb` and `color` standard packages.'), + 'NullFormatter': ('pygments.formatters.other', 'Text only', ('text', 'null'), ('*.txt',), 'Output the text unchanged without any formatting.'), + 'PangoMarkupFormatter': ('pygments.formatters.pangomarkup', 'Pango Markup', ('pango', 'pangomarkup'), (), 'Format tokens as Pango Markup code. It can then be rendered to an SVG.'), + 'RawTokenFormatter': ('pygments.formatters.other', 'Raw tokens', ('raw', 'tokens'), ('*.raw',), 'Format tokens as a raw representation for storing token streams.'), + 'RtfFormatter': ('pygments.formatters.rtf', 'RTF', ('rtf',), ('*.rtf',), 'Format tokens as RTF markup. This formatter automatically outputs full RTF documents with color information and other useful stuff. Perfect for Copy and Paste into Microsoft(R) Word(R) documents.'), + 'SvgFormatter': ('pygments.formatters.svg', 'SVG', ('svg',), ('*.svg',), 'Format tokens as an SVG graphics file. This formatter is still experimental. Each line of code is a ```` element with explicit ``x`` and ``y`` coordinates containing ```` elements with the individual token styles.'), + 'Terminal256Formatter': ('pygments.formatters.terminal256', 'Terminal256', ('terminal256', 'console256', '256'), (), 'Format tokens with ANSI color sequences, for output in a 256-color terminal or console. Like in `TerminalFormatter` color sequences are terminated at newlines, so that paging the output works correctly.'), + 'TerminalFormatter': ('pygments.formatters.terminal', 'Terminal', ('terminal', 'console'), (), 'Format tokens with ANSI color sequences, for output in a text console. Color sequences are terminated at newlines, so that paging the output works correctly.'), + 'TerminalTrueColorFormatter': ('pygments.formatters.terminal256', 'TerminalTrueColor', ('terminal16m', 'console16m', '16m'), (), 'Format tokens with ANSI color sequences, for output in a true-color terminal or console. Like in `TerminalFormatter` color sequences are terminated at newlines, so that paging the output works correctly.'), + 'TestcaseFormatter': ('pygments.formatters.other', 'Testcase', ('testcase',), (), 'Format tokens as appropriate for a new testcase.'), +} diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/formatters/bbcode.py b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/bbcode.py new file mode 100644 index 00000000..339edf9d --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/bbcode.py @@ -0,0 +1,108 @@ +""" + pygments.formatters.bbcode + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + BBcode formatter. + + :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + + +from pygments.formatter import Formatter +from pygments.util import get_bool_opt + +__all__ = ['BBCodeFormatter'] + + +class BBCodeFormatter(Formatter): + """ + Format tokens with BBcodes. These formatting codes are used by many + bulletin boards, so you can highlight your sourcecode with pygments before + posting it there. + + This formatter has no support for background colors and borders, as there + are no common BBcode tags for that. + + Some board systems (e.g. phpBB) don't support colors in their [code] tag, + so you can't use the highlighting together with that tag. + Text in a [code] tag usually is shown with a monospace font (which this + formatter can do with the ``monofont`` option) and no spaces (which you + need for indentation) are removed. + + Additional options accepted: + + `style` + The style to use, can be a string or a Style subclass (default: + ``'default'``). + + `codetag` + If set to true, put the output into ``[code]`` tags (default: + ``false``) + + `monofont` + If set to true, add a tag to show the code with a monospace font + (default: ``false``). + """ + name = 'BBCode' + aliases = ['bbcode', 'bb'] + filenames = [] + + def __init__(self, **options): + Formatter.__init__(self, **options) + self._code = get_bool_opt(options, 'codetag', False) + self._mono = get_bool_opt(options, 'monofont', False) + + self.styles = {} + self._make_styles() + + def _make_styles(self): + for ttype, ndef in self.style: + start = end = '' + if ndef['color']: + start += '[color=#{}]'.format(ndef['color']) + end = '[/color]' + end + if ndef['bold']: + start += '[b]' + end = '[/b]' + end + if ndef['italic']: + start += '[i]' + end = '[/i]' + end + if ndef['underline']: + start += '[u]' + end = '[/u]' + end + # there are no common BBcodes for background-color and border + + self.styles[ttype] = start, end + + def format_unencoded(self, tokensource, outfile): + if self._code: + outfile.write('[code]') + if self._mono: + outfile.write('[font=monospace]') + + lastval = '' + lasttype = None + + for ttype, value in tokensource: + while ttype not in self.styles: + ttype = ttype.parent + if ttype == lasttype: + lastval += value + else: + if lastval: + start, end = self.styles[lasttype] + outfile.write(''.join((start, lastval, end))) + lastval = value + lasttype = ttype + + if lastval: + start, end = self.styles[lasttype] + outfile.write(''.join((start, lastval, end))) + + if self._mono: + outfile.write('[/font]') + if self._code: + outfile.write('[/code]') + if self._code or self._mono: + outfile.write('\n') diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/formatters/groff.py b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/groff.py new file mode 100644 index 00000000..028fec4e --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/groff.py @@ -0,0 +1,170 @@ +""" + pygments.formatters.groff + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + Formatter for groff output. + + :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import math +from pygments.formatter import Formatter +from pygments.util import get_bool_opt, get_int_opt + +__all__ = ['GroffFormatter'] + + +class GroffFormatter(Formatter): + """ + Format tokens with groff escapes to change their color and font style. + + .. versionadded:: 2.11 + + Additional options accepted: + + `style` + The style to use, can be a string or a Style subclass (default: + ``'default'``). + + `monospaced` + If set to true, monospace font will be used (default: ``true``). + + `linenos` + If set to true, print the line numbers (default: ``false``). + + `wrap` + Wrap lines to the specified number of characters. Disabled if set to 0 + (default: ``0``). + """ + + name = 'groff' + aliases = ['groff','troff','roff'] + filenames = [] + + def __init__(self, **options): + Formatter.__init__(self, **options) + + self.monospaced = get_bool_opt(options, 'monospaced', True) + self.linenos = get_bool_opt(options, 'linenos', False) + self._lineno = 0 + self.wrap = get_int_opt(options, 'wrap', 0) + self._linelen = 0 + + self.styles = {} + self._make_styles() + + + def _make_styles(self): + regular = '\\f[CR]' if self.monospaced else '\\f[R]' + bold = '\\f[CB]' if self.monospaced else '\\f[B]' + italic = '\\f[CI]' if self.monospaced else '\\f[I]' + + for ttype, ndef in self.style: + start = end = '' + if ndef['color']: + start += '\\m[{}]'.format(ndef['color']) + end = '\\m[]' + end + if ndef['bold']: + start += bold + end = regular + end + if ndef['italic']: + start += italic + end = regular + end + if ndef['bgcolor']: + start += '\\M[{}]'.format(ndef['bgcolor']) + end = '\\M[]' + end + + self.styles[ttype] = start, end + + + def _define_colors(self, outfile): + colors = set() + for _, ndef in self.style: + if ndef['color'] is not None: + colors.add(ndef['color']) + + for color in sorted(colors): + outfile.write('.defcolor ' + color + ' rgb #' + color + '\n') + + + def _write_lineno(self, outfile): + self._lineno += 1 + outfile.write("%s% 4d " % (self._lineno != 1 and '\n' or '', self._lineno)) + + + def _wrap_line(self, line): + length = len(line.rstrip('\n')) + space = ' ' if self.linenos else '' + newline = '' + + if length > self.wrap: + for i in range(0, math.floor(length / self.wrap)): + chunk = line[i*self.wrap:i*self.wrap+self.wrap] + newline += (chunk + '\n' + space) + remainder = length % self.wrap + if remainder > 0: + newline += line[-remainder-1:] + self._linelen = remainder + elif self._linelen + length > self.wrap: + newline = ('\n' + space) + line + self._linelen = length + else: + newline = line + self._linelen += length + + return newline + + + def _escape_chars(self, text): + text = text.replace('\\', '\\[u005C]'). \ + replace('.', '\\[char46]'). \ + replace('\'', '\\[u0027]'). \ + replace('`', '\\[u0060]'). \ + replace('~', '\\[u007E]') + copy = text + + for char in copy: + if len(char) != len(char.encode()): + uni = char.encode('unicode_escape') \ + .decode()[1:] \ + .replace('x', 'u00') \ + .upper() + text = text.replace(char, '\\[u' + uni[1:] + ']') + + return text + + + def format_unencoded(self, tokensource, outfile): + self._define_colors(outfile) + + outfile.write('.nf\n\\f[CR]\n') + + if self.linenos: + self._write_lineno(outfile) + + for ttype, value in tokensource: + while ttype not in self.styles: + ttype = ttype.parent + start, end = self.styles[ttype] + + for line in value.splitlines(True): + if self.wrap > 0: + line = self._wrap_line(line) + + if start and end: + text = self._escape_chars(line.rstrip('\n')) + if text != '': + outfile.write(''.join((start, text, end))) + else: + outfile.write(self._escape_chars(line.rstrip('\n'))) + + if line.endswith('\n'): + if self.linenos: + self._write_lineno(outfile) + self._linelen = 0 + else: + outfile.write('\n') + self._linelen = 0 + + outfile.write('\n.fi') diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/formatters/html.py b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/html.py new file mode 100644 index 00000000..4ef18368 --- /dev/null +++ b/Backend/venv/lib/python3.12/site-packages/pygments/formatters/html.py @@ -0,0 +1,995 @@ +""" + pygments.formatters.html + ~~~~~~~~~~~~~~~~~~~~~~~~ + + Formatter for HTML output. + + :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import functools +import os +import sys +import os.path +from io import StringIO + +from pygments.formatter import Formatter +from pygments.token import Token, Text, STANDARD_TYPES +from pygments.util import get_bool_opt, get_int_opt, get_list_opt + +try: + import ctags +except ImportError: + ctags = None + +__all__ = ['HtmlFormatter'] + + +_escape_html_table = { + ord('&'): '&', + ord('<'): '<', + ord('>'): '>', + ord('"'): '"', + ord("'"): ''', +} + + +def escape_html(text, table=_escape_html_table): + """Escape &, <, > as well as single and double quotes for HTML.""" + return text.translate(table) + + +def webify(color): + if color.startswith('calc') or color.startswith('var'): + return color + else: + # Check if the color can be shortened from 6 to 3 characters + color = color.upper() + if (len(color) == 6 and + ( color[0] == color[1] + and color[2] == color[3] + and color[4] == color[5])): + return f'#{color[0]}{color[2]}{color[4]}' + else: + return f'#{color}' + + +def _get_ttype_class(ttype): + fname = STANDARD_TYPES.get(ttype) + if fname: + return fname + aname = '' + while fname is None: + aname = '-' + ttype[-1] + aname + ttype = ttype.parent + fname = STANDARD_TYPES.get(ttype) + return fname + aname + + +CSSFILE_TEMPLATE = '''\ +/* +generated by Pygments +Copyright 2006-2025 by the Pygments team. +Licensed under the BSD license, see LICENSE for details. +*/ +%(styledefs)s +''' + +DOC_HEADER = '''\ + + + + + %(title)s + + + + +

%(title)s

+ +''' + +DOC_HEADER_EXTERNALCSS = '''\ + + + + + %(title)s + + + + +

%(title)s

+ +''' + +DOC_FOOTER = '''\ + + +''' + + +class HtmlFormatter(Formatter): + r""" + Format tokens as HTML 4 ```` tags. By default, the content is enclosed + in a ``
`` tag, itself wrapped in a ``
`` tag (but see the `nowrap` option). + The ``
``'s CSS class can be set by the `cssclass` option. + + If the `linenos` option is set to ``"table"``, the ``
`` is
+    additionally wrapped inside a ```` which has one row and two
+    cells: one containing the line numbers and one containing the code.
+    Example:
+
+    .. sourcecode:: html
+
+        
+
+ + +
+
1
+            2
+
+
def foo(bar):
+              pass
+            
+
+ + (whitespace added to improve clarity). + + A list of lines can be specified using the `hl_lines` option to make these + lines highlighted (as of Pygments 0.11). + + With the `full` option, a complete HTML 4 document is output, including + the style definitions inside a ``$)', _handle_cssblock), + + include('keywords'), + include('inline'), + ], + 'keywords': [ + (words(( + '\\define', '\\end', 'caption', 'created', 'modified', 'tags', + 'title', 'type'), prefix=r'^', suffix=r'\b'), + Keyword), + ], + 'inline': [ + # escape + (r'\\.', Text), + # created or modified date + (r'\d{17}', Number.Integer), + # italics + (r'(\s)(//[^/]+//)((?=\W|\n))', + bygroups(Text, Generic.Emph, Text)), + # superscript + (r'(\s)(\^\^[^\^]+\^\^)', bygroups(Text, Generic.Emph)), + # subscript + (r'(\s)(,,[^,]+,,)', bygroups(Text, Generic.Emph)), + # underscore + (r'(\s)(__[^_]+__)', bygroups(Text, Generic.Strong)), + # bold + (r"(\s)(''[^']+'')((?=\W|\n))", + bygroups(Text, Generic.Strong, Text)), + # strikethrough + (r'(\s)(~~[^~]+~~)((?=\W|\n))', + bygroups(Text, Generic.Deleted, Text)), + # TiddlyWiki variables + (r'<<[^>]+>>', Name.Tag), + (r'\$\$[^$]+\$\$', Name.Tag), + (r'\$\([^)]+\)\$', Name.Tag), + # TiddlyWiki style or class + (r'^@@.*$', Name.Tag), + # HTML tags + (r']+>', Name.Tag), + # inline code + (r'`[^`]+`', String.Backtick), + # HTML escaped symbols + (r'&\S*?;', String.Regex), + # Wiki links + (r'(\[{2})([^]\|]+)(\]{2})', bygroups(Text, Name.Tag, Text)), + # External links + (r'(\[{2})([^]\|]+)(\|)([^]\|]+)(\]{2})', + bygroups(Text, Name.Tag, Text, Name.Attribute, Text)), + # Transclusion + (r'(\{{2})([^}]+)(\}{2})', bygroups(Text, Name.Tag, Text)), + # URLs + (r'(\b.?.?tps?://[^\s"]+)', bygroups(Name.Attribute)), + + # general text, must come last! + (r'[\w]+', Text), + (r'.', Text) + ], + } + + def __init__(self, **options): + self.handlecodeblocks = get_bool_opt(options, 'handlecodeblocks', True) + RegexLexer.__init__(self, **options) + + +class WikitextLexer(RegexLexer): + """ + For MediaWiki Wikitext. + + Parsing Wikitext is tricky, and results vary between different MediaWiki + installations, so we only highlight common syntaxes (built-in or from + popular extensions), and also assume templates produce no unbalanced + syntaxes. + """ + name = 'Wikitext' + url = 'https://www.mediawiki.org/wiki/Wikitext' + aliases = ['wikitext', 'mediawiki'] + filenames = [] + mimetypes = ['text/x-wiki'] + version_added = '2.15' + flags = re.MULTILINE + + def nowiki_tag_rules(tag_name): + return [ + (rf'(?i)()', bygroups(Punctuation, + Name.Tag, Whitespace, Punctuation), '#pop'), + include('entity'), + include('text'), + ] + + def plaintext_tag_rules(tag_name): + return [ + (rf'(?si)(.*?)()', bygroups(Text, + Punctuation, Name.Tag, Whitespace, Punctuation), '#pop'), + ] + + def delegate_tag_rules(tag_name, lexer, **lexer_kwargs): + return [ + (rf'(?i)()', bygroups(Punctuation, + Name.Tag, Whitespace, Punctuation), '#pop'), + (rf'(?si).+?(?=)', using(lexer, **lexer_kwargs)), + ] + + def text_rules(token): + return [ + (r'\w+', token), + (r'[^\S\n]+', token), + (r'(?s).', token), + ] + + def handle_syntaxhighlight(self, match, ctx): + from pygments.lexers import get_lexer_by_name + + attr_content = match.group() + start = 0 + index = 0 + while True: + index = attr_content.find('>', start) + # Exclude comment end (-->) + if attr_content[index-2:index] != '--': + break + start = index + 1 + + if index == -1: + # No tag end + yield from self.get_tokens_unprocessed(attr_content, stack=['root', 'attr']) + return + attr = attr_content[:index] + yield from self.get_tokens_unprocessed(attr, stack=['root', 'attr']) + yield match.start(3) + index, Punctuation, '>' + + lexer = None + content = attr_content[index+1:] + lang_match = re.findall(r'\blang=("|\'|)(\w+)(\1)', attr) + + if len(lang_match) >= 1: + # Pick the last match in case of multiple matches + lang = lang_match[-1][1] + try: + lexer = get_lexer_by_name(lang) + except ClassNotFound: + pass + + if lexer is None: + yield match.start() + index + 1, Text, content + else: + yield from lexer.get_tokens_unprocessed(content) + + def handle_score(self, match, ctx): + attr_content = match.group() + start = 0 + index = 0 + while True: + index = attr_content.find('>', start) + # Exclude comment end (-->) + if attr_content[index-2:index] != '--': + break + start = index + 1 + + if index == -1: + # No tag end + yield from self.get_tokens_unprocessed(attr_content, stack=['root', 'attr']) + return + attr = attr_content[:index] + content = attr_content[index+1:] + yield from self.get_tokens_unprocessed(attr, stack=['root', 'attr']) + yield match.start(3) + index, Punctuation, '>' + + lang_match = re.findall(r'\blang=("|\'|)(\w+)(\1)', attr) + # Pick the last match in case of multiple matches + lang = lang_match[-1][1] if len(lang_match) >= 1 else 'lilypond' + + if lang == 'lilypond': # Case sensitive + yield from LilyPondLexer().get_tokens_unprocessed(content) + else: # ABC + # FIXME: Use ABC lexer in the future + yield match.start() + index + 1, Text, content + + # a-z removed to prevent linter from complaining, REMEMBER to use (?i) + title_char = r' %!"$&\'()*,\-./0-9:;=?@A-Z\\\^_`~+\u0080-\uFFFF' + nbsp_char = r'(?:\t| |&\#0*160;|&\#[Xx]0*[Aa]0;|[ \xA0\u1680\u2000-\u200A\u202F\u205F\u3000])' + link_address = r'(?:[0-9.]+|\[[0-9a-f:.]+\]|[^\x00-\x20"<>\[\]\x7F\xA0\u1680\u2000-\u200A\u202F\u205F\u3000\uFFFD])' + link_char_class = r'[^\x00-\x20"<>\[\]\x7F\xA0\u1680\u2000-\u200A\u202F\u205F\u3000\uFFFD]' + double_slashes_i = { + '__FORCETOC__', '__NOCONTENTCONVERT__', '__NOCC__', '__NOEDITSECTION__', '__NOGALLERY__', + '__NOTITLECONVERT__', '__NOTC__', '__NOTOC__', '__TOC__', + } + double_slashes = { + '__EXPECTUNUSEDCATEGORY__', '__HIDDENCAT__', '__INDEX__', '__NEWSECTIONLINK__', + '__NOINDEX__', '__NONEWSECTIONLINK__', '__STATICREDIRECT__', '__NOGLOBAL__', + '__DISAMBIG__', '__EXPECTED_UNCONNECTED_PAGE__', + } + protocols = { + 'bitcoin:', 'ftp://', 'ftps://', 'geo:', 'git://', 'gopher://', 'http://', 'https://', + 'irc://', 'ircs://', 'magnet:', 'mailto:', 'mms://', 'news:', 'nntp://', 'redis://', + 'sftp://', 'sip:', 'sips:', 'sms:', 'ssh://', 'svn://', 'tel:', 'telnet://', 'urn:', + 'worldwind://', 'xmpp:', '//', + } + non_relative_protocols = protocols - {'//'} + html_tags = { + 'abbr', 'b', 'bdi', 'bdo', 'big', 'blockquote', 'br', 'caption', 'center', 'cite', 'code', + 'data', 'dd', 'del', 'dfn', 'div', 'dl', 'dt', 'em', 'font', 'h1', 'h2', 'h3', 'h4', 'h5', + 'h6', 'hr', 'i', 'ins', 'kbd', 'li', 'link', 'mark', 'meta', 'ol', 'p', 'q', 'rb', 'rp', + 'rt', 'rtc', 'ruby', 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', + 'table', 'td', 'th', 'time', 'tr', 'tt', 'u', 'ul', 'var', 'wbr', + } + parser_tags = { + 'graph', 'charinsert', 'rss', 'chem', 'categorytree', 'nowiki', 'inputbox', 'math', + 'hiero', 'score', 'pre', 'ref', 'translate', 'imagemap', 'templatestyles', 'languages', + 'noinclude', 'mapframe', 'section', 'poem', 'syntaxhighlight', 'includeonly', 'tvar', + 'onlyinclude', 'templatedata', 'langconvert', 'timeline', 'dynamicpagelist', 'gallery', + 'maplink', 'ce', 'references', + } + variant_langs = { + # ZhConverter.php + 'zh', 'zh-hans', 'zh-hant', 'zh-cn', 'zh-hk', 'zh-mo', 'zh-my', 'zh-sg', 'zh-tw', + # WuuConverter.php + 'wuu', 'wuu-hans', 'wuu-hant', + # UzConverter.php + 'uz', 'uz-latn', 'uz-cyrl', + # TlyConverter.php + 'tly', 'tly-cyrl', + # TgConverter.php + 'tg', 'tg-latn', + # SrConverter.php + 'sr', 'sr-ec', 'sr-el', + # ShiConverter.php + 'shi', 'shi-tfng', 'shi-latn', + # ShConverter.php + 'sh-latn', 'sh-cyrl', + # KuConverter.php + 'ku', 'ku-arab', 'ku-latn', + # IuConverter.php + 'iu', 'ike-cans', 'ike-latn', + # GanConverter.php + 'gan', 'gan-hans', 'gan-hant', + # EnConverter.php + 'en', 'en-x-piglatin', + # CrhConverter.php + 'crh', 'crh-cyrl', 'crh-latn', + # BanConverter.php + 'ban', 'ban-bali', 'ban-x-dharma', 'ban-x-palmleaf', 'ban-x-pku', + } + magic_vars_i = { + 'ARTICLEPATH', 'INT', 'PAGEID', 'SCRIPTPATH', 'SERVER', 'SERVERNAME', 'STYLEPATH', + } + magic_vars = { + '!', '=', 'BASEPAGENAME', 'BASEPAGENAMEE', 'CASCADINGSOURCES', 'CONTENTLANGUAGE', + 'CONTENTLANG', 'CURRENTDAY', 'CURRENTDAY2', 'CURRENTDAYNAME', 'CURRENTDOW', 'CURRENTHOUR', + 'CURRENTMONTH', 'CURRENTMONTH2', 'CURRENTMONTH1', 'CURRENTMONTHABBREV', 'CURRENTMONTHNAME', + 'CURRENTMONTHNAMEGEN', 'CURRENTTIME', 'CURRENTTIMESTAMP', 'CURRENTVERSION', 'CURRENTWEEK', + 'CURRENTYEAR', 'DIRECTIONMARK', 'DIRMARK', 'FULLPAGENAME', 'FULLPAGENAMEE', 'LOCALDAY', + 'LOCALDAY2', 'LOCALDAYNAME', 'LOCALDOW', 'LOCALHOUR', 'LOCALMONTH', 'LOCALMONTH2', + 'LOCALMONTH1', 'LOCALMONTHABBREV', 'LOCALMONTHNAME', 'LOCALMONTHNAMEGEN', 'LOCALTIME', + 'LOCALTIMESTAMP', 'LOCALWEEK', 'LOCALYEAR', 'NAMESPACE', 'NAMESPACEE', 'NAMESPACENUMBER', + 'NUMBEROFACTIVEUSERS', 'NUMBEROFADMINS', 'NUMBEROFARTICLES', 'NUMBEROFEDITS', + 'NUMBEROFFILES', 'NUMBEROFPAGES', 'NUMBEROFUSERS', 'PAGELANGUAGE', 'PAGENAME', 'PAGENAMEE', + 'REVISIONDAY', 'REVISIONDAY2', 'REVISIONID', 'REVISIONMONTH', 'REVISIONMONTH1', + 'REVISIONSIZE', 'REVISIONTIMESTAMP', 'REVISIONUSER', 'REVISIONYEAR', 'ROOTPAGENAME', + 'ROOTPAGENAMEE', 'SITENAME', 'SUBJECTPAGENAME', 'ARTICLEPAGENAME', 'SUBJECTPAGENAMEE', + 'ARTICLEPAGENAMEE', 'SUBJECTSPACE', 'ARTICLESPACE', 'SUBJECTSPACEE', 'ARTICLESPACEE', + 'SUBPAGENAME', 'SUBPAGENAMEE', 'TALKPAGENAME', 'TALKPAGENAMEE', 'TALKSPACE', 'TALKSPACEE', + } + parser_functions_i = { + 'ANCHORENCODE', 'BIDI', 'CANONICALURL', 'CANONICALURLE', 'FILEPATH', 'FORMATNUM', + 'FULLURL', 'FULLURLE', 'GENDER', 'GRAMMAR', 'INT', r'\#LANGUAGE', 'LC', 'LCFIRST', 'LOCALURL', + 'LOCALURLE', 'NS', 'NSE', 'PADLEFT', 'PADRIGHT', 'PAGEID', 'PLURAL', 'UC', 'UCFIRST', + 'URLENCODE', + } + parser_functions = { + 'BASEPAGENAME', 'BASEPAGENAMEE', 'CASCADINGSOURCES', 'DEFAULTSORT', 'DEFAULTSORTKEY', + 'DEFAULTCATEGORYSORT', 'FULLPAGENAME', 'FULLPAGENAMEE', 'NAMESPACE', 'NAMESPACEE', + 'NAMESPACENUMBER', 'NUMBERINGROUP', 'NUMINGROUP', 'NUMBEROFACTIVEUSERS', 'NUMBEROFADMINS', + 'NUMBEROFARTICLES', 'NUMBEROFEDITS', 'NUMBEROFFILES', 'NUMBEROFPAGES', 'NUMBEROFUSERS', + 'PAGENAME', 'PAGENAMEE', 'PAGESINCATEGORY', 'PAGESINCAT', 'PAGESIZE', 'PROTECTIONEXPIRY', + 'PROTECTIONLEVEL', 'REVISIONDAY', 'REVISIONDAY2', 'REVISIONID', 'REVISIONMONTH', + 'REVISIONMONTH1', 'REVISIONTIMESTAMP', 'REVISIONUSER', 'REVISIONYEAR', 'ROOTPAGENAME', + 'ROOTPAGENAMEE', 'SUBJECTPAGENAME', 'ARTICLEPAGENAME', 'SUBJECTPAGENAMEE', + 'ARTICLEPAGENAMEE', 'SUBJECTSPACE', 'ARTICLESPACE', 'SUBJECTSPACEE', 'ARTICLESPACEE', + 'SUBPAGENAME', 'SUBPAGENAMEE', 'TALKPAGENAME', 'TALKPAGENAMEE', 'TALKSPACE', 'TALKSPACEE', + 'INT', 'DISPLAYTITLE', 'PAGESINNAMESPACE', 'PAGESINNS', + } + + tokens = { + 'root': [ + # Redirects + (r"""(?xi) + (\A\s*?)(\#REDIRECT:?) # may contain a colon + (\s+)(\[\[) (?=[^\]\n]* \]\]$) + """, + bygroups(Whitespace, Keyword, Whitespace, Punctuation), 'redirect-inner'), + # Subheadings + (r'^(={2,6})(.+?)(\1)(\s*$\n)', + bygroups(Generic.Subheading, Generic.Subheading, Generic.Subheading, Whitespace)), + # Headings + (r'^(=.+?=)(\s*$\n)', + bygroups(Generic.Heading, Whitespace)), + # Double-slashed magic words + (words(double_slashes_i, prefix=r'(?i)'), Name.Function.Magic), + (words(double_slashes), Name.Function.Magic), + # Raw URLs + (r'(?i)\b(?:{}){}{}*'.format('|'.join(protocols), + link_address, link_char_class), Name.Label), + # Magic links + (rf'\b(?:RFC|PMID){nbsp_char}+[0-9]+\b', + Name.Function.Magic), + (r"""(?x) + \bISBN {nbsp_char} + (?: 97[89] {nbsp_dash}? )? + (?: [0-9] {nbsp_dash}? ){{9}} # escape format() + [0-9Xx]\b + """.format(nbsp_char=nbsp_char, nbsp_dash=f'(?:-|{nbsp_char})'), Name.Function.Magic), + include('list'), + include('inline'), + include('text'), + ], + 'redirect-inner': [ + (r'(\]\])(\s*?\n)', bygroups(Punctuation, Whitespace), '#pop'), + (r'(\#)([^#]*?)', bygroups(Punctuation, Name.Label)), + (rf'(?i)[{title_char}]+', Name.Tag), + ], + 'list': [ + # Description lists + (r'^;', Keyword, 'dt'), + # Ordered lists, unordered lists and indents + (r'^[#:*]+', Keyword), + # Horizontal rules + (r'^-{4,}', Keyword), + ], + 'inline': [ + # Signatures + (r'~{3,5}', Keyword), + # Entities + include('entity'), + # Bold & italic + (r"('')(''')(?!')", bygroups(Generic.Emph, + Generic.EmphStrong), 'inline-italic-bold'), + (r"'''(?!')", Generic.Strong, 'inline-bold'), + (r"''(?!')", Generic.Emph, 'inline-italic'), + # Comments & parameters & templates + include('replaceable'), + # Media links + ( + r"""(?xi) + (\[\[) + (File|Image) (:) + ((?: [{}] | \{{{{2,3}}[^{{}}]*?\}}{{2,3}} | )*) + (?: (\#) ([{}]*?) )? + """.format(title_char, f'{title_char}#'), + bygroups(Punctuation, Name.Namespace, Punctuation, + using(this, state=['wikilink-name']), Punctuation, Name.Label), + 'medialink-inner' + ), + # Wikilinks + ( + r"""(?xi) + (\[\[)(?!{}) # Should not contain URLs + (?: ([{}]*) (:))? + ((?: [{}] | \{{{{2,3}}[^{{}}]*?\}}{{2,3}} | )*?) + (?: (\#) ([{}]*?) )? + (\]\]) + """.format('|'.join(protocols), title_char.replace('/', ''), + title_char, f'{title_char}#'), + bygroups(Punctuation, Name.Namespace, Punctuation, + using(this, state=['wikilink-name']), Punctuation, Name.Label, Punctuation) + ), + ( + r"""(?xi) + (\[\[)(?!{}) + (?: ([{}]*) (:))? + ((?: [{}] | \{{{{2,3}}[^{{}}]*?\}}{{2,3}} | )*?) + (?: (\#) ([{}]*?) )? + (\|) + """.format('|'.join(protocols), title_char.replace('/', ''), + title_char, f'{title_char}#'), + bygroups(Punctuation, Name.Namespace, Punctuation, + using(this, state=['wikilink-name']), Punctuation, Name.Label, Punctuation), + 'wikilink-inner' + ), + # External links + ( + r"""(?xi) + (\[) + ((?:{}) {} {}*) + (\s*) + """.format('|'.join(protocols), link_address, link_char_class), + bygroups(Punctuation, Name.Label, Whitespace), + 'extlink-inner' + ), + # Tables + (r'^(:*)(\s*?)(\{\|)([^\n]*)$', bygroups(Keyword, + Whitespace, Punctuation, using(this, state=['root', 'attr'])), 'table'), + # HTML tags + (r'(?i)(<)({})\b'.format('|'.join(html_tags)), + bygroups(Punctuation, Name.Tag), 'tag-inner-ordinary'), + (r'(?i)()'.format('|'.join(html_tags)), + bygroups(Punctuation, Name.Tag, Whitespace, Punctuation)), + # + (r'(?i)(<)(nowiki)\b', bygroups(Punctuation, + Name.Tag), ('tag-nowiki', 'tag-inner')), + #
+            (r'(?i)(<)(pre)\b', bygroups(Punctuation,
+             Name.Tag), ('tag-pre', 'tag-inner')),
+            # 
+            (r'(?i)(<)(categorytree)\b', bygroups(
+                Punctuation, Name.Tag), ('tag-categorytree', 'tag-inner')),
+            # 
+            (r'(?i)(<)(hiero)\b', bygroups(Punctuation,
+             Name.Tag), ('tag-hiero', 'tag-inner')),
+            # 
+            (r'(?i)(<)(math)\b', bygroups(Punctuation,
+             Name.Tag), ('tag-math', 'tag-inner')),
+            # 
+            (r'(?i)(<)(chem)\b', bygroups(Punctuation,
+             Name.Tag), ('tag-chem', 'tag-inner')),
+            # 
+            (r'(?i)(<)(ce)\b', bygroups(Punctuation,
+             Name.Tag), ('tag-ce', 'tag-inner')),
+            # 
+            (r'(?i)(<)(charinsert)\b', bygroups(
+                Punctuation, Name.Tag), ('tag-charinsert', 'tag-inner')),
+            # 
+            (r'(?i)(<)(templatedata)\b', bygroups(
+                Punctuation, Name.Tag), ('tag-templatedata', 'tag-inner')),
+            # 
+            (r'(?i)(<)(gallery)\b', bygroups(
+                Punctuation, Name.Tag), ('tag-gallery', 'tag-inner')),
+            # 
+            (r'(?i)(<)(gallery)\b', bygroups(
+                Punctuation, Name.Tag), ('tag-graph', 'tag-inner')),
+            # 
+            (r'(?i)(<)(dynamicpagelist)\b', bygroups(
+                Punctuation, Name.Tag), ('tag-dynamicpagelist', 'tag-inner')),
+            # 
+            (r'(?i)(<)(inputbox)\b', bygroups(
+                Punctuation, Name.Tag), ('tag-inputbox', 'tag-inner')),
+            # 
+            (r'(?i)(<)(rss)\b', bygroups(
+                Punctuation, Name.Tag), ('tag-rss', 'tag-inner')),
+            # 
+            (r'(?i)(<)(imagemap)\b', bygroups(
+                Punctuation, Name.Tag), ('tag-imagemap', 'tag-inner')),
+            # 
+            (r'(?i)()',
+             bygroups(Punctuation, Name.Tag, Whitespace, Punctuation)),
+            (r'(?si)(<)(syntaxhighlight)\b([^>]*?(?.*?)(?=)',
+             bygroups(Punctuation, Name.Tag, handle_syntaxhighlight)),
+            # : Fallback case for self-closing tags
+            (r'(?i)(<)(syntaxhighlight)\b(\s*?)((?:[^>]|-->)*?)(/\s*?(?)*?)(/\s*?(?)*?)(/\s*?(?|\Z)', Comment.Multiline),
+            # Parameters
+            (
+                r"""(?x)
+                (\{{3})
+                    ([^|]*?)
+                    (?=\}{3}|\|)
+                """,
+                bygroups(Punctuation, Name.Variable),
+                'parameter-inner',
+            ),
+            # Magic variables
+            (r'(?i)(\{{\{{)(\s*)({})(\s*)(\}}\}})'.format('|'.join(magic_vars_i)),
+             bygroups(Punctuation, Whitespace, Name.Function, Whitespace, Punctuation)),
+            (r'(\{{\{{)(\s*)({})(\s*)(\}}\}})'.format('|'.join(magic_vars)),
+                bygroups(Punctuation, Whitespace, Name.Function, Whitespace, Punctuation)),
+            # Parser functions & templates
+            (r'\{\{', Punctuation, 'template-begin-space'),
+            #  legacy syntax
+            (r'(?i)(<)(tvar)\b(\|)([^>]*?)(>)', bygroups(Punctuation,
+             Name.Tag, Punctuation, String, Punctuation)),
+            (r'', Punctuation, '#pop'),
+            # 
+            (r'(?i)(<)(tvar)\b', bygroups(Punctuation, Name.Tag), 'tag-inner-ordinary'),
+            (r'(?i)()',
+             bygroups(Punctuation, Name.Tag, Whitespace, Punctuation)),
+        ],
+        'parameter-inner': [
+            (r'\}{3}', Punctuation, '#pop'),
+            (r'\|', Punctuation),
+            include('inline'),
+            include('text'),
+        ],
+        'template-begin-space': [
+            # Templates allow line breaks at the beginning, and due to how MediaWiki handles
+            # comments, an extra state is required to handle things like {{\n\n name}}
+            (r'|\Z)', Comment.Multiline),
+            (r'\s+', Whitespace),
+            # Parser functions
+            (
+                r'(?i)(\#[{}]*?|{})(:)'.format(title_char,
+                                           '|'.join(parser_functions_i)),
+                bygroups(Name.Function, Punctuation), ('#pop', 'template-inner')
+            ),
+            (
+                r'({})(:)'.format('|'.join(parser_functions)),
+                bygroups(Name.Function, Punctuation), ('#pop', 'template-inner')
+            ),
+            # Templates
+            (
+                rf'(?i)([{title_char}]*?)(:)',
+                bygroups(Name.Namespace, Punctuation), ('#pop', 'template-name')
+            ),
+            default(('#pop', 'template-name'),),
+        ],
+        'template-name': [
+            (r'(\s*?)(\|)', bygroups(Text, Punctuation), ('#pop', 'template-inner')),
+            (r'\}\}', Punctuation, '#pop'),
+            (r'\n', Text, '#pop'),
+            include('replaceable'),
+            *text_rules(Name.Tag),
+        ],
+        'template-inner': [
+            (r'\}\}', Punctuation, '#pop'),
+            (r'\|', Punctuation),
+            (
+                r"""(?x)
+                    (?<=\|)
+                    ( (?: (?! \{\{ | \}\} )[^=\|<])*? ) # Exclude templates and tags
+                    (=)
+                """,
+                bygroups(Name.Label, Operator)
+            ),
+            include('inline'),
+            include('text'),
+        ],
+        'table': [
+            # Use [ \t\n\r\0\x0B] instead of \s to follow PHP trim() behavior
+            # Endings
+            (r'^([ \t\n\r\0\x0B]*?)(\|\})',
+             bygroups(Whitespace, Punctuation), '#pop'),
+            # Table rows
+            (r'^([ \t\n\r\0\x0B]*?)(\|-+)(.*)$', bygroups(Whitespace, Punctuation,
+             using(this, state=['root', 'attr']))),
+            # Captions
+            (
+                r"""(?x)
+                ^([ \t\n\r\0\x0B]*?)(\|\+)
+                # Exclude links, template and tags
+                (?: ( (?: (?! \[\[ | \{\{ )[^|\n<] )*? )(\|) )?
+                (.*?)$
+                """,
+                bygroups(Whitespace, Punctuation, using(this, state=[
+                         'root', 'attr']), Punctuation, Generic.Heading),
+            ),
+            # Table data
+            (
+                r"""(?x)
+                ( ^(?:[ \t\n\r\0\x0B]*?)\| | \|\| )
+                (?: ( (?: (?! \[\[ | \{\{ )[^|\n<] )*? )(\|)(?!\|) )?
+                """,
+                bygroups(Punctuation, using(this, state=[
+                         'root', 'attr']), Punctuation),
+            ),
+            # Table headers
+            (
+                r"""(?x)
+                ( ^(?:[ \t\n\r\0\x0B]*?)!  )
+                (?: ( (?: (?! \[\[ | \{\{ )[^|\n<] )*? )(\|)(?!\|) )?
+                """,
+                bygroups(Punctuation, using(this, state=[
+                         'root', 'attr']), Punctuation),
+                'table-header',
+            ),
+            include('list'),
+            include('inline'),
+            include('text'),
+        ],
+        'table-header': [
+            # Requires another state for || handling inside headers
+            (r'\n', Text, '#pop'),
+            (
+                r"""(?x)
+                (!!|\|\|)
+                (?:
+                    ( (?: (?! \[\[ | \{\{ )[^|\n<] )*? )
+                    (\|)(?!\|)
+                )?
+                """,
+                bygroups(Punctuation, using(this, state=[
+                         'root', 'attr']), Punctuation)
+            ),
+            *text_rules(Generic.Subheading),
+        ],
+        'entity': [
+            (r'&\S*?;', Name.Entity),
+        ],
+        'dt': [
+            (r'\n', Text, '#pop'),
+            include('inline'),
+            (r':', Keyword, '#pop'),
+            include('text'),
+        ],
+        'extlink-inner': [
+            (r'\]', Punctuation, '#pop'),
+            include('inline'),
+            include('text'),
+        ],
+        'nowiki-ish': [
+            include('entity'),
+            include('text'),
+        ],
+        'attr': [
+            include('replaceable'),
+            (r'\s+', Whitespace),
+            (r'(=)(\s*)(")', bygroups(Operator, Whitespace, String.Double), 'attr-val-2'),
+            (r"(=)(\s*)(')", bygroups(Operator, Whitespace, String.Single), 'attr-val-1'),
+            (r'(=)(\s*)', bygroups(Operator, Whitespace), 'attr-val-0'),
+            (r'[\w:-]+', Name.Attribute),
+
+        ],
+        'attr-val-0': [
+            (r'\s', Whitespace, '#pop'),
+            include('replaceable'),
+            *text_rules(String),
+        ],
+        'attr-val-1': [
+            (r"'", String.Single, '#pop'),
+            include('replaceable'),
+            *text_rules(String.Single),
+        ],
+        'attr-val-2': [
+            (r'"', String.Double, '#pop'),
+            include('replaceable'),
+            *text_rules(String.Double),
+        ],
+        'tag-inner-ordinary': [
+            (r'/?\s*>', Punctuation, '#pop'),
+            include('tag-attr'),
+        ],
+        'tag-inner': [
+            # Return to root state for self-closing tags
+            (r'/\s*>', Punctuation, '#pop:2'),
+            (r'\s*>', Punctuation, '#pop'),
+            include('tag-attr'),
+        ],
+        # There states below are just like their non-tag variants, the key difference is
+        # they forcibly quit when encountering tag closing markup
+        'tag-attr': [
+            include('replaceable'),
+            (r'\s+', Whitespace),
+            (r'(=)(\s*)(")', bygroups(Operator,
+             Whitespace, String.Double), 'tag-attr-val-2'),
+            (r"(=)(\s*)(')", bygroups(Operator,
+             Whitespace, String.Single), 'tag-attr-val-1'),
+            (r'(=)(\s*)', bygroups(Operator, Whitespace), 'tag-attr-val-0'),
+            (r'[\w:-]+', Name.Attribute),
+
+        ],
+        'tag-attr-val-0': [
+            (r'\s', Whitespace, '#pop'),
+            (r'/?>', Punctuation, '#pop:2'),
+            include('replaceable'),
+            *text_rules(String),
+        ],
+        'tag-attr-val-1': [
+            (r"'", String.Single, '#pop'),
+            (r'/?>', Punctuation, '#pop:2'),
+            include('replaceable'),
+            *text_rules(String.Single),
+        ],
+        'tag-attr-val-2': [
+            (r'"', String.Double, '#pop'),
+            (r'/?>', Punctuation, '#pop:2'),
+            include('replaceable'),
+            *text_rules(String.Double),
+        ],
+        'tag-nowiki': nowiki_tag_rules('nowiki'),
+        'tag-pre': nowiki_tag_rules('pre'),
+        'tag-categorytree': plaintext_tag_rules('categorytree'),
+        'tag-dynamicpagelist': plaintext_tag_rules('dynamicpagelist'),
+        'tag-hiero': plaintext_tag_rules('hiero'),
+        'tag-inputbox': plaintext_tag_rules('inputbox'),
+        'tag-imagemap': plaintext_tag_rules('imagemap'),
+        'tag-charinsert': plaintext_tag_rules('charinsert'),
+        'tag-timeline': plaintext_tag_rules('timeline'),
+        'tag-gallery': plaintext_tag_rules('gallery'),
+        'tag-graph': plaintext_tag_rules('graph'),
+        'tag-rss': plaintext_tag_rules('rss'),
+        'tag-math': delegate_tag_rules('math', TexLexer, state='math'),
+        'tag-chem': delegate_tag_rules('chem', TexLexer, state='math'),
+        'tag-ce': delegate_tag_rules('ce', TexLexer, state='math'),
+        'tag-templatedata': delegate_tag_rules('templatedata', JsonLexer),
+        'text-italic': text_rules(Generic.Emph),
+        'text-bold': text_rules(Generic.Strong),
+        'text-bold-italic': text_rules(Generic.EmphStrong),
+        'text': text_rules(Text),
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/math.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/math.py
new file mode 100644
index 00000000..b225ffcf
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/math.py
@@ -0,0 +1,21 @@
+"""
+    pygments.lexers.math
+    ~~~~~~~~~~~~~~~~~~~~
+
+    Just export lexers that were contained in this module.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+# ruff: noqa: F401
+from pygments.lexers.python import NumPyLexer
+from pygments.lexers.matlab import MatlabLexer, MatlabSessionLexer, \
+    OctaveLexer, ScilabLexer
+from pygments.lexers.julia import JuliaLexer, JuliaConsoleLexer
+from pygments.lexers.r import RConsoleLexer, SLexer, RdLexer
+from pygments.lexers.modeling import BugsLexer, JagsLexer, StanLexer
+from pygments.lexers.idl import IDLLexer
+from pygments.lexers.algebra import MuPADLexer
+
+__all__ = []
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/matlab.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/matlab.py
new file mode 100644
index 00000000..8eeffc9d
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/matlab.py
@@ -0,0 +1,3307 @@
+"""
+    pygments.lexers.matlab
+    ~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for Matlab and related languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import Lexer, RegexLexer, bygroups, default, words, \
+    do_insertions, include
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation, Generic, Whitespace
+
+from pygments.lexers import _scilab_builtins
+
+__all__ = ['MatlabLexer', 'MatlabSessionLexer', 'OctaveLexer', 'ScilabLexer']
+
+
+class MatlabLexer(RegexLexer):
+    """
+    For Matlab source code.
+    """
+    name = 'Matlab'
+    aliases = ['matlab']
+    filenames = ['*.m']
+    mimetypes = ['text/matlab']
+    url = 'https://www.mathworks.com/products/matlab.html'
+    version_added = '0.10'
+
+    _operators = r'-|==|~=|<=|>=|<|>|&&|&|~|\|\|?|\.\*|\*|\+|\.\^|\^|\.\\|\./|/|\\'
+
+    tokens = {
+        'expressions': [
+            # operators:
+            (_operators, Operator),
+
+            # numbers (must come before punctuation to handle `.5`; cannot use
+            # `\b` due to e.g. `5. + .5`).  The negative lookahead on operators
+            # avoids including the dot in `1./x` (the dot is part of `./`).
+            (rf'(? and then
+            # (equal | open-parenthesis |  | ).
+            (rf'(?:^|(?<=;))(\s*)(\w+)(\s+)(?!=|\(|{_operators}\s|\s)',
+             bygroups(Whitespace, Name, Whitespace), 'commandargs'),
+
+            include('expressions')
+        ],
+        'blockcomment': [
+            (r'^\s*%\}', Comment.Multiline, '#pop'),
+            (r'^.*\n', Comment.Multiline),
+            (r'.', Comment.Multiline),
+        ],
+        'deffunc': [
+            (r'(\s*)(?:(\S+)(\s*)(=)(\s*))?(.+)(\()(.*)(\))(\s*)',
+             bygroups(Whitespace, Text, Whitespace, Punctuation,
+                      Whitespace, Name.Function, Punctuation, Text,
+                      Punctuation, Whitespace), '#pop'),
+            # function with no args
+            (r'(\s*)([a-zA-Z_]\w*)',
+             bygroups(Whitespace, Name.Function), '#pop'),
+        ],
+        'propattrs': [
+            (r'(\w+)(\s*)(=)(\s*)(\d+)',
+             bygroups(Name.Builtin, Whitespace, Punctuation, Whitespace,
+                      Number)),
+            (r'(\w+)(\s*)(=)(\s*)([a-zA-Z]\w*)',
+             bygroups(Name.Builtin, Whitespace, Punctuation, Whitespace,
+                      Keyword)),
+            (r',', Punctuation),
+            (r'\)', Punctuation, '#pop'),
+            (r'\s+', Whitespace),
+            (r'.', Text),
+        ],
+        'defprops': [
+            (r'%\{\s*\n', Comment.Multiline, 'blockcomment'),
+            (r'%.*$', Comment),
+            (r'(?.
+    """
+    name = 'Matlab session'
+    aliases = ['matlabsession']
+    url = 'https://www.mathworks.com/products/matlab.html'
+    version_added = '0.10'
+    _example = "matlabsession/matlabsession_sample.txt"
+
+    def get_tokens_unprocessed(self, text):
+        mlexer = MatlabLexer(**self.options)
+
+        curcode = ''
+        insertions = []
+        continuation = False
+
+        for match in line_re.finditer(text):
+            line = match.group()
+
+            if line.startswith('>> '):
+                insertions.append((len(curcode),
+                                   [(0, Generic.Prompt, line[:3])]))
+                curcode += line[3:]
+
+            elif line.startswith('>>'):
+                insertions.append((len(curcode),
+                                   [(0, Generic.Prompt, line[:2])]))
+                curcode += line[2:]
+
+            elif line.startswith('???'):
+
+                idx = len(curcode)
+
+                # without is showing error on same line as before...?
+                # line = "\n" + line
+                token = (0, Generic.Traceback, line)
+                insertions.append((idx, [token]))
+            elif continuation and insertions:
+                # line_start is the length of the most recent prompt symbol
+                line_start = len(insertions[-1][-1][-1])
+                # Set leading spaces with the length of the prompt to be a generic prompt
+                # This keeps code aligned when prompts are removed, say with some Javascript
+                if line.startswith(' '*line_start):
+                    insertions.append(
+                        (len(curcode), [(0, Generic.Prompt, line[:line_start])]))
+                    curcode += line[line_start:]
+                else:
+                    curcode += line
+            else:
+                if curcode:
+                    yield from do_insertions(
+                        insertions, mlexer.get_tokens_unprocessed(curcode))
+                    curcode = ''
+                    insertions = []
+
+                yield match.start(), Generic.Output, line
+
+            # Does not allow continuation if a comment is included after the ellipses.
+            # Continues any line that ends with ..., even comments (lines that start with %)
+            if line.strip().endswith('...'):
+                continuation = True
+            else:
+                continuation = False
+
+        if curcode:  # or item:
+            yield from do_insertions(
+                insertions, mlexer.get_tokens_unprocessed(curcode))
+
+
+class OctaveLexer(RegexLexer):
+    """
+    For GNU Octave source code.
+    """
+    name = 'Octave'
+    url = 'https://www.gnu.org/software/octave/index'
+    aliases = ['octave']
+    filenames = ['*.m']
+    mimetypes = ['text/octave']
+    version_added = '1.5'
+
+    # These lists are generated automatically.
+    # Run the following in bash shell:
+    #
+    # First dump all of the Octave manual into a plain text file:
+    #
+    #   $ info octave --subnodes -o octave-manual
+    #
+    # Now grep through it:
+
+    # for i in \
+    #     "Built-in Function" "Command" "Function File" \
+    #     "Loadable Function" "Mapping Function";
+    # do
+    #     perl -e '@name = qw('"$i"');
+    #              print lc($name[0]),"_kw = [\n"';
+    #
+    #     perl -n -e 'print "\"$1\",\n" if /-- '"$i"': .* (\w*) \(/;' \
+    #         octave-manual | sort | uniq ;
+    #     echo "]" ;
+    #     echo;
+    # done
+
+    # taken from Octave Mercurial changeset 8cc154f45e37 (30-jan-2011)
+
+    builtin_kw = (
+        "addlistener", "addpath", "addproperty", "all",
+        "and", "any", "argnames", "argv", "assignin",
+        "atexit", "autoload",
+        "available_graphics_toolkits", "beep_on_error",
+        "bitand", "bitmax", "bitor", "bitshift", "bitxor",
+        "cat", "cell", "cellstr", "char", "class", "clc",
+        "columns", "command_line_path",
+        "completion_append_char", "completion_matches",
+        "complex", "confirm_recursive_rmdir", "cputime",
+        "crash_dumps_octave_core", "ctranspose", "cumprod",
+        "cumsum", "debug_on_error", "debug_on_interrupt",
+        "debug_on_warning", "default_save_options",
+        "dellistener", "diag", "diff", "disp",
+        "doc_cache_file", "do_string_escapes", "double",
+        "drawnow", "e", "echo_executing_commands", "eps",
+        "eq", "errno", "errno_list", "error", "eval",
+        "evalin", "exec", "exist", "exit", "eye", "false",
+        "fclear", "fclose", "fcntl", "fdisp", "feof",
+        "ferror", "feval", "fflush", "fgetl", "fgets",
+        "fieldnames", "file_in_loadpath", "file_in_path",
+        "filemarker", "filesep", "find_dir_in_path",
+        "fixed_point_format", "fnmatch", "fopen", "fork",
+        "formula", "fprintf", "fputs", "fread", "freport",
+        "frewind", "fscanf", "fseek", "fskipl", "ftell",
+        "functions", "fwrite", "ge", "genpath", "get",
+        "getegid", "getenv", "geteuid", "getgid",
+        "getpgrp", "getpid", "getppid", "getuid", "glob",
+        "gt", "gui_mode", "history_control",
+        "history_file", "history_size",
+        "history_timestamp_format_string", "home",
+        "horzcat", "hypot", "ifelse",
+        "ignore_function_time_stamp", "inferiorto",
+        "info_file", "info_program", "inline", "input",
+        "intmax", "intmin", "ipermute",
+        "is_absolute_filename", "isargout", "isbool",
+        "iscell", "iscellstr", "ischar", "iscomplex",
+        "isempty", "isfield", "isfloat", "isglobal",
+        "ishandle", "isieee", "isindex", "isinteger",
+        "islogical", "ismatrix", "ismethod", "isnull",
+        "isnumeric", "isobject", "isreal",
+        "is_rooted_relative_filename", "issorted",
+        "isstruct", "isvarname", "kbhit", "keyboard",
+        "kill", "lasterr", "lasterror", "lastwarn",
+        "ldivide", "le", "length", "link", "linspace",
+        "logical", "lstat", "lt", "make_absolute_filename",
+        "makeinfo_program", "max_recursion_depth", "merge",
+        "methods", "mfilename", "minus", "mislocked",
+        "mkdir", "mkfifo", "mkstemp", "mldivide", "mlock",
+        "mouse_wheel_zoom", "mpower", "mrdivide", "mtimes",
+        "munlock", "nargin", "nargout",
+        "native_float_format", "ndims", "ne", "nfields",
+        "nnz", "norm", "not", "numel", "nzmax",
+        "octave_config_info", "octave_core_file_limit",
+        "octave_core_file_name",
+        "octave_core_file_options", "ones", "or",
+        "output_max_field_width", "output_precision",
+        "page_output_immediately", "page_screen_output",
+        "path", "pathsep", "pause", "pclose", "permute",
+        "pi", "pipe", "plus", "popen", "power",
+        "print_empty_dimensions", "printf",
+        "print_struct_array_contents", "prod",
+        "program_invocation_name", "program_name",
+        "putenv", "puts", "pwd", "quit", "rats", "rdivide",
+        "readdir", "readlink", "read_readline_init_file",
+        "realmax", "realmin", "rehash", "rename",
+        "repelems", "re_read_readline_init_file", "reset",
+        "reshape", "resize", "restoredefaultpath",
+        "rethrow", "rmdir", "rmfield", "rmpath", "rows",
+        "save_header_format_string", "save_precision",
+        "saving_history", "scanf", "set", "setenv",
+        "shell_cmd", "sighup_dumps_octave_core",
+        "sigterm_dumps_octave_core", "silent_functions",
+        "single", "size", "size_equal", "sizemax",
+        "sizeof", "sleep", "source", "sparse_auto_mutate",
+        "split_long_rows", "sprintf", "squeeze", "sscanf",
+        "stat", "stderr", "stdin", "stdout", "strcmp",
+        "strcmpi", "string_fill_char", "strncmp",
+        "strncmpi", "struct", "struct_levels_to_print",
+        "strvcat", "subsasgn", "subsref", "sum", "sumsq",
+        "superiorto", "suppress_verbose_help_message",
+        "symlink", "system", "tic", "tilde_expand",
+        "times", "tmpfile", "tmpnam", "toc", "toupper",
+        "transpose", "true", "typeinfo", "umask", "uminus",
+        "uname", "undo_string_escapes", "unlink", "uplus",
+        "upper", "usage", "usleep", "vec", "vectorize",
+        "vertcat", "waitpid", "warning", "warranty",
+        "whos_line_format", "yes_or_no", "zeros",
+        "inf", "Inf", "nan", "NaN")
+
+    command_kw = ("close", "load", "who", "whos")
+
+    function_kw = (
+        "accumarray", "accumdim", "acosd", "acotd",
+        "acscd", "addtodate", "allchild", "ancestor",
+        "anova", "arch_fit", "arch_rnd", "arch_test",
+        "area", "arma_rnd", "arrayfun", "ascii", "asctime",
+        "asecd", "asind", "assert", "atand",
+        "autoreg_matrix", "autumn", "axes", "axis", "bar",
+        "barh", "bartlett", "bartlett_test", "beep",
+        "betacdf", "betainv", "betapdf", "betarnd",
+        "bicgstab", "bicubic", "binary", "binocdf",
+        "binoinv", "binopdf", "binornd", "bitcmp",
+        "bitget", "bitset", "blackman", "blanks",
+        "blkdiag", "bone", "box", "brighten", "calendar",
+        "cast", "cauchy_cdf", "cauchy_inv", "cauchy_pdf",
+        "cauchy_rnd", "caxis", "celldisp", "center", "cgs",
+        "chisquare_test_homogeneity",
+        "chisquare_test_independence", "circshift", "cla",
+        "clabel", "clf", "clock", "cloglog", "closereq",
+        "colon", "colorbar", "colormap", "colperm",
+        "comet", "common_size", "commutation_matrix",
+        "compan", "compare_versions", "compass",
+        "computer", "cond", "condest", "contour",
+        "contourc", "contourf", "contrast", "conv",
+        "convhull", "cool", "copper", "copyfile", "cor",
+        "corrcoef", "cor_test", "cosd", "cotd", "cov",
+        "cplxpair", "cross", "cscd", "cstrcat", "csvread",
+        "csvwrite", "ctime", "cumtrapz", "curl", "cut",
+        "cylinder", "date", "datenum", "datestr",
+        "datetick", "datevec", "dblquad", "deal",
+        "deblank", "deconv", "delaunay", "delaunayn",
+        "delete", "demo", "detrend", "diffpara", "diffuse",
+        "dir", "discrete_cdf", "discrete_inv",
+        "discrete_pdf", "discrete_rnd", "display",
+        "divergence", "dlmwrite", "dos", "dsearch",
+        "dsearchn", "duplication_matrix", "durbinlevinson",
+        "ellipsoid", "empirical_cdf", "empirical_inv",
+        "empirical_pdf", "empirical_rnd", "eomday",
+        "errorbar", "etime", "etreeplot", "example",
+        "expcdf", "expinv", "expm", "exppdf", "exprnd",
+        "ezcontour", "ezcontourf", "ezmesh", "ezmeshc",
+        "ezplot", "ezpolar", "ezsurf", "ezsurfc", "factor",
+        "factorial", "fail", "fcdf", "feather", "fftconv",
+        "fftfilt", "fftshift", "figure", "fileattrib",
+        "fileparts", "fill", "findall", "findobj",
+        "findstr", "finv", "flag", "flipdim", "fliplr",
+        "flipud", "fpdf", "fplot", "fractdiff", "freqz",
+        "freqz_plot", "frnd", "fsolve",
+        "f_test_regression", "ftp", "fullfile", "fzero",
+        "gamcdf", "gaminv", "gampdf", "gamrnd", "gca",
+        "gcbf", "gcbo", "gcf", "genvarname", "geocdf",
+        "geoinv", "geopdf", "geornd", "getfield", "ginput",
+        "glpk", "gls", "gplot", "gradient",
+        "graphics_toolkit", "gray", "grid", "griddata",
+        "griddatan", "gtext", "gunzip", "gzip", "hadamard",
+        "hamming", "hankel", "hanning", "hggroup",
+        "hidden", "hilb", "hist", "histc", "hold", "hot",
+        "hotelling_test", "housh", "hsv", "hurst",
+        "hygecdf", "hygeinv", "hygepdf", "hygernd",
+        "idivide", "ifftshift", "image", "imagesc",
+        "imfinfo", "imread", "imshow", "imwrite", "index",
+        "info", "inpolygon", "inputname", "interpft",
+        "interpn", "intersect", "invhilb", "iqr", "isa",
+        "isdefinite", "isdir", "is_duplicate_entry",
+        "isequal", "isequalwithequalnans", "isfigure",
+        "ishermitian", "ishghandle", "is_leap_year",
+        "isletter", "ismac", "ismember", "ispc", "isprime",
+        "isprop", "isscalar", "issquare", "isstrprop",
+        "issymmetric", "isunix", "is_valid_file_id",
+        "isvector", "jet", "kendall",
+        "kolmogorov_smirnov_cdf",
+        "kolmogorov_smirnov_test", "kruskal_wallis_test",
+        "krylov", "kurtosis", "laplace_cdf", "laplace_inv",
+        "laplace_pdf", "laplace_rnd", "legend", "legendre",
+        "license", "line", "linkprop", "list_primes",
+        "loadaudio", "loadobj", "logistic_cdf",
+        "logistic_inv", "logistic_pdf", "logistic_rnd",
+        "logit", "loglog", "loglogerr", "logm", "logncdf",
+        "logninv", "lognpdf", "lognrnd", "logspace",
+        "lookfor", "ls_command", "lsqnonneg", "magic",
+        "mahalanobis", "manova", "matlabroot",
+        "mcnemar_test", "mean", "meansq", "median", "menu",
+        "mesh", "meshc", "meshgrid", "meshz", "mexext",
+        "mget", "mkpp", "mode", "moment", "movefile",
+        "mpoles", "mput", "namelengthmax", "nargchk",
+        "nargoutchk", "nbincdf", "nbininv", "nbinpdf",
+        "nbinrnd", "nchoosek", "ndgrid", "newplot", "news",
+        "nonzeros", "normcdf", "normest", "norminv",
+        "normpdf", "normrnd", "now", "nthroot", "null",
+        "ocean", "ols", "onenormest", "optimget",
+        "optimset", "orderfields", "orient", "orth",
+        "pack", "pareto", "parseparams", "pascal", "patch",
+        "pathdef", "pcg", "pchip", "pcolor", "pcr",
+        "peaks", "periodogram", "perl", "perms", "pie",
+        "pink", "planerot", "playaudio", "plot",
+        "plotmatrix", "plotyy", "poisscdf", "poissinv",
+        "poisspdf", "poissrnd", "polar", "poly",
+        "polyaffine", "polyarea", "polyderiv", "polyfit",
+        "polygcd", "polyint", "polyout", "polyreduce",
+        "polyval", "polyvalm", "postpad", "powerset",
+        "ppder", "ppint", "ppjumps", "ppplot", "ppval",
+        "pqpnonneg", "prepad", "primes", "print",
+        "print_usage", "prism", "probit", "qp", "qqplot",
+        "quadcc", "quadgk", "quadl", "quadv", "quiver",
+        "qzhess", "rainbow", "randi", "range", "rank",
+        "ranks", "rat", "reallog", "realpow", "realsqrt",
+        "record", "rectangle_lw", "rectangle_sw",
+        "rectint", "refresh", "refreshdata",
+        "regexptranslate", "repmat", "residue", "ribbon",
+        "rindex", "roots", "rose", "rosser", "rotdim",
+        "rref", "run", "run_count", "rundemos", "run_test",
+        "runtests", "saveas", "saveaudio", "saveobj",
+        "savepath", "scatter", "secd", "semilogx",
+        "semilogxerr", "semilogy", "semilogyerr",
+        "setaudio", "setdiff", "setfield", "setxor",
+        "shading", "shift", "shiftdim", "sign_test",
+        "sinc", "sind", "sinetone", "sinewave", "skewness",
+        "slice", "sombrero", "sortrows", "spaugment",
+        "spconvert", "spdiags", "spearman", "spectral_adf",
+        "spectral_xdf", "specular", "speed", "spencer",
+        "speye", "spfun", "sphere", "spinmap", "spline",
+        "spones", "sprand", "sprandn", "sprandsym",
+        "spring", "spstats", "spy", "sqp", "stairs",
+        "statistics", "std", "stdnormal_cdf",
+        "stdnormal_inv", "stdnormal_pdf", "stdnormal_rnd",
+        "stem", "stft", "strcat", "strchr", "strjust",
+        "strmatch", "strread", "strsplit", "strtok",
+        "strtrim", "strtrunc", "structfun", "studentize",
+        "subplot", "subsindex", "subspace", "substr",
+        "substruct", "summer", "surf", "surface", "surfc",
+        "surfl", "surfnorm", "svds", "swapbytes",
+        "sylvester_matrix", "symvar", "synthesis", "table",
+        "tand", "tar", "tcdf", "tempdir", "tempname",
+        "test", "text", "textread", "textscan", "tinv",
+        "title", "toeplitz", "tpdf", "trace", "trapz",
+        "treelayout", "treeplot", "triangle_lw",
+        "triangle_sw", "tril", "trimesh", "triplequad",
+        "triplot", "trisurf", "triu", "trnd", "tsearchn",
+        "t_test", "t_test_regression", "type", "unidcdf",
+        "unidinv", "unidpdf", "unidrnd", "unifcdf",
+        "unifinv", "unifpdf", "unifrnd", "union", "unique",
+        "unix", "unmkpp", "unpack", "untabify", "untar",
+        "unwrap", "unzip", "u_test", "validatestring",
+        "vander", "var", "var_test", "vech", "ver",
+        "version", "view", "voronoi", "voronoin",
+        "waitforbuttonpress", "wavread", "wavwrite",
+        "wblcdf", "wblinv", "wblpdf", "wblrnd", "weekday",
+        "welch_test", "what", "white", "whitebg",
+        "wienrnd", "wilcoxon_test", "wilkinson", "winter",
+        "xlabel", "xlim", "ylabel", "yulewalker", "zip",
+        "zlabel", "z_test")
+
+    loadable_kw = (
+        "airy", "amd", "balance", "besselh", "besseli",
+        "besselj", "besselk", "bessely", "bitpack",
+        "bsxfun", "builtin", "ccolamd", "cellfun",
+        "cellslices", "chol", "choldelete", "cholinsert",
+        "cholinv", "cholshift", "cholupdate", "colamd",
+        "colloc", "convhulln", "convn", "csymamd",
+        "cummax", "cummin", "daspk", "daspk_options",
+        "dasrt", "dasrt_options", "dassl", "dassl_options",
+        "dbclear", "dbdown", "dbstack", "dbstatus",
+        "dbstop", "dbtype", "dbup", "dbwhere", "det",
+        "dlmread", "dmperm", "dot", "eig", "eigs",
+        "endgrent", "endpwent", "etree", "fft", "fftn",
+        "fftw", "filter", "find", "full", "gcd",
+        "getgrent", "getgrgid", "getgrnam", "getpwent",
+        "getpwnam", "getpwuid", "getrusage", "givens",
+        "gmtime", "gnuplot_binary", "hess", "ifft",
+        "ifftn", "inv", "isdebugmode", "issparse", "kron",
+        "localtime", "lookup", "lsode", "lsode_options",
+        "lu", "luinc", "luupdate", "matrix_type", "max",
+        "min", "mktime", "pinv", "qr", "qrdelete",
+        "qrinsert", "qrshift", "qrupdate", "quad",
+        "quad_options", "qz", "rand", "rande", "randg",
+        "randn", "randp", "randperm", "rcond", "regexp",
+        "regexpi", "regexprep", "schur", "setgrent",
+        "setpwent", "sort", "spalloc", "sparse", "spparms",
+        "sprank", "sqrtm", "strfind", "strftime",
+        "strptime", "strrep", "svd", "svd_driver", "syl",
+        "symamd", "symbfact", "symrcm", "time", "tsearch",
+        "typecast", "urlread", "urlwrite")
+
+    mapping_kw = (
+        "abs", "acos", "acosh", "acot", "acoth", "acsc",
+        "acsch", "angle", "arg", "asec", "asech", "asin",
+        "asinh", "atan", "atanh", "beta", "betainc",
+        "betaln", "bincoeff", "cbrt", "ceil", "conj", "cos",
+        "cosh", "cot", "coth", "csc", "csch", "erf", "erfc",
+        "erfcx", "erfinv", "exp", "finite", "fix", "floor",
+        "fmod", "gamma", "gammainc", "gammaln", "imag",
+        "isalnum", "isalpha", "isascii", "iscntrl",
+        "isdigit", "isfinite", "isgraph", "isinf",
+        "islower", "isna", "isnan", "isprint", "ispunct",
+        "isspace", "isupper", "isxdigit", "lcm", "lgamma",
+        "log", "lower", "mod", "real", "rem", "round",
+        "roundb", "sec", "sech", "sign", "sin", "sinh",
+        "sqrt", "tan", "tanh", "toascii", "tolower", "xor")
+
+    builtin_consts = (
+        "EDITOR", "EXEC_PATH", "I", "IMAGE_PATH", "NA",
+        "OCTAVE_HOME", "OCTAVE_VERSION", "PAGER",
+        "PAGER_FLAGS", "SEEK_CUR", "SEEK_END", "SEEK_SET",
+        "SIG", "S_ISBLK", "S_ISCHR", "S_ISDIR", "S_ISFIFO",
+        "S_ISLNK", "S_ISREG", "S_ISSOCK", "WCONTINUE",
+        "WCOREDUMP", "WEXITSTATUS", "WIFCONTINUED",
+        "WIFEXITED", "WIFSIGNALED", "WIFSTOPPED", "WNOHANG",
+        "WSTOPSIG", "WTERMSIG", "WUNTRACED")
+
+    tokens = {
+        'root': [
+            (r'%\{\s*\n', Comment.Multiline, 'percentblockcomment'),
+            (r'#\{\s*\n', Comment.Multiline, 'hashblockcomment'),
+            (r'[%#].*$', Comment),
+            (r'^\s*function\b', Keyword, 'deffunc'),
+
+            # from 'iskeyword' on hg changeset 8cc154f45e37
+            (words((
+                '__FILE__', '__LINE__', 'break', 'case', 'catch', 'classdef',
+                'continue', 'do', 'else', 'elseif', 'end', 'end_try_catch',
+                'end_unwind_protect', 'endclassdef', 'endevents', 'endfor',
+                'endfunction', 'endif', 'endmethods', 'endproperties', 'endswitch',
+                'endwhile', 'events', 'for', 'function', 'get', 'global', 'if',
+                'methods', 'otherwise', 'persistent', 'properties', 'return',
+                'set', 'static', 'switch', 'try', 'until', 'unwind_protect',
+                'unwind_protect_cleanup', 'while'), suffix=r'\b'),
+             Keyword),
+
+            (words(builtin_kw + command_kw + function_kw + loadable_kw + mapping_kw,
+                   suffix=r'\b'),  Name.Builtin),
+
+            (words(builtin_consts, suffix=r'\b'), Name.Constant),
+
+            # operators in Octave but not Matlab:
+            (r'-=|!=|!|/=|--', Operator),
+            # operators:
+            (r'-|==|~=|<|>|<=|>=|&&|&|~|\|\|?', Operator),
+            # operators in Octave but not Matlab requiring escape for re:
+            (r'\*=|\+=|\^=|\/=|\\=|\*\*|\+\+|\.\*\*', Operator),
+            # operators requiring escape for re:
+            (r'\.\*|\*|\+|\.\^|\^|\.\\|\.\/|\/|\\', Operator),
+
+
+            # punctuation:
+            (r'[\[\](){}:@.,]', Punctuation),
+            (r'=|:|;', Punctuation),
+
+            (r'"[^"]*"', String),
+
+            (r'(\d+\.\d*|\d*\.\d+)([eEf][+-]?[0-9]+)?', Number.Float),
+            (r'\d+[eEf][+-]?[0-9]+', Number.Float),
+            (r'\d+', Number.Integer),
+
+            # quote can be transpose, instead of string:
+            # (not great, but handles common cases...)
+            (r'(?<=[\w)\].])\'+', Operator),
+            (r'(?|<=|>=|&&|&|~|\|\|?', Operator),
+            # operators requiring escape for re:
+            (r'\.\*|\*|\+|\.\^|\^|\.\\|\.\/|\/|\\', Operator),
+
+            # punctuation:
+            (r'[\[\](){}@.,=:;]+', Punctuation),
+
+            (r'"[^"]*"', String),
+
+            # quote can be transpose, instead of string:
+            # (not great, but handles common cases...)
+            (r'(?<=[\w)\].])\'+', Operator),
+            (r'(?', r'<', r'|', r'!', r"'")
+
+    operator_words = ('and', 'or', 'not')
+
+    tokens = {
+        'root': [
+            (r'/\*', Comment.Multiline, 'comment'),
+            (r'"(?:[^"\\]|\\.)*"', String),
+            (r'\(|\)|\[|\]|\{|\}', Punctuation),
+            (r'[,;$]', Punctuation),
+            (words (constants), Name.Constant),
+            (words (keywords), Keyword),
+            (words (operators), Operator),
+            (words (operator_words), Operator.Word),
+            (r'''(?x)
+              ((?:[a-zA-Z_#][\w#]*|`[^`]*`)
+              (?:::[a-zA-Z_#][\w#]*|`[^`]*`)*)(\s*)([(])''',
+             bygroups(Name.Function, Text.Whitespace, Punctuation)),
+            (r'''(?x)
+              (?:[a-zA-Z_#%][\w#%]*|`[^`]*`)
+              (?:::[a-zA-Z_#%][\w#%]*|`[^`]*`)*''', Name.Variable),
+            (r'[-+]?(\d*\.\d+([bdefls][-+]?\d+)?|\d+(\.\d*)?[bdefls][-+]?\d+)', Number.Float),
+            (r'[-+]?\d+', Number.Integer),
+            (r'\s+', Text.Whitespace),
+            (r'.', Text)
+        ],
+        'comment': [
+            (r'[^*/]+', Comment.Multiline),
+            (r'/\*', Comment.Multiline, '#push'),
+            (r'\*/', Comment.Multiline, '#pop'),
+            (r'[*/]', Comment.Multiline)
+        ]
+    }
+
+    def analyse_text (text):
+        strength = 0.0
+        # Input expression terminator.
+        if re.search (r'\$\s*$', text, re.MULTILINE):
+            strength += 0.05
+        # Function definition operator.
+        if ':=' in text:
+            strength += 0.02
+        return strength
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/meson.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/meson.py
new file mode 100644
index 00000000..6f2c6da3
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/meson.py
@@ -0,0 +1,139 @@
+"""
+    pygments.lexers.meson
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    Pygments lexer for the Meson build system
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, words, include
+from pygments.token import Comment, Name, Number, Punctuation, Operator, \
+    Keyword, String, Whitespace
+
+__all__ = ['MesonLexer']
+
+
+class MesonLexer(RegexLexer):
+    """Meson language lexer.
+
+    The grammar definition use to transcribe the syntax was retrieved from
+    https://mesonbuild.com/Syntax.html#grammar for version 0.58.
+    Some of those definitions are improperly transcribed, so the Meson++
+    implementation was also checked: https://github.com/dcbaker/meson-plus-plus.
+    """
+
+    # TODO String interpolation @VARNAME@ inner matches
+    # TODO keyword_arg: value inner matches
+
+    name = 'Meson'
+    url = 'https://mesonbuild.com/'
+    aliases = ['meson', 'meson.build']
+    filenames = ['meson.build', 'meson_options.txt']
+    mimetypes = ['text/x-meson']
+    version_added = '2.10'
+
+    tokens = {
+        'root': [
+            (r'#.*?$', Comment),
+            (r"'''.*'''", String.Single),
+            (r'[1-9][0-9]*', Number.Integer),
+            (r'0o[0-7]+', Number.Oct),
+            (r'0x[a-fA-F0-9]+', Number.Hex),
+            include('string'),
+            include('keywords'),
+            include('expr'),
+            (r'[a-zA-Z_][a-zA-Z_0-9]*', Name),
+            (r'\s+', Whitespace),
+        ],
+        'string': [
+            (r"[']{3}([']{0,2}([^\\']|\\(.|\n)))*[']{3}", String),
+            (r"'.*?(?`_.
+    """
+
+    name = "MCFunction"
+    url = "https://minecraft.wiki/w/Commands"
+    aliases = ["mcfunction", "mcf"]
+    filenames = ["*.mcfunction"]
+    mimetypes = ["text/mcfunction"]
+    version_added = '2.12'
+
+    # Used to denotate the start of a block comment, borrowed from Github's mcfunction
+    _block_comment_prefix = "[>!]"
+
+    tokens = {
+        "root": [
+            include("names"),
+            include("comments"),
+            include("literals"),
+            include("whitespace"),
+            include("property"),
+            include("operators"),
+            include("selectors"),
+        ],
+
+        "names": [
+            # The start of a command (either beginning of line OR after the run keyword)
+            #  We don't encode a list of keywords since mods, plugins, or even pre-processors
+            #  may add new commands, so we have a 'close-enough' regex which catches them.
+            (r"^(\s*)([a-z_]+)", bygroups(Whitespace, Name.Builtin)),
+            (r"(?<=run)\s+[a-z_]+", Name.Builtin),
+
+            # UUID
+            (r"\b[0-9a-fA-F]+(?:-[0-9a-fA-F]+){4}\b", Name.Variable),
+            include("resource-name"),
+            # normal command names and scoreboards
+            #  there's no way to know the differences unfortuntely
+            (r"[A-Za-z_][\w.#%$]+", Keyword.Constant),
+            (r"[#%$][\w.#%$]+", Name.Variable.Magic),
+        ],
+
+        "resource-name": [
+            # resource names have to be lowercase
+            (r"#?[a-z_][a-z_.-]*:[a-z0-9_./-]+", Name.Function),
+            # similar to above except optional `:``
+            #  a `/` must be present "somewhere"
+            (r"#?[a-z0-9_\.\-]+\/[a-z0-9_\.\-\/]+", Name.Function),
+        ],
+
+        "whitespace": [
+            (r"\s+", Whitespace),
+        ],
+
+        "comments": [
+            (rf"^\s*(#{_block_comment_prefix})", Comment.Multiline,
+             ("comments.block", "comments.block.emphasized")),
+            (r"#.*$", Comment.Single),
+        ],
+        "comments.block": [
+            (rf"^\s*#{_block_comment_prefix}", Comment.Multiline,
+             "comments.block.emphasized"),
+            (r"^\s*#", Comment.Multiline, "comments.block.normal"),
+            default("#pop"),
+        ],
+        "comments.block.normal": [
+            include("comments.block.special"),
+            (r"\S+", Comment.Multiline),
+            (r"\n", Text, "#pop"),
+            include("whitespace"),
+        ],
+        "comments.block.emphasized": [
+            include("comments.block.special"),
+            (r"\S+", String.Doc),
+            (r"\n", Text, "#pop"),
+            include("whitespace"),
+        ],
+        "comments.block.special": [
+            # Params
+            (r"@\S+", Name.Decorator),
+
+            include("resource-name"),
+
+            # Scoreboard player names
+            (r"[#%$][\w.#%$]+", Name.Variable.Magic),
+        ],
+
+        "operators": [
+            (r"[\-~%^?!+*<>\\/|&=.]", Operator),
+        ],
+
+        "literals": [
+            (r"\.\.", Literal),
+            (r"(true|false)", Keyword.Pseudo),
+
+            # these are like unquoted strings and appear in many places
+            (r"[A-Za-z_]+", Name.Variable.Class),
+
+            (r"[0-7]b", Number.Byte),
+            (r"[+-]?\d*\.?\d+([eE]?[+-]?\d+)?[df]?\b", Number.Float),
+            (r"[+-]?\d+\b", Number.Integer),
+            (r'"', String.Double, "literals.string-double"),
+            (r"'", String.Single, "literals.string-single"),
+        ],
+        "literals.string-double": [
+            (r"\\.", String.Escape),
+            (r'[^\\"\n]+', String.Double),
+            (r'"', String.Double, "#pop"),
+        ],
+        "literals.string-single": [
+            (r"\\.", String.Escape),
+            (r"[^\\'\n]+", String.Single),
+            (r"'", String.Single, "#pop"),
+        ],
+
+        "selectors": [
+            (r"@[a-z]", Name.Variable),
+        ],
+
+
+        ## Generic Property Container
+        # There are several, differing instances where the language accepts
+        #  specific contained keys or contained key, value pairings.
+        #
+        # Property Maps:
+        # - Starts with either `[` or `{`
+        # - Key separated by `:` or `=`
+        # - Deliminated by `,`
+        #
+        # Property Lists:
+        # - Starts with `[`
+        # - Deliminated by `,`
+        #
+        # For simplicity, these patterns match a generic, nestable structure
+        #  which follow a key, value pattern. For normal lists, there's only keys.
+        # This allow some "illegal" structures, but we'll accept those for
+        #  sake of simplicity
+        #
+        # Examples:
+        # - `[facing=up, powered=true]` (blockstate)
+        # - `[name="hello world", nbt={key: 1b}]` (selector + nbt)
+        # - `[{"text": "value"}, "literal"]` (json)
+        ##
+        "property": [
+            # This state gets included in root and also several substates
+            # We do this to shortcut the starting of new properties
+            #  within other properties. Lists can have sublists and compounds
+            #  and values can start a new property (see the `difficult_1.txt`
+            #  snippet).
+            (r"\{", Punctuation, ("property.curly", "property.key")),
+            (r"\[", Punctuation, ("property.square", "property.key")),
+        ],
+        "property.curly": [
+            include("whitespace"),
+            include("property"),
+            (r"\}", Punctuation, "#pop"),
+        ],
+        "property.square": [
+            include("whitespace"),
+            include("property"),
+            (r"\]", Punctuation, "#pop"),
+
+            # lists can have sequences of items
+            (r",", Punctuation),
+        ],
+        "property.key": [
+            include("whitespace"),
+
+            # resource names (for advancements)
+            #  can omit `:` to default `minecraft:`
+            # must check if there is a future equals sign if `:` is in the name
+            (r"#?[a-z_][a-z_\.\-]*\:[a-z0-9_\.\-/]+(?=\s*\=)", Name.Attribute, "property.delimiter"),
+            (r"#?[a-z_][a-z0-9_\.\-/]+", Name.Attribute, "property.delimiter"),
+
+            # unquoted NBT key
+            (r"[A-Za-z_\-\+]+", Name.Attribute, "property.delimiter"),
+
+            # quoted JSON or NBT key
+            (r'"', Name.Attribute, "property.delimiter", "literals.string-double"),
+            (r"'", Name.Attribute, "property.delimiter", "literals.string-single"),
+
+            # index for a list
+            (r"-?\d+", Number.Integer, "property.delimiter"),
+
+            default("#pop"),
+        ],
+        "property.key.string-double": [
+            (r"\\.", String.Escape),
+            (r'[^\\"\n]+', Name.Attribute),
+            (r'"', Name.Attribute, "#pop"),
+        ],
+        "property.key.string-single": [
+            (r"\\.", String.Escape),
+            (r"[^\\'\n]+", Name.Attribute),
+            (r"'", Name.Attribute, "#pop"),
+        ],
+        "property.delimiter": [
+            include("whitespace"),
+
+            (r"[:=]!?", Punctuation, "property.value"),
+            (r",", Punctuation),
+
+            default("#pop"),
+        ],
+        "property.value": [
+            include("whitespace"),
+
+            # unquoted resource names are valid literals here
+            (r"#?[a-z_][a-z_\.\-]*\:[a-z0-9_\.\-/]+", Name.Tag),
+            (r"#?[a-z_][a-z0-9_\.\-/]+", Name.Tag),
+
+            include("literals"),
+            include("property"),
+
+            default("#pop"),
+        ],
+    }
+
+
+class MCSchemaLexer(RegexLexer):
+    """Lexer for Minecraft Add-ons data Schemas, an interface structure standard used in Minecraft
+    """
+
+    name = 'MCSchema'
+    url = 'https://learn.microsoft.com/en-us/minecraft/creator/reference/content/schemasreference/'
+    aliases = ['mcschema']
+    filenames = ['*.mcschema']
+    mimetypes = ['text/mcschema']
+    version_added = '2.14'
+
+    tokens = {
+        'commentsandwhitespace': [
+            (r'\s+', Whitespace),
+            (r'//.*?$', Comment.Single),
+            (r'/\*.*?\*/', Comment.Multiline)
+        ],
+        'slashstartsregex': [
+            include('commentsandwhitespace'),
+            (r'/(\\.|[^[/\\\n]|\[(\\.|[^\]\\\n])*])+/'
+             r'([gimuysd]+\b|\B)', String.Regex, '#pop'),
+            (r'(?=/)', Text, ('#pop', 'badregex')),
+            default('#pop')
+        ],
+        'badregex': [
+            (r'\n', Whitespace, '#pop')
+        ],
+        'singlestring': [
+            (r'\\.', String.Escape),
+            (r"'", String.Single, '#pop'),
+            (r"[^\\']+", String.Single),
+        ],
+        'doublestring': [
+            (r'\\.', String.Escape),
+            (r'"', String.Double, '#pop'),
+            (r'[^\\"]+', String.Double),
+        ],
+        'root': [
+            (r'^(?=\s|/|', Comment, '#pop'),
+            (r'[^\-]+|-', Comment),
+        ],
+    }
+
+
+class ReasonLexer(RegexLexer):
+    """
+    For the ReasonML language.
+    """
+
+    name = 'ReasonML'
+    url = 'https://reasonml.github.io/'
+    aliases = ['reasonml', 'reason']
+    filenames = ['*.re', '*.rei']
+    mimetypes = ['text/x-reasonml']
+    version_added = '2.6'
+
+    keywords = (
+        'as', 'assert', 'begin', 'class', 'constraint', 'do', 'done', 'downto',
+        'else', 'end', 'exception', 'external', 'false', 'for', 'fun', 'esfun',
+        'function', 'functor', 'if', 'in', 'include', 'inherit', 'initializer', 'lazy',
+        'let', 'switch', 'module', 'pub', 'mutable', 'new', 'nonrec', 'object', 'of',
+        'open', 'pri', 'rec', 'sig', 'struct', 'then', 'to', 'true', 'try',
+        'type', 'val', 'virtual', 'when', 'while', 'with',
+    )
+    keyopts = (
+        '!=', '#', '&', '&&', r'\(', r'\)', r'\*', r'\+', ',', '-',
+        r'-\.', '=>', r'\.', r'\.\.', r'\.\.\.', ':', '::', ':=', ':>', ';', ';;', '<',
+        '<-', '=', '>', '>]', r'>\}', r'\?', r'\?\?', r'\[', r'\[<', r'\[>',
+        r'\[\|', ']', '_', '`', r'\{', r'\{<', r'\|', r'\|\|', r'\|]', r'\}', '~'
+    )
+
+    operators = r'[!$%&*+\./:<=>?@^|~-]'
+    word_operators = ('and', 'asr', 'land', 'lor', 'lsl', 'lsr', 'lxor', 'mod', 'or')
+    prefix_syms = r'[!?~]'
+    infix_syms = r'[=<>@^|&+\*/$%-]'
+    primitives = ('unit', 'int', 'float', 'bool', 'string', 'char', 'list', 'array')
+
+    tokens = {
+        'escape-sequence': [
+            (r'\\[\\"\'ntbr]', String.Escape),
+            (r'\\[0-9]{3}', String.Escape),
+            (r'\\x[0-9a-fA-F]{2}', String.Escape),
+        ],
+        'root': [
+            (r'\s+', Text),
+            (r'false|true|\(\)|\[\]', Name.Builtin.Pseudo),
+            (r'\b([A-Z][\w\']*)(?=\s*\.)', Name.Namespace, 'dotted'),
+            (r'\b([A-Z][\w\']*)', Name.Class),
+            (r'//.*?\n', Comment.Single),
+            (r'\/\*(?!/)', Comment.Multiline, 'comment'),
+            (r'\b({})\b'.format('|'.join(keywords)), Keyword),
+            (r'({})'.format('|'.join(keyopts[::-1])), Operator.Word),
+            (rf'({infix_syms}|{prefix_syms})?{operators}', Operator),
+            (r'\b({})\b'.format('|'.join(word_operators)), Operator.Word),
+            (r'\b({})\b'.format('|'.join(primitives)), Keyword.Type),
+
+            (r"[^\W\d][\w']*", Name),
+
+            (r'-?\d[\d_]*(.[\d_]*)?([eE][+\-]?\d[\d_]*)', Number.Float),
+            (r'0[xX][\da-fA-F][\da-fA-F_]*', Number.Hex),
+            (r'0[oO][0-7][0-7_]*', Number.Oct),
+            (r'0[bB][01][01_]*', Number.Bin),
+            (r'\d[\d_]*', Number.Integer),
+
+            (r"'(?:(\\[\\\"'ntbr ])|(\\[0-9]{3})|(\\x[0-9a-fA-F]{2}))'",
+             String.Char),
+            (r"'.'", String.Char),
+            (r"'", Keyword),
+
+            (r'"', String.Double, 'string'),
+
+            (r'[~?][a-z][\w\']*:', Name.Variable),
+        ],
+        'comment': [
+            (r'[^/*]+', Comment.Multiline),
+            (r'\/\*', Comment.Multiline, '#push'),
+            (r'\*\/', Comment.Multiline, '#pop'),
+            (r'\*', Comment.Multiline),
+        ],
+        'string': [
+            (r'[^\\"]+', String.Double),
+            include('escape-sequence'),
+            (r'\\\n', String.Double),
+            (r'"', String.Double, '#pop'),
+        ],
+        'dotted': [
+            (r'\s+', Text),
+            (r'\.', Punctuation),
+            (r'[A-Z][\w\']*(?=\s*\.)', Name.Namespace),
+            (r'[A-Z][\w\']*', Name.Class, '#pop'),
+            (r'[a-z_][\w\']*', Name, '#pop'),
+            default('#pop'),
+        ],
+    }
+
+
+class FStarLexer(RegexLexer):
+    """
+    For the F* language.
+    """
+
+    name = 'FStar'
+    url = 'https://www.fstar-lang.org/'
+    aliases = ['fstar']
+    filenames = ['*.fst', '*.fsti']
+    mimetypes = ['text/x-fstar']
+    version_added = '2.7'
+
+    keywords = (
+        'abstract', 'attributes', 'noeq', 'unopteq', 'and'
+        'begin', 'by', 'default', 'effect', 'else', 'end', 'ensures',
+        'exception', 'exists', 'false', 'forall', 'fun', 'function', 'if',
+        'in', 'include', 'inline', 'inline_for_extraction', 'irreducible',
+        'logic', 'match', 'module', 'mutable', 'new', 'new_effect', 'noextract',
+        'of', 'open', 'opaque', 'private', 'range_of', 'reifiable',
+        'reify', 'reflectable', 'requires', 'set_range_of', 'sub_effect',
+        'synth', 'then', 'total', 'true', 'try', 'type', 'unfold', 'unfoldable',
+        'val', 'when', 'with', 'not'
+    )
+    decl_keywords = ('let', 'rec')
+    assume_keywords = ('assume', 'admit', 'assert', 'calc')
+    keyopts = (
+        r'~', r'-', r'/\\', r'\\/', r'<:', r'<@', r'\(\|', r'\|\)', r'#', r'u#',
+        r'&', r'\(', r'\)', r'\(\)', r',', r'~>', r'->', r'<-', r'<--', r'<==>',
+        r'==>', r'\.', r'\?', r'\?\.', r'\.\[', r'\.\(', r'\.\(\|', r'\.\[\|',
+        r'\{:pattern', r':', r'::', r':=', r';', r';;', r'=', r'%\[', r'!\{',
+        r'\[', r'\[@', r'\[\|', r'\|>', r'\]', r'\|\]', r'\{', r'\|', r'\}', r'\$'
+    )
+
+    operators = r'[!$%&*+\./:<=>?@^|~-]'
+    prefix_syms = r'[!?~]'
+    infix_syms = r'[=<>@^|&+\*/$%-]'
+    primitives = ('unit', 'int', 'float', 'bool', 'string', 'char', 'list', 'array')
+
+    tokens = {
+        'escape-sequence': [
+            (r'\\[\\"\'ntbr]', String.Escape),
+            (r'\\[0-9]{3}', String.Escape),
+            (r'\\x[0-9a-fA-F]{2}', String.Escape),
+        ],
+        'root': [
+            (r'\s+', Text),
+            (r'false|true|False|True|\(\)|\[\]', Name.Builtin.Pseudo),
+            (r'\b([A-Z][\w\']*)(?=\s*\.)', Name.Namespace, 'dotted'),
+            (r'\b([A-Z][\w\']*)', Name.Class),
+            (r'\(\*(?![)])', Comment, 'comment'),
+            (r'\/\/.+$', Comment),
+            (r'\b({})\b'.format('|'.join(keywords)), Keyword),
+            (r'\b({})\b'.format('|'.join(assume_keywords)), Name.Exception),
+            (r'\b({})\b'.format('|'.join(decl_keywords)), Keyword.Declaration),
+            (r'({})'.format('|'.join(keyopts[::-1])), Operator),
+            (rf'({infix_syms}|{prefix_syms})?{operators}', Operator),
+            (r'\b({})\b'.format('|'.join(primitives)), Keyword.Type),
+
+            (r"[^\W\d][\w']*", Name),
+
+            (r'-?\d[\d_]*(.[\d_]*)?([eE][+\-]?\d[\d_]*)', Number.Float),
+            (r'0[xX][\da-fA-F][\da-fA-F_]*', Number.Hex),
+            (r'0[oO][0-7][0-7_]*', Number.Oct),
+            (r'0[bB][01][01_]*', Number.Bin),
+            (r'\d[\d_]*', Number.Integer),
+
+            (r"'(?:(\\[\\\"'ntbr ])|(\\[0-9]{3})|(\\x[0-9a-fA-F]{2}))'",
+             String.Char),
+            (r"'.'", String.Char),
+            (r"'", Keyword),  # a stray quote is another syntax element
+            (r"\`([\w\'.]+)\`", Operator.Word),  # for infix applications
+            (r"\`", Keyword),  # for quoting
+            (r'"', String.Double, 'string'),
+
+            (r'[~?][a-z][\w\']*:', Name.Variable),
+        ],
+        'comment': [
+            (r'[^(*)]+', Comment),
+            (r'\(\*', Comment, '#push'),
+            (r'\*\)', Comment, '#pop'),
+            (r'[(*)]', Comment),
+        ],
+        'string': [
+            (r'[^\\"]+', String.Double),
+            include('escape-sequence'),
+            (r'\\\n', String.Double),
+            (r'"', String.Double, '#pop'),
+        ],
+        'dotted': [
+            (r'\s+', Text),
+            (r'\.', Punctuation),
+            (r'[A-Z][\w\']*(?=\s*\.)', Name.Namespace),
+            (r'[A-Z][\w\']*', Name.Class, '#pop'),
+            (r'[a-z_][\w\']*', Name, '#pop'),
+            default('#pop'),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/modeling.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/modeling.py
new file mode 100644
index 00000000..d681e7f3
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/modeling.py
@@ -0,0 +1,366 @@
+"""
+    pygments.lexers.modeling
+    ~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for modeling languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, include, bygroups, using, default
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation, Whitespace
+
+from pygments.lexers.html import HtmlLexer
+from pygments.lexers import _stan_builtins
+
+__all__ = ['ModelicaLexer', 'BugsLexer', 'JagsLexer', 'StanLexer']
+
+
+class ModelicaLexer(RegexLexer):
+    """
+    For Modelica source code.
+    """
+    name = 'Modelica'
+    url = 'http://www.modelica.org/'
+    aliases = ['modelica']
+    filenames = ['*.mo']
+    mimetypes = ['text/x-modelica']
+    version_added = '1.1'
+
+    flags = re.DOTALL | re.MULTILINE
+
+    _name = r"(?:'(?:[^\\']|\\.)+'|[a-zA-Z_]\w*)"
+
+    tokens = {
+        'whitespace': [
+            (r'[\s\ufeff]+', Text),
+            (r'//[^\n]*\n?', Comment.Single),
+            (r'/\*.*?\*/', Comment.Multiline)
+        ],
+        'root': [
+            include('whitespace'),
+            (r'"', String.Double, 'string'),
+            (r'[()\[\]{},;]+', Punctuation),
+            (r'\.?[*^/+-]|\.|<>|[<>:=]=?', Operator),
+            (r'\d+(\.?\d*[eE][-+]?\d+|\.\d*)', Number.Float),
+            (r'\d+', Number.Integer),
+            (r'(abs|acos|actualStream|array|asin|assert|AssertionLevel|atan|'
+             r'atan2|backSample|Boolean|cardinality|cat|ceil|change|Clock|'
+             r'Connections|cos|cosh|cross|delay|diagonal|div|edge|exp|'
+             r'ExternalObject|fill|floor|getInstanceName|hold|homotopy|'
+             r'identity|inStream|integer|Integer|interval|inverse|isPresent|'
+             r'linspace|log|log10|matrix|max|min|mod|ndims|noClock|noEvent|'
+             r'ones|outerProduct|pre|previous|product|Real|reinit|rem|rooted|'
+             r'sample|scalar|semiLinear|shiftSample|sign|sin|sinh|size|skew|'
+             r'smooth|spatialDistribution|sqrt|StateSelect|String|subSample|'
+             r'sum|superSample|symmetric|tan|tanh|terminal|terminate|time|'
+             r'transpose|vector|zeros)\b', Name.Builtin),
+            (r'(algorithm|annotation|break|connect|constant|constrainedby|der|'
+             r'discrete|each|else|elseif|elsewhen|encapsulated|enumeration|'
+             r'equation|exit|expandable|extends|external|firstTick|final|flow|for|if|'
+             r'import|impure|in|initial|inner|input|interval|loop|nondiscrete|outer|'
+             r'output|parameter|partial|protected|public|pure|redeclare|'
+             r'replaceable|return|stream|then|when|while)\b',
+             Keyword.Reserved),
+            (r'(and|not|or)\b', Operator.Word),
+            (r'(block|class|connector|end|function|model|operator|package|'
+             r'record|type)\b', Keyword.Reserved, 'class'),
+            (r'(false|true)\b', Keyword.Constant),
+            (r'within\b', Keyword.Reserved, 'package-prefix'),
+            (_name, Name)
+        ],
+        'class': [
+            include('whitespace'),
+            (r'(function|record)\b', Keyword.Reserved),
+            (r'(if|for|when|while)\b', Keyword.Reserved, '#pop'),
+            (_name, Name.Class, '#pop'),
+            default('#pop')
+        ],
+        'package-prefix': [
+            include('whitespace'),
+            (_name, Name.Namespace, '#pop'),
+            default('#pop')
+        ],
+        'string': [
+            (r'"', String.Double, '#pop'),
+            (r'\\[\'"?\\abfnrtv]', String.Escape),
+            (r'(?i)<\s*html\s*>([^\\"]|\\.)+?(<\s*/\s*html\s*>|(?="))',
+             using(HtmlLexer)),
+            (r'<|\\?[^"\\<]+', String.Double)
+        ]
+    }
+
+
+class BugsLexer(RegexLexer):
+    """
+    Pygments Lexer for OpenBugs and WinBugs
+    models.
+    """
+
+    name = 'BUGS'
+    aliases = ['bugs', 'winbugs', 'openbugs']
+    filenames = ['*.bug']
+    url = 'https://www.mrc-bsu.cam.ac.uk/software/bugs/openbugs'
+    version_added = '1.6'
+
+    _FUNCTIONS = (
+        # Scalar functions
+        'abs', 'arccos', 'arccosh', 'arcsin', 'arcsinh', 'arctan', 'arctanh',
+        'cloglog', 'cos', 'cosh', 'cumulative', 'cut', 'density', 'deviance',
+        'equals', 'expr', 'gammap', 'ilogit', 'icloglog', 'integral', 'log',
+        'logfact', 'loggam', 'logit', 'max', 'min', 'phi', 'post.p.value',
+        'pow', 'prior.p.value', 'probit', 'replicate.post', 'replicate.prior',
+        'round', 'sin', 'sinh', 'solution', 'sqrt', 'step', 'tan', 'tanh',
+        'trunc',
+        # Vector functions
+        'inprod', 'interp.lin', 'inverse', 'logdet', 'mean', 'eigen.vals',
+        'ode', 'prod', 'p.valueM', 'rank', 'ranked', 'replicate.postM',
+        'sd', 'sort', 'sum',
+        # Special
+        'D', 'I', 'F', 'T', 'C')
+    """ OpenBUGS built-in functions
+
+    From http://www.openbugs.info/Manuals/ModelSpecification.html#ContentsAII
+
+    This also includes
+
+    - T, C, I : Truncation and censoring.
+      ``T`` and ``C`` are in OpenBUGS. ``I`` in WinBUGS.
+    - D : ODE
+    - F : Functional http://www.openbugs.info/Examples/Functionals.html
+
+    """
+
+    _DISTRIBUTIONS = ('dbern', 'dbin', 'dcat', 'dnegbin', 'dpois',
+                      'dhyper', 'dbeta', 'dchisqr', 'ddexp', 'dexp',
+                      'dflat', 'dgamma', 'dgev', 'df', 'dggamma', 'dgpar',
+                      'dloglik', 'dlnorm', 'dlogis', 'dnorm', 'dpar',
+                      'dt', 'dunif', 'dweib', 'dmulti', 'ddirch', 'dmnorm',
+                      'dmt', 'dwish')
+    """ OpenBUGS built-in distributions
+
+    Functions from
+    http://www.openbugs.info/Manuals/ModelSpecification.html#ContentsAI
+    """
+
+    tokens = {
+        'whitespace': [
+            (r"\s+", Text),
+        ],
+        'comments': [
+            # Comments
+            (r'#.*$', Comment.Single),
+        ],
+        'root': [
+            # Comments
+            include('comments'),
+            include('whitespace'),
+            # Block start
+            (r'(model)(\s+)(\{)',
+             bygroups(Keyword.Namespace, Text, Punctuation)),
+            # Reserved Words
+            (r'(for|in)(?![\w.])', Keyword.Reserved),
+            # Built-in Functions
+            (r'({})(?=\s*\()'.format(r'|'.join(_FUNCTIONS + _DISTRIBUTIONS)),
+             Name.Builtin),
+            # Regular variable names
+            (r'[A-Za-z][\w.]*', Name),
+            # Number Literals
+            (r'[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?', Number),
+            # Punctuation
+            (r'\[|\]|\(|\)|:|,|;', Punctuation),
+            # Assignment operators
+            # SLexer makes these tokens Operators.
+            (r'<-|~', Operator),
+            # Infix and prefix operators
+            (r'\+|-|\*|/', Operator),
+            # Block
+            (r'[{}]', Punctuation),
+        ]
+    }
+
+    def analyse_text(text):
+        if re.search(r"^\s*model\s*{", text, re.M):
+            return 0.7
+        else:
+            return 0.0
+
+
+class JagsLexer(RegexLexer):
+    """
+    Pygments Lexer for JAGS.
+    """
+
+    name = 'JAGS'
+    aliases = ['jags']
+    filenames = ['*.jag', '*.bug']
+    url = 'https://mcmc-jags.sourceforge.io'
+    version_added = '1.6'
+
+    # JAGS
+    _FUNCTIONS = (
+        'abs', 'arccos', 'arccosh', 'arcsin', 'arcsinh', 'arctan', 'arctanh',
+        'cos', 'cosh', 'cloglog',
+        'equals', 'exp', 'icloglog', 'ifelse', 'ilogit', 'log', 'logfact',
+        'loggam', 'logit', 'phi', 'pow', 'probit', 'round', 'sin', 'sinh',
+        'sqrt', 'step', 'tan', 'tanh', 'trunc', 'inprod', 'interp.lin',
+        'logdet', 'max', 'mean', 'min', 'prod', 'sum', 'sd', 'inverse',
+        'rank', 'sort', 't', 'acos', 'acosh', 'asin', 'asinh', 'atan',
+        # Truncation/Censoring (should I include)
+        'T', 'I')
+    # Distributions with density, probability and quartile functions
+    _DISTRIBUTIONS = tuple(f'[dpq]{x}' for x in
+                           ('bern', 'beta', 'dchiqsqr', 'ddexp', 'dexp',
+                            'df', 'gamma', 'gen.gamma', 'logis', 'lnorm',
+                            'negbin', 'nchisqr', 'norm', 'par', 'pois', 'weib'))
+    # Other distributions without density and probability
+    _OTHER_DISTRIBUTIONS = (
+        'dt', 'dunif', 'dbetabin', 'dbern', 'dbin', 'dcat', 'dhyper',
+        'ddirch', 'dmnorm', 'dwish', 'dmt', 'dmulti', 'dbinom', 'dchisq',
+        'dnbinom', 'dweibull', 'ddirich')
+
+    tokens = {
+        'whitespace': [
+            (r"\s+", Text),
+        ],
+        'names': [
+            # Regular variable names
+            (r'[a-zA-Z][\w.]*\b', Name),
+        ],
+        'comments': [
+            # do not use stateful comments
+            (r'(?s)/\*.*?\*/', Comment.Multiline),
+            # Comments
+            (r'#.*$', Comment.Single),
+        ],
+        'root': [
+            # Comments
+            include('comments'),
+            include('whitespace'),
+            # Block start
+            (r'(model|data)(\s+)(\{)',
+             bygroups(Keyword.Namespace, Text, Punctuation)),
+            (r'var(?![\w.])', Keyword.Declaration),
+            # Reserved Words
+            (r'(for|in)(?![\w.])', Keyword.Reserved),
+            # Builtins
+            # Need to use lookahead because . is a valid char
+            (r'({})(?=\s*\()'.format(r'|'.join(_FUNCTIONS
+                                          + _DISTRIBUTIONS
+                                          + _OTHER_DISTRIBUTIONS)),
+             Name.Builtin),
+            # Names
+            include('names'),
+            # Number Literals
+            (r'[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?', Number),
+            (r'\[|\]|\(|\)|:|,|;', Punctuation),
+            # Assignment operators
+            (r'<-|~', Operator),
+            # # JAGS includes many more than OpenBUGS
+            (r'\+|-|\*|\/|\|\|[&]{2}|[<>=]=?|\^|%.*?%', Operator),
+            (r'[{}]', Punctuation),
+        ]
+    }
+
+    def analyse_text(text):
+        if re.search(r'^\s*model\s*\{', text, re.M):
+            if re.search(r'^\s*data\s*\{', text, re.M):
+                return 0.9
+            elif re.search(r'^\s*var', text, re.M):
+                return 0.9
+            else:
+                return 0.3
+        else:
+            return 0
+
+
+class StanLexer(RegexLexer):
+    """Pygments Lexer for Stan models.
+
+    The Stan modeling language is specified in the *Stan Modeling Language
+    User's Guide and Reference Manual, v2.17.0*,
+    `pdf `__.
+    """
+
+    name = 'Stan'
+    aliases = ['stan']
+    filenames = ['*.stan']
+    url = 'https://mc-stan.org'
+    version_added = '1.6'
+
+    tokens = {
+        'whitespace': [
+            (r"\s+", Text),
+        ],
+        'comments': [
+            (r'(?s)/\*.*?\*/', Comment.Multiline),
+            # Comments
+            (r'(//|#).*$', Comment.Single),
+        ],
+        'root': [
+            (r'"[^"]*"', String),
+            # Comments
+            include('comments'),
+            # block start
+            include('whitespace'),
+            # Block start
+            (r'({})(\s*)(\{{)'.format(r'|'.join(('functions', 'data', r'transformed\s+?data',
+                        'parameters', r'transformed\s+parameters',
+                        'model', r'generated\s+quantities'))),
+             bygroups(Keyword.Namespace, Text, Punctuation)),
+            # target keyword
+            (r'target\s*\+=', Keyword),
+            # Reserved Words
+            (r'({})\b'.format(r'|'.join(_stan_builtins.KEYWORDS)), Keyword),
+            # Truncation
+            (r'T(?=\s*\[)', Keyword),
+            # Data types
+            (r'({})\b'.format(r'|'.join(_stan_builtins.TYPES)), Keyword.Type),
+             # < should be punctuation, but elsewhere I can't tell if it is in
+             # a range constraint
+            (r'(<)(\s*)(upper|lower|offset|multiplier)(\s*)(=)',
+             bygroups(Operator, Whitespace, Keyword, Whitespace, Punctuation)),
+            (r'(,)(\s*)(upper)(\s*)(=)',
+             bygroups(Punctuation, Whitespace, Keyword, Whitespace, Punctuation)),
+            # Punctuation
+            (r"[;,\[\]()]", Punctuation),
+            # Builtin
+            (r'({})(?=\s*\()'.format('|'.join(_stan_builtins.FUNCTIONS)), Name.Builtin),
+            (r'(~)(\s*)({})(?=\s*\()'.format('|'.join(_stan_builtins.DISTRIBUTIONS)),
+                bygroups(Operator, Whitespace, Name.Builtin)),
+            # Special names ending in __, like lp__
+            (r'[A-Za-z]\w*__\b', Name.Builtin.Pseudo),
+            (r'({})\b'.format(r'|'.join(_stan_builtins.RESERVED)), Keyword.Reserved),
+            # user-defined functions
+            (r'[A-Za-z]\w*(?=\s*\()]', Name.Function),
+            # Imaginary Literals
+            (r'[0-9]+(\.[0-9]*)?([eE][+-]?[0-9]+)?i', Number.Float),
+            (r'\.[0-9]+([eE][+-]?[0-9]+)?i', Number.Float),
+            (r'[0-9]+i', Number.Float),
+            # Real Literals
+            (r'[0-9]+(\.[0-9]*)?([eE][+-]?[0-9]+)?', Number.Float),
+            (r'\.[0-9]+([eE][+-]?[0-9]+)?', Number.Float),
+            # Integer Literals
+            (r'[0-9]+', Number.Integer),
+            # Regular variable names
+            (r'[A-Za-z]\w*\b', Name),
+            # Assignment operators
+            (r'<-|(?:\+|-|\.?/|\.?\*|=)?=|~', Operator),
+            # Infix, prefix and postfix operators (and = )
+            (r"\+|-|\.?\*|\.?/|\\|'|\.?\^|!=?|<=?|>=?|\|\||&&|%|\?|:|%/%|!", Operator),
+            # Block delimiters
+            (r'[{}]', Punctuation),
+            # Distribution |
+            (r'\|', Punctuation)
+        ]
+    }
+
+    def analyse_text(text):
+        if re.search(r'^\s*parameters\s*\{', text, re.M):
+            return 1.0
+        else:
+            return 0.0
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/modula2.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/modula2.py
new file mode 100644
index 00000000..713f4722
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/modula2.py
@@ -0,0 +1,1579 @@
+"""
+    pygments.lexers.modula2
+    ~~~~~~~~~~~~~~~~~~~~~~~
+
+    Multi-Dialect Lexer for Modula-2.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, include
+from pygments.util import get_bool_opt, get_list_opt
+from pygments.token import Text, Comment, Operator, Keyword, Name, \
+    String, Number, Punctuation, Error
+
+__all__ = ['Modula2Lexer']
+
+
+# Multi-Dialect Modula-2 Lexer
+class Modula2Lexer(RegexLexer):
+    """
+    For Modula-2 source code.
+
+    The Modula-2 lexer supports several dialects.  By default, it operates in
+    fallback mode, recognising the *combined* literals, punctuation symbols
+    and operators of all supported dialects, and the *combined* reserved words
+    and builtins of PIM Modula-2, ISO Modula-2 and Modula-2 R10, while not
+    differentiating between library defined identifiers.
+
+    To select a specific dialect, a dialect option may be passed
+    or a dialect tag may be embedded into a source file.
+
+    Dialect Options:
+
+    `m2pim`
+        Select PIM Modula-2 dialect.
+    `m2iso`
+        Select ISO Modula-2 dialect.
+    `m2r10`
+        Select Modula-2 R10 dialect.
+    `objm2`
+        Select Objective Modula-2 dialect.
+
+    The PIM and ISO dialect options may be qualified with a language extension.
+
+    Language Extensions:
+
+    `+aglet`
+        Select Aglet Modula-2 extensions, available with m2iso.
+    `+gm2`
+        Select GNU Modula-2 extensions, available with m2pim.
+    `+p1`
+        Select p1 Modula-2 extensions, available with m2iso.
+    `+xds`
+        Select XDS Modula-2 extensions, available with m2iso.
+
+
+    Passing a Dialect Option via Unix Commandline Interface
+
+    Dialect options may be passed to the lexer using the `dialect` key.
+    Only one such option should be passed. If multiple dialect options are
+    passed, the first valid option is used, any subsequent options are ignored.
+
+    Examples:
+
+    `$ pygmentize -O full,dialect=m2iso -f html -o /path/to/output /path/to/input`
+        Use ISO dialect to render input to HTML output
+    `$ pygmentize -O full,dialect=m2iso+p1 -f rtf -o /path/to/output /path/to/input`
+        Use ISO dialect with p1 extensions to render input to RTF output
+
+
+    Embedding a Dialect Option within a source file
+
+    A dialect option may be embedded in a source file in form of a dialect
+    tag, a specially formatted comment that specifies a dialect option.
+
+    Dialect Tag EBNF::
+
+       dialectTag :
+           OpeningCommentDelim Prefix dialectOption ClosingCommentDelim ;
+
+       dialectOption :
+           'm2pim' | 'm2iso' | 'm2r10' | 'objm2' |
+           'm2iso+aglet' | 'm2pim+gm2' | 'm2iso+p1' | 'm2iso+xds' ;
+
+       Prefix : '!' ;
+
+       OpeningCommentDelim : '(*' ;
+
+       ClosingCommentDelim : '*)' ;
+
+    No whitespace is permitted between the tokens of a dialect tag.
+
+    In the event that a source file contains multiple dialect tags, the first
+    tag that contains a valid dialect option will be used and any subsequent
+    dialect tags will be ignored.  Ideally, a dialect tag should be placed
+    at the beginning of a source file.
+
+    An embedded dialect tag overrides a dialect option set via command line.
+
+    Examples:
+
+    ``(*!m2r10*) DEFINITION MODULE Foobar; ...``
+        Use Modula2 R10 dialect to render this source file.
+    ``(*!m2pim+gm2*) DEFINITION MODULE Bazbam; ...``
+        Use PIM dialect with GNU extensions to render this source file.
+
+
+    Algol Publication Mode:
+
+    In Algol publication mode, source text is rendered for publication of
+    algorithms in scientific papers and academic texts, following the format
+    of the Revised Algol-60 Language Report.  It is activated by passing
+    one of two corresponding styles as an option:
+
+    `algol`
+        render reserved words lowercase underline boldface
+        and builtins lowercase boldface italic
+    `algol_nu`
+        render reserved words lowercase boldface (no underlining)
+        and builtins lowercase boldface italic
+
+    The lexer automatically performs the required lowercase conversion when
+    this mode is activated.
+
+    Example:
+
+    ``$ pygmentize -O full,style=algol -f latex -o /path/to/output /path/to/input``
+        Render input file in Algol publication mode to LaTeX output.
+
+
+    Rendering Mode of First Class ADT Identifiers:
+
+    The rendering of standard library first class ADT identifiers is controlled
+    by option flag "treat_stdlib_adts_as_builtins".
+
+    When this option is turned on, standard library ADT identifiers are rendered
+    as builtins.  When it is turned off, they are rendered as ordinary library
+    identifiers.
+
+    `treat_stdlib_adts_as_builtins` (default: On)
+
+    The option is useful for dialects that support ADTs as first class objects
+    and provide ADTs in the standard library that would otherwise be built-in.
+
+    At present, only Modula-2 R10 supports library ADTs as first class objects
+    and therefore, no ADT identifiers are defined for any other dialects.
+
+    Example:
+
+    ``$ pygmentize -O full,dialect=m2r10,treat_stdlib_adts_as_builtins=Off ...``
+        Render standard library ADTs as ordinary library types.
+
+    .. versionchanged:: 2.1
+       Added multi-dialect support.
+    """
+    name = 'Modula-2'
+    url = 'http://www.modula2.org/'
+    aliases = ['modula2', 'm2']
+    filenames = ['*.def', '*.mod']
+    mimetypes = ['text/x-modula2']
+    version_added = '1.3'
+
+    flags = re.MULTILINE | re.DOTALL
+
+    tokens = {
+        'whitespace': [
+            (r'\n+', Text),  # blank lines
+            (r'\s+', Text),  # whitespace
+        ],
+        'dialecttags': [
+            # PIM Dialect Tag
+            (r'\(\*!m2pim\*\)', Comment.Special),
+            # ISO Dialect Tag
+            (r'\(\*!m2iso\*\)', Comment.Special),
+            # M2R10 Dialect Tag
+            (r'\(\*!m2r10\*\)', Comment.Special),
+            # ObjM2 Dialect Tag
+            (r'\(\*!objm2\*\)', Comment.Special),
+            # Aglet Extensions Dialect Tag
+            (r'\(\*!m2iso\+aglet\*\)', Comment.Special),
+            # GNU Extensions Dialect Tag
+            (r'\(\*!m2pim\+gm2\*\)', Comment.Special),
+            # p1 Extensions Dialect Tag
+            (r'\(\*!m2iso\+p1\*\)', Comment.Special),
+            # XDS Extensions Dialect Tag
+            (r'\(\*!m2iso\+xds\*\)', Comment.Special),
+        ],
+        'identifiers': [
+            (r'([a-zA-Z_$][\w$]*)', Name),
+        ],
+        'prefixed_number_literals': [
+            #
+            # Base-2, whole number
+            (r'0b[01]+(\'[01]+)*', Number.Bin),
+            #
+            # Base-16, whole number
+            (r'0[ux][0-9A-F]+(\'[0-9A-F]+)*', Number.Hex),
+        ],
+        'plain_number_literals': [
+            #
+            # Base-10, real number with exponent
+            (r'[0-9]+(\'[0-9]+)*'  # integral part
+             r'\.[0-9]+(\'[0-9]+)*'  # fractional part
+             r'[eE][+-]?[0-9]+(\'[0-9]+)*',  # exponent
+             Number.Float),
+            #
+            # Base-10, real number without exponent
+            (r'[0-9]+(\'[0-9]+)*'  # integral part
+             r'\.[0-9]+(\'[0-9]+)*',  # fractional part
+             Number.Float),
+            #
+            # Base-10, whole number
+            (r'[0-9]+(\'[0-9]+)*', Number.Integer),
+        ],
+        'suffixed_number_literals': [
+            #
+            # Base-8, whole number
+            (r'[0-7]+B', Number.Oct),
+            #
+            # Base-8, character code
+            (r'[0-7]+C', Number.Oct),
+            #
+            # Base-16, number
+            (r'[0-9A-F]+H', Number.Hex),
+        ],
+        'string_literals': [
+            (r'"(\\\\|\\[^\\]|[^"\\])*"', String.Double),
+            (r"'(\\\\|\\[^\\]|[^'\\])*'", String.Single),
+        ],
+        'digraph_operators': [
+            # Dot Product Operator
+            (r'\*\.', Operator),
+            # Array Concatenation Operator
+            (r'\+>', Operator),  # M2R10 + ObjM2
+            # Inequality Operator
+            (r'<>', Operator),  # ISO + PIM
+            # Less-Or-Equal, Subset
+            (r'<=', Operator),
+            # Greater-Or-Equal, Superset
+            (r'>=', Operator),
+            # Identity Operator
+            (r'==', Operator),  # M2R10 + ObjM2
+            # Type Conversion Operator
+            (r'::', Operator),  # M2R10 + ObjM2
+            # Assignment Symbol
+            (r':=', Operator),
+            # Postfix Increment Mutator
+            (r'\+\+', Operator),  # M2R10 + ObjM2
+            # Postfix Decrement Mutator
+            (r'--', Operator),  # M2R10 + ObjM2
+        ],
+        'unigraph_operators': [
+            # Arithmetic Operators
+            (r'[+-]', Operator),
+            (r'[*/]', Operator),
+            # ISO 80000-2 compliant Set Difference Operator
+            (r'\\', Operator),  # M2R10 + ObjM2
+            # Relational Operators
+            (r'[=#<>]', Operator),
+            # Dereferencing Operator
+            (r'\^', Operator),
+            # Dereferencing Operator Synonym
+            (r'@', Operator),  # ISO
+            # Logical AND Operator Synonym
+            (r'&', Operator),  # PIM + ISO
+            # Logical NOT Operator Synonym
+            (r'~', Operator),  # PIM + ISO
+            # Smalltalk Message Prefix
+            (r'`', Operator),  # ObjM2
+        ],
+        'digraph_punctuation': [
+            # Range Constructor
+            (r'\.\.', Punctuation),
+            # Opening Chevron Bracket
+            (r'<<', Punctuation),  # M2R10 + ISO
+            # Closing Chevron Bracket
+            (r'>>', Punctuation),  # M2R10 + ISO
+            # Blueprint Punctuation
+            (r'->', Punctuation),  # M2R10 + ISO
+            # Distinguish |# and # in M2 R10
+            (r'\|#', Punctuation),
+            # Distinguish ## and # in M2 R10
+            (r'##', Punctuation),
+            # Distinguish |* and * in M2 R10
+            (r'\|\*', Punctuation),
+        ],
+        'unigraph_punctuation': [
+            # Common Punctuation
+            (r'[()\[\]{},.:;|]', Punctuation),
+            # Case Label Separator Synonym
+            (r'!', Punctuation),  # ISO
+            # Blueprint Punctuation
+            (r'\?', Punctuation),  # M2R10 + ObjM2
+        ],
+        'comments': [
+            # Single Line Comment
+            (r'^//.*?\n', Comment.Single),  # M2R10 + ObjM2
+            # Block Comment
+            (r'\(\*([^$].*?)\*\)', Comment.Multiline),
+            # Template Block Comment
+            (r'/\*(.*?)\*/', Comment.Multiline),  # M2R10 + ObjM2
+        ],
+        'pragmas': [
+            # ISO Style Pragmas
+            (r'<\*.*?\*>', Comment.Preproc),  # ISO, M2R10 + ObjM2
+            # Pascal Style Pragmas
+            (r'\(\*\$.*?\*\)', Comment.Preproc),  # PIM
+        ],
+        'root': [
+            include('whitespace'),
+            include('dialecttags'),
+            include('pragmas'),
+            include('comments'),
+            include('identifiers'),
+            include('suffixed_number_literals'),  # PIM + ISO
+            include('prefixed_number_literals'),  # M2R10 + ObjM2
+            include('plain_number_literals'),
+            include('string_literals'),
+            include('digraph_punctuation'),
+            include('digraph_operators'),
+            include('unigraph_punctuation'),
+            include('unigraph_operators'),
+        ]
+    }
+
+#  C o m m o n   D a t a s e t s
+
+    # Common Reserved Words Dataset
+    common_reserved_words = (
+        # 37 common reserved words
+        'AND', 'ARRAY', 'BEGIN', 'BY', 'CASE', 'CONST', 'DEFINITION', 'DIV',
+        'DO', 'ELSE', 'ELSIF', 'END', 'EXIT', 'FOR', 'FROM', 'IF',
+        'IMPLEMENTATION', 'IMPORT', 'IN', 'LOOP', 'MOD', 'MODULE', 'NOT',
+        'OF', 'OR', 'POINTER', 'PROCEDURE', 'RECORD', 'REPEAT', 'RETURN',
+        'SET', 'THEN', 'TO', 'TYPE', 'UNTIL', 'VAR', 'WHILE',
+    )
+
+    # Common Builtins Dataset
+    common_builtins = (
+        # 16 common builtins
+        'ABS', 'BOOLEAN', 'CARDINAL', 'CHAR', 'CHR', 'FALSE', 'INTEGER',
+        'LONGINT', 'LONGREAL', 'MAX', 'MIN', 'NIL', 'ODD', 'ORD', 'REAL',
+        'TRUE',
+    )
+
+    # Common Pseudo-Module Builtins Dataset
+    common_pseudo_builtins = (
+        # 4 common pseudo builtins
+        'ADDRESS', 'BYTE', 'WORD', 'ADR'
+    )
+
+#  P I M   M o d u l a - 2   D a t a s e t s
+
+    # Lexemes to Mark as Error Tokens for PIM Modula-2
+    pim_lexemes_to_reject = (
+        '!', '`', '@', '$', '%', '?', '\\', '==', '++', '--', '::', '*.',
+        '+>', '->', '<<', '>>', '|#', '##',
+    )
+
+    # PIM Modula-2 Additional Reserved Words Dataset
+    pim_additional_reserved_words = (
+        # 3 additional reserved words
+        'EXPORT', 'QUALIFIED', 'WITH',
+    )
+
+    # PIM Modula-2 Additional Builtins Dataset
+    pim_additional_builtins = (
+        # 16 additional builtins
+        'BITSET', 'CAP', 'DEC', 'DISPOSE', 'EXCL', 'FLOAT', 'HALT', 'HIGH',
+        'INC', 'INCL', 'NEW', 'NIL', 'PROC', 'SIZE', 'TRUNC', 'VAL',
+    )
+
+    # PIM Modula-2 Additional Pseudo-Module Builtins Dataset
+    pim_additional_pseudo_builtins = (
+        # 5 additional pseudo builtins
+        'SYSTEM', 'PROCESS', 'TSIZE', 'NEWPROCESS', 'TRANSFER',
+    )
+
+#  I S O   M o d u l a - 2   D a t a s e t s
+
+    # Lexemes to Mark as Error Tokens for ISO Modula-2
+    iso_lexemes_to_reject = (
+        '`', '$', '%', '?', '\\', '==', '++', '--', '::', '*.', '+>', '->',
+        '<<', '>>', '|#', '##',
+    )
+
+    # ISO Modula-2 Additional Reserved Words Dataset
+    iso_additional_reserved_words = (
+        # 9 additional reserved words (ISO 10514-1)
+        'EXCEPT', 'EXPORT', 'FINALLY', 'FORWARD', 'PACKEDSET', 'QUALIFIED',
+        'REM', 'RETRY', 'WITH',
+        # 10 additional reserved words (ISO 10514-2 & ISO 10514-3)
+        'ABSTRACT', 'AS', 'CLASS', 'GUARD', 'INHERIT', 'OVERRIDE', 'READONLY',
+        'REVEAL', 'TRACED', 'UNSAFEGUARDED',
+    )
+
+    # ISO Modula-2 Additional Builtins Dataset
+    iso_additional_builtins = (
+        # 26 additional builtins (ISO 10514-1)
+        'BITSET', 'CAP', 'CMPLX', 'COMPLEX', 'DEC', 'DISPOSE', 'EXCL', 'FLOAT',
+        'HALT', 'HIGH', 'IM', 'INC', 'INCL', 'INT', 'INTERRUPTIBLE',  'LENGTH',
+        'LFLOAT', 'LONGCOMPLEX', 'NEW', 'PROC', 'PROTECTION', 'RE', 'SIZE',
+        'TRUNC', 'UNINTERRUBTIBLE', 'VAL',
+        # 5 additional builtins (ISO 10514-2 & ISO 10514-3)
+        'CREATE', 'DESTROY', 'EMPTY', 'ISMEMBER', 'SELF',
+    )
+
+    # ISO Modula-2 Additional Pseudo-Module Builtins Dataset
+    iso_additional_pseudo_builtins = (
+        # 14 additional builtins (SYSTEM)
+        'SYSTEM', 'BITSPERLOC', 'LOCSPERBYTE', 'LOCSPERWORD', 'LOC',
+        'ADDADR', 'SUBADR', 'DIFADR', 'MAKEADR', 'ADR',
+        'ROTATE', 'SHIFT', 'CAST', 'TSIZE',
+        # 13 additional builtins (COROUTINES)
+        'COROUTINES', 'ATTACH', 'COROUTINE', 'CURRENT', 'DETACH', 'HANDLER',
+        'INTERRUPTSOURCE', 'IOTRANSFER', 'IsATTACHED', 'LISTEN',
+        'NEWCOROUTINE', 'PROT', 'TRANSFER',
+        # 9 additional builtins (EXCEPTIONS)
+        'EXCEPTIONS', 'AllocateSource', 'CurrentNumber', 'ExceptionNumber',
+        'ExceptionSource', 'GetMessage', 'IsCurrentSource',
+        'IsExceptionalExecution', 'RAISE',
+        # 3 additional builtins (TERMINATION)
+        'TERMINATION', 'IsTerminating', 'HasHalted',
+        # 4 additional builtins (M2EXCEPTION)
+        'M2EXCEPTION', 'M2Exceptions', 'M2Exception', 'IsM2Exception',
+        'indexException', 'rangeException', 'caseSelectException',
+        'invalidLocation', 'functionException', 'wholeValueException',
+        'wholeDivException', 'realValueException', 'realDivException',
+        'complexValueException', 'complexDivException', 'protException',
+        'sysException', 'coException', 'exException',
+    )
+
+#  M o d u l a - 2   R 1 0   D a t a s e t s
+
+    # Lexemes to Mark as Error Tokens for Modula-2 R10
+    m2r10_lexemes_to_reject = (
+        '!', '`', '@', '$', '%', '&', '<>',
+    )
+
+    # Modula-2 R10 reserved words in addition to the common set
+    m2r10_additional_reserved_words = (
+        # 12 additional reserved words
+        'ALIAS', 'ARGLIST', 'BLUEPRINT', 'COPY', 'GENLIB', 'INDETERMINATE',
+        'NEW', 'NONE', 'OPAQUE', 'REFERENTIAL', 'RELEASE', 'RETAIN',
+        # 2 additional reserved words with symbolic assembly option
+        'ASM', 'REG',
+    )
+
+    # Modula-2 R10 builtins in addition to the common set
+    m2r10_additional_builtins = (
+        # 26 additional builtins
+        'CARDINAL', 'COUNT', 'EMPTY', 'EXISTS', 'INSERT', 'LENGTH', 'LONGCARD',
+        'OCTET', 'PTR', 'PRED', 'READ', 'READNEW', 'REMOVE', 'RETRIEVE', 'SORT',
+        'STORE', 'SUBSET', 'SUCC', 'TLIMIT', 'TMAX', 'TMIN', 'TRUE', 'TSIZE',
+        'UNICHAR', 'WRITE', 'WRITEF',
+    )
+
+    # Modula-2 R10 Additional Pseudo-Module Builtins Dataset
+    m2r10_additional_pseudo_builtins = (
+        # 13 additional builtins (TPROPERTIES)
+        'TPROPERTIES', 'PROPERTY', 'LITERAL', 'TPROPERTY', 'TLITERAL',
+        'TBUILTIN', 'TDYN', 'TREFC', 'TNIL', 'TBASE', 'TPRECISION',
+        'TMAXEXP', 'TMINEXP',
+        # 4 additional builtins (CONVERSION)
+        'CONVERSION', 'TSXFSIZE', 'SXF', 'VAL',
+        # 35 additional builtins (UNSAFE)
+        'UNSAFE', 'CAST', 'INTRINSIC', 'AVAIL', 'ADD', 'SUB', 'ADDC', 'SUBC',
+        'FETCHADD', 'FETCHSUB', 'SHL', 'SHR', 'ASHR', 'ROTL', 'ROTR', 'ROTLC',
+        'ROTRC', 'BWNOT', 'BWAND', 'BWOR', 'BWXOR', 'BWNAND', 'BWNOR',
+        'SETBIT', 'TESTBIT', 'LSBIT', 'MSBIT', 'CSBITS', 'BAIL', 'HALT',
+        'TODO', 'FFI', 'ADDR', 'VARGLIST', 'VARGC',
+        # 11 additional builtins (ATOMIC)
+        'ATOMIC', 'INTRINSIC', 'AVAIL', 'SWAP', 'CAS', 'INC', 'DEC', 'BWAND',
+        'BWNAND', 'BWOR', 'BWXOR',
+        # 7 additional builtins (COMPILER)
+        'COMPILER', 'DEBUG', 'MODNAME', 'PROCNAME', 'LINENUM', 'DEFAULT',
+        'HASH',
+        # 5 additional builtins (ASSEMBLER)
+        'ASSEMBLER', 'REGISTER', 'SETREG', 'GETREG', 'CODE',
+    )
+
+#  O b j e c t i v e   M o d u l a - 2   D a t a s e t s
+
+    # Lexemes to Mark as Error Tokens for Objective Modula-2
+    objm2_lexemes_to_reject = (
+        '!', '$', '%', '&', '<>',
+    )
+
+    # Objective Modula-2 Extensions
+    # reserved words in addition to Modula-2 R10
+    objm2_additional_reserved_words = (
+        # 16 additional reserved words
+        'BYCOPY', 'BYREF', 'CLASS', 'CONTINUE', 'CRITICAL', 'INOUT', 'METHOD',
+        'ON', 'OPTIONAL', 'OUT', 'PRIVATE', 'PROTECTED', 'PROTOCOL', 'PUBLIC',
+        'SUPER', 'TRY',
+    )
+
+    # Objective Modula-2 Extensions
+    # builtins in addition to Modula-2 R10
+    objm2_additional_builtins = (
+        # 3 additional builtins
+        'OBJECT', 'NO', 'YES',
+    )
+
+    # Objective Modula-2 Extensions
+    # pseudo-module builtins in addition to Modula-2 R10
+    objm2_additional_pseudo_builtins = (
+        # None
+    )
+
+#  A g l e t   M o d u l a - 2   D a t a s e t s
+
+    # Aglet Extensions
+    # reserved words in addition to ISO Modula-2
+    aglet_additional_reserved_words = (
+        # None
+    )
+
+    # Aglet Extensions
+    # builtins in addition to ISO Modula-2
+    aglet_additional_builtins = (
+        # 9 additional builtins
+        'BITSET8', 'BITSET16', 'BITSET32', 'CARDINAL8', 'CARDINAL16',
+        'CARDINAL32', 'INTEGER8', 'INTEGER16', 'INTEGER32',
+    )
+
+    # Aglet Modula-2 Extensions
+    # pseudo-module builtins in addition to ISO Modula-2
+    aglet_additional_pseudo_builtins = (
+        # None
+    )
+
+#  G N U   M o d u l a - 2   D a t a s e t s
+
+    # GNU Extensions
+    # reserved words in addition to PIM Modula-2
+    gm2_additional_reserved_words = (
+        # 10 additional reserved words
+        'ASM', '__ATTRIBUTE__', '__BUILTIN__', '__COLUMN__', '__DATE__',
+        '__FILE__', '__FUNCTION__', '__LINE__', '__MODULE__', 'VOLATILE',
+    )
+
+    # GNU Extensions
+    # builtins in addition to PIM Modula-2
+    gm2_additional_builtins = (
+        # 21 additional builtins
+        'BITSET8', 'BITSET16', 'BITSET32', 'CARDINAL8', 'CARDINAL16',
+        'CARDINAL32', 'CARDINAL64', 'COMPLEX32', 'COMPLEX64', 'COMPLEX96',
+        'COMPLEX128', 'INTEGER8', 'INTEGER16', 'INTEGER32', 'INTEGER64',
+        'REAL8', 'REAL16', 'REAL32', 'REAL96', 'REAL128', 'THROW',
+    )
+
+    # GNU Extensions
+    # pseudo-module builtins in addition to PIM Modula-2
+    gm2_additional_pseudo_builtins = (
+        # None
+    )
+
+#  p 1   M o d u l a - 2   D a t a s e t s
+
+    # p1 Extensions
+    # reserved words in addition to ISO Modula-2
+    p1_additional_reserved_words = (
+        # None
+    )
+
+    # p1 Extensions
+    # builtins in addition to ISO Modula-2
+    p1_additional_builtins = (
+        # None
+    )
+
+    # p1 Modula-2 Extensions
+    # pseudo-module builtins in addition to ISO Modula-2
+    p1_additional_pseudo_builtins = (
+        # 1 additional builtin
+        'BCD',
+    )
+
+#  X D S   M o d u l a - 2   D a t a s e t s
+
+    # XDS Extensions
+    # reserved words in addition to ISO Modula-2
+    xds_additional_reserved_words = (
+        # 1 additional reserved word
+        'SEQ',
+    )
+
+    # XDS Extensions
+    # builtins in addition to ISO Modula-2
+    xds_additional_builtins = (
+        # 9 additional builtins
+        'ASH', 'ASSERT', 'DIFFADR_TYPE', 'ENTIER', 'INDEX', 'LEN',
+        'LONGCARD', 'SHORTCARD', 'SHORTINT',
+    )
+
+    # XDS Modula-2 Extensions
+    # pseudo-module builtins in addition to ISO Modula-2
+    xds_additional_pseudo_builtins = (
+        # 22 additional builtins (SYSTEM)
+        'PROCESS', 'NEWPROCESS', 'BOOL8', 'BOOL16', 'BOOL32', 'CARD8',
+        'CARD16', 'CARD32', 'INT8', 'INT16', 'INT32', 'REF', 'MOVE',
+        'FILL', 'GET', 'PUT', 'CC', 'int', 'unsigned', 'size_t', 'void'
+        # 3 additional builtins (COMPILER)
+        'COMPILER', 'OPTION', 'EQUATION'
+    )
+
+#  P I M   S t a n d a r d   L i b r a r y   D a t a s e t s
+
+    # PIM Modula-2 Standard Library Modules Dataset
+    pim_stdlib_module_identifiers = (
+        'Terminal', 'FileSystem', 'InOut', 'RealInOut', 'MathLib0', 'Storage',
+    )
+
+    # PIM Modula-2 Standard Library Types Dataset
+    pim_stdlib_type_identifiers = (
+        'Flag', 'FlagSet', 'Response', 'Command', 'Lock', 'Permission',
+        'MediumType', 'File', 'FileProc', 'DirectoryProc', 'FileCommand',
+        'DirectoryCommand',
+    )
+
+    # PIM Modula-2 Standard Library Procedures Dataset
+    pim_stdlib_proc_identifiers = (
+        'Read', 'BusyRead', 'ReadAgain', 'Write', 'WriteString', 'WriteLn',
+        'Create', 'Lookup', 'Close', 'Delete', 'Rename', 'SetRead', 'SetWrite',
+        'SetModify', 'SetOpen', 'Doio', 'SetPos', 'GetPos', 'Length', 'Reset',
+        'Again', 'ReadWord', 'WriteWord', 'ReadChar', 'WriteChar',
+        'CreateMedium', 'DeleteMedium', 'AssignName', 'DeassignName',
+        'ReadMedium', 'LookupMedium', 'OpenInput', 'OpenOutput', 'CloseInput',
+        'CloseOutput', 'ReadString', 'ReadInt', 'ReadCard', 'ReadWrd',
+        'WriteInt', 'WriteCard', 'WriteOct', 'WriteHex', 'WriteWrd',
+        'ReadReal', 'WriteReal', 'WriteFixPt', 'WriteRealOct', 'sqrt', 'exp',
+        'ln', 'sin', 'cos', 'arctan', 'entier', 'ALLOCATE', 'DEALLOCATE',
+    )
+
+    # PIM Modula-2 Standard Library Variables Dataset
+    pim_stdlib_var_identifiers = (
+        'Done', 'termCH', 'in', 'out'
+    )
+
+    # PIM Modula-2 Standard Library Constants Dataset
+    pim_stdlib_const_identifiers = (
+        'EOL',
+    )
+
+#  I S O   S t a n d a r d   L i b r a r y   D a t a s e t s
+
+    # ISO Modula-2 Standard Library Modules Dataset
+    iso_stdlib_module_identifiers = (
+        # TO DO
+    )
+
+    # ISO Modula-2 Standard Library Types Dataset
+    iso_stdlib_type_identifiers = (
+        # TO DO
+    )
+
+    # ISO Modula-2 Standard Library Procedures Dataset
+    iso_stdlib_proc_identifiers = (
+        # TO DO
+    )
+
+    # ISO Modula-2 Standard Library Variables Dataset
+    iso_stdlib_var_identifiers = (
+        # TO DO
+    )
+
+    # ISO Modula-2 Standard Library Constants Dataset
+    iso_stdlib_const_identifiers = (
+        # TO DO
+    )
+
+#  M 2   R 1 0   S t a n d a r d   L i b r a r y   D a t a s e t s
+
+    # Modula-2 R10 Standard Library ADTs Dataset
+    m2r10_stdlib_adt_identifiers = (
+        'BCD', 'LONGBCD', 'BITSET', 'SHORTBITSET', 'LONGBITSET',
+        'LONGLONGBITSET', 'COMPLEX', 'LONGCOMPLEX', 'SHORTCARD', 'LONGLONGCARD',
+        'SHORTINT', 'LONGLONGINT', 'POSINT', 'SHORTPOSINT', 'LONGPOSINT',
+        'LONGLONGPOSINT', 'BITSET8', 'BITSET16', 'BITSET32', 'BITSET64',
+        'BITSET128', 'BS8', 'BS16', 'BS32', 'BS64', 'BS128', 'CARDINAL8',
+        'CARDINAL16', 'CARDINAL32', 'CARDINAL64', 'CARDINAL128', 'CARD8',
+        'CARD16', 'CARD32', 'CARD64', 'CARD128', 'INTEGER8', 'INTEGER16',
+        'INTEGER32', 'INTEGER64', 'INTEGER128', 'INT8', 'INT16', 'INT32',
+        'INT64', 'INT128', 'STRING', 'UNISTRING',
+    )
+
+    # Modula-2 R10 Standard Library Blueprints Dataset
+    m2r10_stdlib_blueprint_identifiers = (
+        'ProtoRoot', 'ProtoComputational', 'ProtoNumeric', 'ProtoScalar',
+        'ProtoNonScalar', 'ProtoCardinal', 'ProtoInteger', 'ProtoReal',
+        'ProtoComplex', 'ProtoVector', 'ProtoTuple', 'ProtoCompArray',
+        'ProtoCollection', 'ProtoStaticArray', 'ProtoStaticSet',
+        'ProtoStaticString', 'ProtoArray', 'ProtoString', 'ProtoSet',
+        'ProtoMultiSet', 'ProtoDictionary', 'ProtoMultiDict', 'ProtoExtension',
+        'ProtoIO', 'ProtoCardMath', 'ProtoIntMath', 'ProtoRealMath',
+    )
+
+    # Modula-2 R10 Standard Library Modules Dataset
+    m2r10_stdlib_module_identifiers = (
+        'ASCII', 'BooleanIO', 'CharIO', 'UnicharIO', 'OctetIO',
+        'CardinalIO', 'LongCardIO', 'IntegerIO', 'LongIntIO', 'RealIO',
+        'LongRealIO', 'BCDIO', 'LongBCDIO', 'CardMath', 'LongCardMath',
+        'IntMath', 'LongIntMath', 'RealMath', 'LongRealMath', 'BCDMath',
+        'LongBCDMath', 'FileIO', 'FileSystem', 'Storage', 'IOSupport',
+    )
+
+    # Modula-2 R10 Standard Library Types Dataset
+    m2r10_stdlib_type_identifiers = (
+        'File', 'Status',
+        # TO BE COMPLETED
+    )
+
+    # Modula-2 R10 Standard Library Procedures Dataset
+    m2r10_stdlib_proc_identifiers = (
+        'ALLOCATE', 'DEALLOCATE', 'SIZE',
+        # TO BE COMPLETED
+    )
+
+    # Modula-2 R10 Standard Library Variables Dataset
+    m2r10_stdlib_var_identifiers = (
+        'stdIn', 'stdOut', 'stdErr',
+    )
+
+    # Modula-2 R10 Standard Library Constants Dataset
+    m2r10_stdlib_const_identifiers = (
+        'pi', 'tau',
+    )
+
+#  D i a l e c t s
+
+    # Dialect modes
+    dialects = (
+        'unknown',
+        'm2pim', 'm2iso', 'm2r10', 'objm2',
+        'm2iso+aglet', 'm2pim+gm2', 'm2iso+p1', 'm2iso+xds',
+    )
+
+#   D a t a b a s e s
+
+    # Lexemes to Mark as Errors Database
+    lexemes_to_reject_db = {
+        # Lexemes to reject for unknown dialect
+        'unknown': (
+            # LEAVE THIS EMPTY
+        ),
+        # Lexemes to reject for PIM Modula-2
+        'm2pim': (
+            pim_lexemes_to_reject,
+        ),
+        # Lexemes to reject for ISO Modula-2
+        'm2iso': (
+            iso_lexemes_to_reject,
+        ),
+        # Lexemes to reject for Modula-2 R10
+        'm2r10': (
+            m2r10_lexemes_to_reject,
+        ),
+        # Lexemes to reject for Objective Modula-2
+        'objm2': (
+            objm2_lexemes_to_reject,
+        ),
+        # Lexemes to reject for Aglet Modula-2
+        'm2iso+aglet': (
+            iso_lexemes_to_reject,
+        ),
+        # Lexemes to reject for GNU Modula-2
+        'm2pim+gm2': (
+            pim_lexemes_to_reject,
+        ),
+        # Lexemes to reject for p1 Modula-2
+        'm2iso+p1': (
+            iso_lexemes_to_reject,
+        ),
+        # Lexemes to reject for XDS Modula-2
+        'm2iso+xds': (
+            iso_lexemes_to_reject,
+        ),
+    }
+
+    # Reserved Words Database
+    reserved_words_db = {
+        # Reserved words for unknown dialect
+        'unknown': (
+            common_reserved_words,
+            pim_additional_reserved_words,
+            iso_additional_reserved_words,
+            m2r10_additional_reserved_words,
+        ),
+
+        # Reserved words for PIM Modula-2
+        'm2pim': (
+            common_reserved_words,
+            pim_additional_reserved_words,
+        ),
+
+        # Reserved words for Modula-2 R10
+        'm2iso': (
+            common_reserved_words,
+            iso_additional_reserved_words,
+        ),
+
+        # Reserved words for ISO Modula-2
+        'm2r10': (
+            common_reserved_words,
+            m2r10_additional_reserved_words,
+        ),
+
+        # Reserved words for Objective Modula-2
+        'objm2': (
+            common_reserved_words,
+            m2r10_additional_reserved_words,
+            objm2_additional_reserved_words,
+        ),
+
+        # Reserved words for Aglet Modula-2 Extensions
+        'm2iso+aglet': (
+            common_reserved_words,
+            iso_additional_reserved_words,
+            aglet_additional_reserved_words,
+        ),
+
+        # Reserved words for GNU Modula-2 Extensions
+        'm2pim+gm2': (
+            common_reserved_words,
+            pim_additional_reserved_words,
+            gm2_additional_reserved_words,
+        ),
+
+        # Reserved words for p1 Modula-2 Extensions
+        'm2iso+p1': (
+            common_reserved_words,
+            iso_additional_reserved_words,
+            p1_additional_reserved_words,
+        ),
+
+        # Reserved words for XDS Modula-2 Extensions
+        'm2iso+xds': (
+            common_reserved_words,
+            iso_additional_reserved_words,
+            xds_additional_reserved_words,
+        ),
+    }
+
+    # Builtins Database
+    builtins_db = {
+        # Builtins for unknown dialect
+        'unknown': (
+            common_builtins,
+            pim_additional_builtins,
+            iso_additional_builtins,
+            m2r10_additional_builtins,
+        ),
+
+        # Builtins for PIM Modula-2
+        'm2pim': (
+            common_builtins,
+            pim_additional_builtins,
+        ),
+
+        # Builtins for ISO Modula-2
+        'm2iso': (
+            common_builtins,
+            iso_additional_builtins,
+        ),
+
+        # Builtins for ISO Modula-2
+        'm2r10': (
+            common_builtins,
+            m2r10_additional_builtins,
+        ),
+
+        # Builtins for Objective Modula-2
+        'objm2': (
+            common_builtins,
+            m2r10_additional_builtins,
+            objm2_additional_builtins,
+        ),
+
+        # Builtins for Aglet Modula-2 Extensions
+        'm2iso+aglet': (
+            common_builtins,
+            iso_additional_builtins,
+            aglet_additional_builtins,
+        ),
+
+        # Builtins for GNU Modula-2 Extensions
+        'm2pim+gm2': (
+            common_builtins,
+            pim_additional_builtins,
+            gm2_additional_builtins,
+        ),
+
+        # Builtins for p1 Modula-2 Extensions
+        'm2iso+p1': (
+            common_builtins,
+            iso_additional_builtins,
+            p1_additional_builtins,
+        ),
+
+        # Builtins for XDS Modula-2 Extensions
+        'm2iso+xds': (
+            common_builtins,
+            iso_additional_builtins,
+            xds_additional_builtins,
+        ),
+    }
+
+    # Pseudo-Module Builtins Database
+    pseudo_builtins_db = {
+        # Builtins for unknown dialect
+        'unknown': (
+            common_pseudo_builtins,
+            pim_additional_pseudo_builtins,
+            iso_additional_pseudo_builtins,
+            m2r10_additional_pseudo_builtins,
+        ),
+
+        # Builtins for PIM Modula-2
+        'm2pim': (
+            common_pseudo_builtins,
+            pim_additional_pseudo_builtins,
+        ),
+
+        # Builtins for ISO Modula-2
+        'm2iso': (
+            common_pseudo_builtins,
+            iso_additional_pseudo_builtins,
+        ),
+
+        # Builtins for ISO Modula-2
+        'm2r10': (
+            common_pseudo_builtins,
+            m2r10_additional_pseudo_builtins,
+        ),
+
+        # Builtins for Objective Modula-2
+        'objm2': (
+            common_pseudo_builtins,
+            m2r10_additional_pseudo_builtins,
+            objm2_additional_pseudo_builtins,
+        ),
+
+        # Builtins for Aglet Modula-2 Extensions
+        'm2iso+aglet': (
+            common_pseudo_builtins,
+            iso_additional_pseudo_builtins,
+            aglet_additional_pseudo_builtins,
+        ),
+
+        # Builtins for GNU Modula-2 Extensions
+        'm2pim+gm2': (
+            common_pseudo_builtins,
+            pim_additional_pseudo_builtins,
+            gm2_additional_pseudo_builtins,
+        ),
+
+        # Builtins for p1 Modula-2 Extensions
+        'm2iso+p1': (
+            common_pseudo_builtins,
+            iso_additional_pseudo_builtins,
+            p1_additional_pseudo_builtins,
+        ),
+
+        # Builtins for XDS Modula-2 Extensions
+        'm2iso+xds': (
+            common_pseudo_builtins,
+            iso_additional_pseudo_builtins,
+            xds_additional_pseudo_builtins,
+        ),
+    }
+
+    # Standard Library ADTs Database
+    stdlib_adts_db = {
+        # Empty entry for unknown dialect
+        'unknown': (
+            # LEAVE THIS EMPTY
+        ),
+        # Standard Library ADTs for PIM Modula-2
+        'm2pim': (
+            # No first class library types
+        ),
+
+        # Standard Library ADTs for ISO Modula-2
+        'm2iso': (
+            # No first class library types
+        ),
+
+        # Standard Library ADTs for Modula-2 R10
+        'm2r10': (
+            m2r10_stdlib_adt_identifiers,
+        ),
+
+        # Standard Library ADTs for Objective Modula-2
+        'objm2': (
+            m2r10_stdlib_adt_identifiers,
+        ),
+
+        # Standard Library ADTs for Aglet Modula-2
+        'm2iso+aglet': (
+            # No first class library types
+        ),
+
+        # Standard Library ADTs for GNU Modula-2
+        'm2pim+gm2': (
+            # No first class library types
+        ),
+
+        # Standard Library ADTs for p1 Modula-2
+        'm2iso+p1': (
+            # No first class library types
+        ),
+
+        # Standard Library ADTs for XDS Modula-2
+        'm2iso+xds': (
+            # No first class library types
+        ),
+    }
+
+    # Standard Library Modules Database
+    stdlib_modules_db = {
+        # Empty entry for unknown dialect
+        'unknown': (
+            # LEAVE THIS EMPTY
+        ),
+        # Standard Library Modules for PIM Modula-2
+        'm2pim': (
+            pim_stdlib_module_identifiers,
+        ),
+
+        # Standard Library Modules for ISO Modula-2
+        'm2iso': (
+            iso_stdlib_module_identifiers,
+        ),
+
+        # Standard Library Modules for Modula-2 R10
+        'm2r10': (
+            m2r10_stdlib_blueprint_identifiers,
+            m2r10_stdlib_module_identifiers,
+            m2r10_stdlib_adt_identifiers,
+        ),
+
+        # Standard Library Modules for Objective Modula-2
+        'objm2': (
+            m2r10_stdlib_blueprint_identifiers,
+            m2r10_stdlib_module_identifiers,
+        ),
+
+        # Standard Library Modules for Aglet Modula-2
+        'm2iso+aglet': (
+            iso_stdlib_module_identifiers,
+        ),
+
+        # Standard Library Modules for GNU Modula-2
+        'm2pim+gm2': (
+            pim_stdlib_module_identifiers,
+        ),
+
+        # Standard Library Modules for p1 Modula-2
+        'm2iso+p1': (
+            iso_stdlib_module_identifiers,
+        ),
+
+        # Standard Library Modules for XDS Modula-2
+        'm2iso+xds': (
+            iso_stdlib_module_identifiers,
+        ),
+    }
+
+    # Standard Library Types Database
+    stdlib_types_db = {
+        # Empty entry for unknown dialect
+        'unknown': (
+            # LEAVE THIS EMPTY
+        ),
+        # Standard Library Types for PIM Modula-2
+        'm2pim': (
+            pim_stdlib_type_identifiers,
+        ),
+
+        # Standard Library Types for ISO Modula-2
+        'm2iso': (
+            iso_stdlib_type_identifiers,
+        ),
+
+        # Standard Library Types for Modula-2 R10
+        'm2r10': (
+            m2r10_stdlib_type_identifiers,
+        ),
+
+        # Standard Library Types for Objective Modula-2
+        'objm2': (
+            m2r10_stdlib_type_identifiers,
+        ),
+
+        # Standard Library Types for Aglet Modula-2
+        'm2iso+aglet': (
+            iso_stdlib_type_identifiers,
+        ),
+
+        # Standard Library Types for GNU Modula-2
+        'm2pim+gm2': (
+            pim_stdlib_type_identifiers,
+        ),
+
+        # Standard Library Types for p1 Modula-2
+        'm2iso+p1': (
+            iso_stdlib_type_identifiers,
+        ),
+
+        # Standard Library Types for XDS Modula-2
+        'm2iso+xds': (
+            iso_stdlib_type_identifiers,
+        ),
+    }
+
+    # Standard Library Procedures Database
+    stdlib_procedures_db = {
+        # Empty entry for unknown dialect
+        'unknown': (
+            # LEAVE THIS EMPTY
+        ),
+        # Standard Library Procedures for PIM Modula-2
+        'm2pim': (
+            pim_stdlib_proc_identifiers,
+        ),
+
+        # Standard Library Procedures for ISO Modula-2
+        'm2iso': (
+            iso_stdlib_proc_identifiers,
+        ),
+
+        # Standard Library Procedures for Modula-2 R10
+        'm2r10': (
+            m2r10_stdlib_proc_identifiers,
+        ),
+
+        # Standard Library Procedures for Objective Modula-2
+        'objm2': (
+            m2r10_stdlib_proc_identifiers,
+        ),
+
+        # Standard Library Procedures for Aglet Modula-2
+        'm2iso+aglet': (
+            iso_stdlib_proc_identifiers,
+        ),
+
+        # Standard Library Procedures for GNU Modula-2
+        'm2pim+gm2': (
+            pim_stdlib_proc_identifiers,
+        ),
+
+        # Standard Library Procedures for p1 Modula-2
+        'm2iso+p1': (
+            iso_stdlib_proc_identifiers,
+        ),
+
+        # Standard Library Procedures for XDS Modula-2
+        'm2iso+xds': (
+            iso_stdlib_proc_identifiers,
+        ),
+    }
+
+    # Standard Library Variables Database
+    stdlib_variables_db = {
+        # Empty entry for unknown dialect
+        'unknown': (
+            # LEAVE THIS EMPTY
+        ),
+        # Standard Library Variables for PIM Modula-2
+        'm2pim': (
+            pim_stdlib_var_identifiers,
+        ),
+
+        # Standard Library Variables for ISO Modula-2
+        'm2iso': (
+            iso_stdlib_var_identifiers,
+        ),
+
+        # Standard Library Variables for Modula-2 R10
+        'm2r10': (
+            m2r10_stdlib_var_identifiers,
+        ),
+
+        # Standard Library Variables for Objective Modula-2
+        'objm2': (
+            m2r10_stdlib_var_identifiers,
+        ),
+
+        # Standard Library Variables for Aglet Modula-2
+        'm2iso+aglet': (
+            iso_stdlib_var_identifiers,
+        ),
+
+        # Standard Library Variables for GNU Modula-2
+        'm2pim+gm2': (
+            pim_stdlib_var_identifiers,
+        ),
+
+        # Standard Library Variables for p1 Modula-2
+        'm2iso+p1': (
+            iso_stdlib_var_identifiers,
+        ),
+
+        # Standard Library Variables for XDS Modula-2
+        'm2iso+xds': (
+            iso_stdlib_var_identifiers,
+        ),
+    }
+
+    # Standard Library Constants Database
+    stdlib_constants_db = {
+        # Empty entry for unknown dialect
+        'unknown': (
+            # LEAVE THIS EMPTY
+        ),
+        # Standard Library Constants for PIM Modula-2
+        'm2pim': (
+            pim_stdlib_const_identifiers,
+        ),
+
+        # Standard Library Constants for ISO Modula-2
+        'm2iso': (
+            iso_stdlib_const_identifiers,
+        ),
+
+        # Standard Library Constants for Modula-2 R10
+        'm2r10': (
+            m2r10_stdlib_const_identifiers,
+        ),
+
+        # Standard Library Constants for Objective Modula-2
+        'objm2': (
+            m2r10_stdlib_const_identifiers,
+        ),
+
+        # Standard Library Constants for Aglet Modula-2
+        'm2iso+aglet': (
+            iso_stdlib_const_identifiers,
+        ),
+
+        # Standard Library Constants for GNU Modula-2
+        'm2pim+gm2': (
+            pim_stdlib_const_identifiers,
+        ),
+
+        # Standard Library Constants for p1 Modula-2
+        'm2iso+p1': (
+            iso_stdlib_const_identifiers,
+        ),
+
+        # Standard Library Constants for XDS Modula-2
+        'm2iso+xds': (
+            iso_stdlib_const_identifiers,
+        ),
+    }
+
+#   M e t h o d s
+
+    # initialise a lexer instance
+    def __init__(self, **options):
+        #
+        # check dialect options
+        #
+        dialects = get_list_opt(options, 'dialect', [])
+        #
+        for dialect_option in dialects:
+            if dialect_option in self.dialects[1:-1]:
+                # valid dialect option found
+                self.set_dialect(dialect_option)
+                break
+        #
+        # Fallback Mode (DEFAULT)
+        else:
+            # no valid dialect option
+            self.set_dialect('unknown')
+        #
+        self.dialect_set_by_tag = False
+        #
+        # check style options
+        #
+        styles = get_list_opt(options, 'style', [])
+        #
+        # use lowercase mode for Algol style
+        if 'algol' in styles or 'algol_nu' in styles:
+            self.algol_publication_mode = True
+        else:
+            self.algol_publication_mode = False
+        #
+        # Check option flags
+        #
+        self.treat_stdlib_adts_as_builtins = get_bool_opt(
+            options, 'treat_stdlib_adts_as_builtins', True)
+        #
+        # call superclass initialiser
+        RegexLexer.__init__(self, **options)
+
+    # Set lexer to a specified dialect
+    def set_dialect(self, dialect_id):
+        #
+        # if __debug__:
+        #    print 'entered set_dialect with arg: ', dialect_id
+        #
+        # check dialect name against known dialects
+        if dialect_id not in self.dialects:
+            dialect = 'unknown'  # default
+        else:
+            dialect = dialect_id
+        #
+        # compose lexemes to reject set
+        lexemes_to_reject_set = set()
+        # add each list of reject lexemes for this dialect
+        for list in self.lexemes_to_reject_db[dialect]:
+            lexemes_to_reject_set.update(set(list))
+        #
+        # compose reserved words set
+        reswords_set = set()
+        # add each list of reserved words for this dialect
+        for list in self.reserved_words_db[dialect]:
+            reswords_set.update(set(list))
+        #
+        # compose builtins set
+        builtins_set = set()
+        # add each list of builtins for this dialect excluding reserved words
+        for list in self.builtins_db[dialect]:
+            builtins_set.update(set(list).difference(reswords_set))
+        #
+        # compose pseudo-builtins set
+        pseudo_builtins_set = set()
+        # add each list of builtins for this dialect excluding reserved words
+        for list in self.pseudo_builtins_db[dialect]:
+            pseudo_builtins_set.update(set(list).difference(reswords_set))
+        #
+        # compose ADTs set
+        adts_set = set()
+        # add each list of ADTs for this dialect excluding reserved words
+        for list in self.stdlib_adts_db[dialect]:
+            adts_set.update(set(list).difference(reswords_set))
+        #
+        # compose modules set
+        modules_set = set()
+        # add each list of builtins for this dialect excluding builtins
+        for list in self.stdlib_modules_db[dialect]:
+            modules_set.update(set(list).difference(builtins_set))
+        #
+        # compose types set
+        types_set = set()
+        # add each list of types for this dialect excluding builtins
+        for list in self.stdlib_types_db[dialect]:
+            types_set.update(set(list).difference(builtins_set))
+        #
+        # compose procedures set
+        procedures_set = set()
+        # add each list of procedures for this dialect excluding builtins
+        for list in self.stdlib_procedures_db[dialect]:
+            procedures_set.update(set(list).difference(builtins_set))
+        #
+        # compose variables set
+        variables_set = set()
+        # add each list of variables for this dialect excluding builtins
+        for list in self.stdlib_variables_db[dialect]:
+            variables_set.update(set(list).difference(builtins_set))
+        #
+        # compose constants set
+        constants_set = set()
+        # add each list of constants for this dialect excluding builtins
+        for list in self.stdlib_constants_db[dialect]:
+            constants_set.update(set(list).difference(builtins_set))
+        #
+        # update lexer state
+        self.dialect = dialect
+        self.lexemes_to_reject = lexemes_to_reject_set
+        self.reserved_words = reswords_set
+        self.builtins = builtins_set
+        self.pseudo_builtins = pseudo_builtins_set
+        self.adts = adts_set
+        self.modules = modules_set
+        self.types = types_set
+        self.procedures = procedures_set
+        self.variables = variables_set
+        self.constants = constants_set
+        #
+        # if __debug__:
+        #    print 'exiting set_dialect'
+        #    print ' self.dialect: ', self.dialect
+        #    print ' self.lexemes_to_reject: ', self.lexemes_to_reject
+        #    print ' self.reserved_words: ', self.reserved_words
+        #    print ' self.builtins: ', self.builtins
+        #    print ' self.pseudo_builtins: ', self.pseudo_builtins
+        #    print ' self.adts: ', self.adts
+        #    print ' self.modules: ', self.modules
+        #    print ' self.types: ', self.types
+        #    print ' self.procedures: ', self.procedures
+        #    print ' self.variables: ', self.variables
+        #    print ' self.types: ', self.types
+        #    print ' self.constants: ', self.constants
+
+    # Extracts a dialect name from a dialect tag comment string  and checks
+    # the extracted name against known dialects.  If a match is found,  the
+    # matching name is returned, otherwise dialect id 'unknown' is returned
+    def get_dialect_from_dialect_tag(self, dialect_tag):
+        #
+        # if __debug__:
+        #    print 'entered get_dialect_from_dialect_tag with arg: ', dialect_tag
+        #
+        # constants
+        left_tag_delim = '(*!'
+        right_tag_delim = '*)'
+        left_tag_delim_len = len(left_tag_delim)
+        right_tag_delim_len = len(right_tag_delim)
+        indicator_start = left_tag_delim_len
+        indicator_end = -(right_tag_delim_len)
+        #
+        # check comment string for dialect indicator
+        if len(dialect_tag) > (left_tag_delim_len + right_tag_delim_len) \
+           and dialect_tag.startswith(left_tag_delim) \
+           and dialect_tag.endswith(right_tag_delim):
+            #
+            # if __debug__:
+            #    print 'dialect tag found'
+            #
+            # extract dialect indicator
+            indicator = dialect_tag[indicator_start:indicator_end]
+            #
+            # if __debug__:
+            #    print 'extracted: ', indicator
+            #
+            # check against known dialects
+            for index in range(1, len(self.dialects)):
+                #
+                # if __debug__:
+                #    print 'dialects[', index, ']: ', self.dialects[index]
+                #
+                if indicator == self.dialects[index]:
+                    #
+                    # if __debug__:
+                    #    print 'matching dialect found'
+                    #
+                    # indicator matches known dialect
+                    return indicator
+            else:
+                # indicator does not match any dialect
+                return 'unknown'  # default
+        else:
+            # invalid indicator string
+            return 'unknown'  # default
+
+    # intercept the token stream, modify token attributes and return them
+    def get_tokens_unprocessed(self, text):
+        for index, token, value in RegexLexer.get_tokens_unprocessed(self, text):
+            #
+            # check for dialect tag if dialect has not been set by tag
+            if not self.dialect_set_by_tag and token == Comment.Special:
+                indicated_dialect = self.get_dialect_from_dialect_tag(value)
+                if indicated_dialect != 'unknown':
+                    # token is a dialect indicator
+                    # reset reserved words and builtins
+                    self.set_dialect(indicated_dialect)
+                    self.dialect_set_by_tag = True
+            #
+            # check for reserved words, predefined and stdlib identifiers
+            if token is Name:
+                if value in self.reserved_words:
+                    token = Keyword.Reserved
+                    if self.algol_publication_mode:
+                        value = value.lower()
+                #
+                elif value in self.builtins:
+                    token = Name.Builtin
+                    if self.algol_publication_mode:
+                        value = value.lower()
+                #
+                elif value in self.pseudo_builtins:
+                    token = Name.Builtin.Pseudo
+                    if self.algol_publication_mode:
+                        value = value.lower()
+                #
+                elif value in self.adts:
+                    if not self.treat_stdlib_adts_as_builtins:
+                        token = Name.Namespace
+                    else:
+                        token = Name.Builtin.Pseudo
+                        if self.algol_publication_mode:
+                            value = value.lower()
+                #
+                elif value in self.modules:
+                    token = Name.Namespace
+                #
+                elif value in self.types:
+                    token = Name.Class
+                #
+                elif value in self.procedures:
+                    token = Name.Function
+                #
+                elif value in self.variables:
+                    token = Name.Variable
+                #
+                elif value in self.constants:
+                    token = Name.Constant
+            #
+            elif token in Number:
+                #
+                # mark prefix number literals as error for PIM and ISO dialects
+                if self.dialect not in ('unknown', 'm2r10', 'objm2'):
+                    if "'" in value or value[0:2] in ('0b', '0x', '0u'):
+                        token = Error
+                #
+                elif self.dialect in ('m2r10', 'objm2'):
+                    # mark base-8 number literals as errors for M2 R10 and ObjM2
+                    if token is Number.Oct:
+                        token = Error
+                    # mark suffix base-16 literals as errors for M2 R10 and ObjM2
+                    elif token is Number.Hex and 'H' in value:
+                        token = Error
+                    # mark real numbers with E as errors for M2 R10 and ObjM2
+                    elif token is Number.Float and 'E' in value:
+                        token = Error
+            #
+            elif token in Comment:
+                #
+                # mark single line comment as error for PIM and ISO dialects
+                if token is Comment.Single:
+                    if self.dialect not in ('unknown', 'm2r10', 'objm2'):
+                        token = Error
+                #
+                if token is Comment.Preproc:
+                    # mark ISO pragma as error for PIM dialects
+                    if value.startswith('<*') and \
+                       self.dialect.startswith('m2pim'):
+                        token = Error
+                    # mark PIM pragma as comment for other dialects
+                    elif value.startswith('(*$') and \
+                            self.dialect != 'unknown' and \
+                            not self.dialect.startswith('m2pim'):
+                        token = Comment.Multiline
+            #
+            else:  # token is neither Name nor Comment
+                #
+                # mark lexemes matching the dialect's error token set as errors
+                if value in self.lexemes_to_reject:
+                    token = Error
+                #
+                # substitute lexemes when in Algol mode
+                if self.algol_publication_mode:
+                    if value == '#':
+                        value = '≠'
+                    elif value == '<=':
+                        value = '≤'
+                    elif value == '>=':
+                        value = '≥'
+                    elif value == '==':
+                        value = '≡'
+                    elif value == '*.':
+                        value = '•'
+
+            # return result
+            yield index, token, value
+
+    def analyse_text(text):
+        """It's Pascal-like, but does not use FUNCTION -- uses PROCEDURE
+        instead."""
+
+        # Check if this looks like Pascal, if not, bail out early
+        if not ('(*' in text and '*)' in text and ':=' in text):
+            return
+
+        result = 0
+        # Procedure is in Modula2
+        if re.search(r'\bPROCEDURE\b', text):
+            result += 0.6
+
+        # FUNCTION is only valid in Pascal, but not in Modula2
+        if re.search(r'\bFUNCTION\b', text):
+            result = 0.0
+
+        return result
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/mojo.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/mojo.py
new file mode 100644
index 00000000..4df18c4f
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/mojo.py
@@ -0,0 +1,707 @@
+"""
+    pygments.lexers.mojo
+    ~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for Mojo and related languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import keyword
+
+from pygments import unistring as uni
+from pygments.lexer import (
+    RegexLexer,
+    bygroups,
+    combined,
+    default,
+    include,
+    this,
+    using,
+    words,
+)
+from pygments.token import (
+    Comment,
+    # Error,
+    Keyword,
+    Name,
+    Number,
+    Operator,
+    Punctuation,
+    String,
+    Text,
+    Whitespace,
+)
+from pygments.util import shebang_matches
+
+__all__ = ["MojoLexer"]
+
+
+class MojoLexer(RegexLexer):
+    """
+    For Mojo source code (version 24.2.1).
+    """
+
+    name = "Mojo"
+    url = "https://docs.modular.com/mojo/"
+    aliases = ["mojo", "🔥"]
+    filenames = [
+        "*.mojo",
+        "*.🔥",
+    ]
+    mimetypes = [
+        "text/x-mojo",
+        "application/x-mojo",
+    ]
+    version_added = "2.18"
+
+    uni_name = f"[{uni.xid_start}][{uni.xid_continue}]*"
+
+    def innerstring_rules(ttype):
+        return [
+            # the old style '%s' % (...) string formatting (still valid in Py3)
+            (
+                r"%(\(\w+\))?[-#0 +]*([0-9]+|[*])?(\.([0-9]+|[*]))?"
+                "[hlL]?[E-GXc-giorsaux%]",
+                String.Interpol,
+            ),
+            # the new style '{}'.format(...) string formatting
+            (
+                r"\{"
+                r"((\w+)((\.\w+)|(\[[^\]]+\]))*)?"  # field name
+                r"(\![sra])?"  # conversion
+                r"(\:(.?[<>=\^])?[-+ ]?#?0?(\d+)?,?(\.\d+)?[E-GXb-gnosx%]?)?"
+                r"\}",
+                String.Interpol,
+            ),
+            # backslashes, quotes and formatting signs must be parsed one at a time
+            (r'[^\\\'"%{\n]+', ttype),
+            (r'[\'"\\]', ttype),
+            # unhandled string formatting sign
+            (r"%|(\{{1,2})", ttype),
+            # newlines are an error (use "nl" state)
+        ]
+
+    def fstring_rules(ttype):
+        return [
+            # Assuming that a '}' is the closing brace after format specifier.
+            # Sadly, this means that we won't detect syntax error. But it's
+            # more important to parse correct syntax correctly, than to
+            # highlight invalid syntax.
+            (r"\}", String.Interpol),
+            (r"\{", String.Interpol, "expr-inside-fstring"),
+            # backslashes, quotes and formatting signs must be parsed one at a time
+            (r'[^\\\'"{}\n]+', ttype),
+            (r'[\'"\\]', ttype),
+            # newlines are an error (use "nl" state)
+        ]
+
+    tokens = {
+        "root": [
+            (r"\s+", Whitespace),
+            (
+                r'^(\s*)([rRuUbB]{,2})("""(?:.|\n)*?""")',
+                bygroups(Whitespace, String.Affix, String.Doc),
+            ),
+            (
+                r"^(\s*)([rRuUbB]{,2})('''(?:.|\n)*?''')",
+                bygroups(Whitespace, String.Affix, String.Doc),
+            ),
+            (r"\A#!.+$", Comment.Hashbang),
+            (r"#.*$", Comment.Single),
+            (r"\\\n", Whitespace),
+            (r"\\", Whitespace),
+            include("keywords"),
+            include("soft-keywords"),
+            # In the original PR, all the below here used ((?:\s|\\\s)+) to
+            # designate whitespace, but I can't find any example of this being
+            # needed in the example file, so we're replacing it with `\s+`.
+            (
+                r"(alias)(\s+)",
+                bygroups(Keyword, Whitespace),
+                "varname",  # TODO varname the right fit?
+            ),
+            (r"(var)(\s+)", bygroups(Keyword, Whitespace), "varname"),
+            (r"(def)(\s+)", bygroups(Keyword, Whitespace), "funcname"),
+            (r"(fn)(\s+)", bygroups(Keyword, Whitespace), "funcname"),
+            (
+                r"(class)(\s+)",
+                bygroups(Keyword, Whitespace),
+                "classname",
+            ),  # not implemented yet
+            (r"(struct)(\s+)", bygroups(Keyword, Whitespace), "structname"),
+            (r"(trait)(\s+)", bygroups(Keyword, Whitespace), "structname"),
+            (r"(from)(\s+)", bygroups(Keyword.Namespace, Whitespace), "fromimport"),
+            (r"(import)(\s+)", bygroups(Keyword.Namespace, Whitespace), "import"),
+            include("expr"),
+        ],
+        "expr": [
+            # raw f-strings
+            (
+                '(?i)(rf|fr)(""")',
+                bygroups(String.Affix, String.Double),
+                combined("rfstringescape", "tdqf"),
+            ),
+            (
+                "(?i)(rf|fr)(''')",
+                bygroups(String.Affix, String.Single),
+                combined("rfstringescape", "tsqf"),
+            ),
+            (
+                '(?i)(rf|fr)(")',
+                bygroups(String.Affix, String.Double),
+                combined("rfstringescape", "dqf"),
+            ),
+            (
+                "(?i)(rf|fr)(')",
+                bygroups(String.Affix, String.Single),
+                combined("rfstringescape", "sqf"),
+            ),
+            # non-raw f-strings
+            (
+                '([fF])(""")',
+                bygroups(String.Affix, String.Double),
+                combined("fstringescape", "tdqf"),
+            ),
+            (
+                "([fF])(''')",
+                bygroups(String.Affix, String.Single),
+                combined("fstringescape", "tsqf"),
+            ),
+            (
+                '([fF])(")',
+                bygroups(String.Affix, String.Double),
+                combined("fstringescape", "dqf"),
+            ),
+            (
+                "([fF])(')",
+                bygroups(String.Affix, String.Single),
+                combined("fstringescape", "sqf"),
+            ),
+            # raw bytes and strings
+            ('(?i)(rb|br|r)(""")', bygroups(String.Affix, String.Double), "tdqs"),
+            ("(?i)(rb|br|r)(''')", bygroups(String.Affix, String.Single), "tsqs"),
+            ('(?i)(rb|br|r)(")', bygroups(String.Affix, String.Double), "dqs"),
+            ("(?i)(rb|br|r)(')", bygroups(String.Affix, String.Single), "sqs"),
+            # non-raw strings
+            (
+                '([uU]?)(""")',
+                bygroups(String.Affix, String.Double),
+                combined("stringescape", "tdqs"),
+            ),
+            (
+                "([uU]?)(''')",
+                bygroups(String.Affix, String.Single),
+                combined("stringescape", "tsqs"),
+            ),
+            (
+                '([uU]?)(")',
+                bygroups(String.Affix, String.Double),
+                combined("stringescape", "dqs"),
+            ),
+            (
+                "([uU]?)(')",
+                bygroups(String.Affix, String.Single),
+                combined("stringescape", "sqs"),
+            ),
+            # non-raw bytes
+            (
+                '([bB])(""")',
+                bygroups(String.Affix, String.Double),
+                combined("bytesescape", "tdqs"),
+            ),
+            (
+                "([bB])(''')",
+                bygroups(String.Affix, String.Single),
+                combined("bytesescape", "tsqs"),
+            ),
+            (
+                '([bB])(")',
+                bygroups(String.Affix, String.Double),
+                combined("bytesescape", "dqs"),
+            ),
+            (
+                "([bB])(')",
+                bygroups(String.Affix, String.Single),
+                combined("bytesescape", "sqs"),
+            ),
+            (r"[^\S\n]+", Text),
+            include("numbers"),
+            (r"!=|==|<<|>>|:=|[-~+/*%=<>&^|.]", Operator),
+            (r"([]{}:\(\),;[])+", Punctuation),
+            (r"(in|is|and|or|not)\b", Operator.Word),
+            include("expr-keywords"),
+            include("builtins"),
+            include("magicfuncs"),
+            include("magicvars"),
+            include("name"),
+        ],
+        "expr-inside-fstring": [
+            (r"[{([]", Punctuation, "expr-inside-fstring-inner"),
+            # without format specifier
+            (
+                r"(=\s*)?"  # debug (https://bugs.python.org/issue36817)
+                r"(\![sraf])?"  # conversion
+                r"\}",
+                String.Interpol,
+                "#pop",
+            ),
+            # with format specifier
+            # we'll catch the remaining '}' in the outer scope
+            (
+                r"(=\s*)?"  # debug (https://bugs.python.org/issue36817)
+                r"(\![sraf])?"  # conversion
+                r":",
+                String.Interpol,
+                "#pop",
+            ),
+            (r"\s+", Whitespace),  # allow new lines
+            include("expr"),
+        ],
+        "expr-inside-fstring-inner": [
+            (r"[{([]", Punctuation, "expr-inside-fstring-inner"),
+            (r"[])}]", Punctuation, "#pop"),
+            (r"\s+", Whitespace),  # allow new lines
+            include("expr"),
+        ],
+        "expr-keywords": [
+            # Based on https://docs.python.org/3/reference/expressions.html
+            (
+                words(
+                    (
+                        "async for",  # TODO https://docs.modular.com/mojo/roadmap#no-async-for-or-async-with
+                        "async with",  # TODO https://docs.modular.com/mojo/roadmap#no-async-for-or-async-with
+                        "await",
+                        "else",
+                        "for",
+                        "if",
+                        "lambda",
+                        "yield",
+                        "yield from",
+                    ),
+                    suffix=r"\b",
+                ),
+                Keyword,
+            ),
+            (words(("True", "False", "None"), suffix=r"\b"), Keyword.Constant),
+        ],
+        "keywords": [
+            (
+                words(
+                    (
+                        "assert",
+                        "async",
+                        "await",
+                        "borrowed",
+                        "break",
+                        "continue",
+                        "del",
+                        "elif",
+                        "else",
+                        "except",
+                        "finally",
+                        "for",
+                        "global",
+                        "if",
+                        "lambda",
+                        "pass",
+                        "raise",
+                        "nonlocal",
+                        "return",
+                        "try",
+                        "while",
+                        "yield",
+                        "yield from",
+                        "as",
+                        "with",
+                    ),
+                    suffix=r"\b",
+                ),
+                Keyword,
+            ),
+            (words(("True", "False", "None"), suffix=r"\b"), Keyword.Constant),
+        ],
+        "soft-keywords": [
+            # `match`, `case` and `_` soft keywords
+            (
+                r"(^[ \t]*)"  # at beginning of line + possible indentation
+                r"(match|case)\b"  # a possible keyword
+                r"(?![ \t]*(?:"  # not followed by...
+                r"[:,;=^&|@~)\]}]|(?:" +  # characters and keywords that mean this isn't
+                # pattern matching (but None/True/False is ok)
+                r"|".join(k for k in keyword.kwlist if k[0].islower())
+                + r")\b))",
+                bygroups(Whitespace, Keyword),
+                "soft-keywords-inner",
+            ),
+        ],
+        "soft-keywords-inner": [
+            # optional `_` keyword
+            (r"(\s+)([^\n_]*)(_\b)", bygroups(Whitespace, using(this), Keyword)),
+            default("#pop"),
+        ],
+        "builtins": [
+            (
+                words(
+                    (
+                        "__import__",
+                        "abs",
+                        "aiter",
+                        "all",
+                        "any",
+                        "bin",
+                        "bool",
+                        "bytearray",
+                        "breakpoint",
+                        "bytes",
+                        "callable",
+                        "chr",
+                        "classmethod",
+                        "compile",
+                        "complex",
+                        "delattr",
+                        "dict",
+                        "dir",
+                        "divmod",
+                        "enumerate",
+                        "eval",
+                        "filter",
+                        "float",
+                        "format",
+                        "frozenset",
+                        "getattr",
+                        "globals",
+                        "hasattr",
+                        "hash",
+                        "hex",
+                        "id",
+                        "input",
+                        "int",
+                        "isinstance",
+                        "issubclass",
+                        "iter",
+                        "len",
+                        "list",
+                        "locals",
+                        "map",
+                        "max",
+                        "memoryview",
+                        "min",
+                        "next",
+                        "object",
+                        "oct",
+                        "open",
+                        "ord",
+                        "pow",
+                        "print",
+                        "property",
+                        "range",
+                        "repr",
+                        "reversed",
+                        "round",
+                        "set",
+                        "setattr",
+                        "slice",
+                        "sorted",
+                        "staticmethod",
+                        "str",
+                        "sum",
+                        "super",
+                        "tuple",
+                        "type",
+                        "vars",
+                        "zip",
+                        # Mojo builtin types: https://docs.modular.com/mojo/stdlib/builtin/
+                        "AnyType",
+                        "Coroutine",
+                        "DType",
+                        "Error",
+                        "Int",
+                        "List",
+                        "ListLiteral",
+                        "Scalar",
+                        "Int8",
+                        "UInt8",
+                        "Int16",
+                        "UInt16",
+                        "Int32",
+                        "UInt32",
+                        "Int64",
+                        "UInt64",
+                        "BFloat16",
+                        "Float16",
+                        "Float32",
+                        "Float64",
+                        "SIMD",
+                        "String",
+                        "Tensor",
+                        "Tuple",
+                        "Movable",
+                        "Copyable",
+                        "CollectionElement",
+                    ),
+                    prefix=r"(?>',
+    # Binary augmented
+    '+=', '-=', '*=', '/=', '%=', '**=', '&=', '|=', '^=', '<<=', '>>=',
+    # Comparison
+    '==', '!=', '<', '<=', '>', '>=', '<=>',
+    # Patterns and assignment
+    ':=', '?', '=~', '!~', '=>',
+    # Calls and sends
+    '.', '<-', '->',
+]
+_escape_pattern = (
+    r'(?:\\x[0-9a-fA-F]{2}|\\u[0-9a-fA-F]{4}|\\U[0-9a-fA-F]{8}|'
+    r'\\["\'\\bftnr])')
+# _char = _escape_chars + [('.', String.Char)]
+_identifier = r'[_a-zA-Z]\w*'
+
+_constants = [
+    # Void constants
+    'null',
+    # Bool constants
+    'false', 'true',
+    # Double constants
+    'Infinity', 'NaN',
+    # Special objects
+    'M', 'Ref', 'throw', 'traceln',
+]
+
+_guards = [
+    'Any', 'Binding', 'Bool', 'Bytes', 'Char', 'DeepFrozen', 'Double',
+    'Empty', 'Int', 'List', 'Map', 'Near', 'NullOk', 'Same', 'Selfless',
+    'Set', 'Str', 'SubrangeGuard', 'Transparent', 'Void',
+]
+
+_safeScope = [
+    '_accumulateList', '_accumulateMap', '_auditedBy', '_bind',
+    '_booleanFlow', '_comparer', '_equalizer', '_iterForever', '_loop',
+    '_makeBytes', '_makeDouble', '_makeFinalSlot', '_makeInt', '_makeList',
+    '_makeMap', '_makeMessageDesc', '_makeOrderedSpace', '_makeParamDesc',
+    '_makeProtocolDesc', '_makeSourceSpan', '_makeString', '_makeVarSlot',
+    '_makeVerbFacet', '_mapExtract', '_matchSame', '_quasiMatcher',
+    '_slotToBinding', '_splitList', '_suchThat', '_switchFailed',
+    '_validateFor', 'b__quasiParser', 'eval', 'import', 'm__quasiParser',
+    'makeBrandPair', 'makeLazySlot', 'safeScope', 'simple__quasiParser',
+]
+
+
+class MonteLexer(RegexLexer):
+    """
+    Lexer for the Monte programming language.
+    """
+    name = 'Monte'
+    url = 'https://monte.readthedocs.io/'
+    aliases = ['monte']
+    filenames = ['*.mt']
+    version_added = '2.2'
+
+    tokens = {
+        'root': [
+            # Comments
+            (r'#[^\n]*\n', Comment),
+
+            # Docstrings
+            # Apologies for the non-greedy matcher here.
+            (r'/\*\*.*?\*/', String.Doc),
+
+            # `var` declarations
+            (r'\bvar\b', Keyword.Declaration, 'var'),
+
+            # `interface` declarations
+            (r'\binterface\b', Keyword.Declaration, 'interface'),
+
+            # method declarations
+            (words(_methods, prefix='\\b', suffix='\\b'),
+             Keyword, 'method'),
+
+            # All other declarations
+            (words(_declarations, prefix='\\b', suffix='\\b'),
+             Keyword.Declaration),
+
+            # Keywords
+            (words(_keywords, prefix='\\b', suffix='\\b'), Keyword),
+
+            # Literals
+            ('[+-]?0x[_0-9a-fA-F]+', Number.Hex),
+            (r'[+-]?[_0-9]+\.[_0-9]*([eE][+-]?[_0-9]+)?', Number.Float),
+            ('[+-]?[_0-9]+', Number.Integer),
+            ("'", String.Double, 'char'),
+            ('"', String.Double, 'string'),
+
+            # Quasiliterals
+            ('`', String.Backtick, 'ql'),
+
+            # Operators
+            (words(_operators), Operator),
+
+            # Verb operators
+            (_identifier + '=', Operator.Word),
+
+            # Safe scope constants
+            (words(_constants, prefix='\\b', suffix='\\b'),
+             Keyword.Pseudo),
+
+            # Safe scope guards
+            (words(_guards, prefix='\\b', suffix='\\b'), Keyword.Type),
+
+            # All other safe scope names
+            (words(_safeScope, prefix='\\b', suffix='\\b'),
+             Name.Builtin),
+
+            # Identifiers
+            (_identifier, Name),
+
+            # Punctuation
+            (r'\(|\)|\{|\}|\[|\]|:|,', Punctuation),
+
+            # Whitespace
+            (' +', Whitespace),
+
+            # Definite lexer errors
+            ('=', Error),
+        ],
+        'char': [
+            # It is definitely an error to have a char of width == 0.
+            ("'", Error, 'root'),
+            (_escape_pattern, String.Escape, 'charEnd'),
+            ('.', String.Char, 'charEnd'),
+        ],
+        'charEnd': [
+            ("'", String.Char, '#pop:2'),
+            # It is definitely an error to have a char of width > 1.
+            ('.', Error),
+        ],
+        # The state of things coming into an interface.
+        'interface': [
+            (' +', Whitespace),
+            (_identifier, Name.Class, '#pop'),
+            include('root'),
+        ],
+        # The state of things coming into a method.
+        'method': [
+            (' +', Whitespace),
+            (_identifier, Name.Function, '#pop'),
+            include('root'),
+        ],
+        'string': [
+            ('"', String.Double, 'root'),
+            (_escape_pattern, String.Escape),
+            (r'\n', String.Double),
+            ('.', String.Double),
+        ],
+        'ql': [
+            ('`', String.Backtick, 'root'),
+            (r'\$' + _escape_pattern, String.Escape),
+            (r'\$\$', String.Escape),
+            (r'@@', String.Escape),
+            (r'\$\{', String.Interpol, 'qlNest'),
+            (r'@\{', String.Interpol, 'qlNest'),
+            (r'\$' + _identifier, Name),
+            ('@' + _identifier, Name),
+            ('.', String.Backtick),
+        ],
+        'qlNest': [
+            (r'\}', String.Interpol, '#pop'),
+            include('root'),
+        ],
+        # The state of things immediately following `var`.
+        'var': [
+            (' +', Whitespace),
+            (_identifier, Name.Variable, '#pop'),
+            include('root'),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/mosel.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/mosel.py
new file mode 100644
index 00000000..426c9a14
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/mosel.py
@@ -0,0 +1,447 @@
+"""
+    pygments.lexers.mosel
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for the mosel language.
+    http://www.fico.com/en/products/fico-xpress-optimization
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, words
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation
+
+__all__ = ['MoselLexer']
+
+FUNCTIONS = (
+    # core functions
+    '_',
+    'abs',
+    'arctan',
+    'asproc',
+    'assert',
+    'bitflip',
+    'bitneg',
+    'bitset',
+    'bitshift',
+    'bittest',
+    'bitval',
+    'ceil',
+    'cos',
+    'create',
+    'currentdate',
+    'currenttime',
+    'cutelt',
+    'cutfirst',
+    'cuthead',
+    'cutlast',
+    'cuttail',
+    'datablock',
+    'delcell',
+    'exists',
+    'exit',
+    'exp',
+    'exportprob',
+    'fclose',
+    'fflush',
+    'finalize',
+    'findfirst',
+    'findlast',
+    'floor',
+    'fopen',
+    'fselect',
+    'fskipline',
+    'fwrite',
+    'fwrite_',
+    'fwriteln',
+    'fwriteln_',
+    'getact',
+    'getcoeff',
+    'getcoeffs',
+    'getdual',
+    'getelt',
+    'getfid',
+    'getfirst',
+    'getfname',
+    'gethead',
+    'getlast',
+    'getobjval',
+    'getparam',
+    'getrcost',
+    'getreadcnt',
+    'getreverse',
+    'getsize',
+    'getslack',
+    'getsol',
+    'gettail',
+    'gettype',
+    'getvars',
+    'isdynamic',
+    'iseof',
+    'isfinite',
+    'ishidden',
+    'isinf',
+    'isnan',
+    'isodd',
+    'ln',
+    'localsetparam',
+    'log',
+    'makesos1',
+    'makesos2',
+    'maxlist',
+    'memoryuse',
+    'minlist',
+    'newmuid',
+    'publish',
+    'random',
+    'read',
+    'readln',
+    'reset',
+    'restoreparam',
+    'reverse',
+    'round',
+    'setcoeff',
+    'sethidden',
+    'setioerr',
+    'setmatherr',
+    'setname',
+    'setparam',
+    'setrandseed',
+    'setrange',
+    'settype',
+    'sin',
+    'splithead',
+    'splittail',
+    'sqrt',
+    'strfmt',
+    'substr',
+    'timestamp',
+    'unpublish',
+    'versionnum',
+    'versionstr',
+    'write',
+    'write_',
+    'writeln',
+    'writeln_',
+
+    # mosel exam mmxprs | sed -n -e "s/ [pf][a-z]* \([a-zA-Z0-9_]*\).*/'\1',/p" | sort -u
+    'addcut',
+    'addcuts',
+    'addmipsol',
+    'basisstability',
+    'calcsolinfo',
+    'clearmipdir',
+    'clearmodcut',
+    'command',
+    'copysoltoinit',
+    'crossoverlpsol',
+    'defdelayedrows',
+    'defsecurevecs',
+    'delcuts',
+    'dropcuts',
+    'estimatemarginals',
+    'fixglobal',
+    'flushmsgq',
+    'getbstat',
+    'getcnlist',
+    'getcplist',
+    'getdualray',
+    'getiis',
+    'getiissense',
+    'getiistype',
+    'getinfcause',
+    'getinfeas',
+    'getlb',
+    'getlct',
+    'getleft',
+    'getloadedlinctrs',
+    'getloadedmpvars',
+    'getname',
+    'getprimalray',
+    'getprobstat',
+    'getrange',
+    'getright',
+    'getsensrng',
+    'getsize',
+    'getsol',
+    'gettype',
+    'getub',
+    'getvars',
+    'gety',
+    'hasfeature',
+    'implies',
+    'indicator',
+    'initglobal',
+    'ishidden',
+    'isiisvalid',
+    'isintegral',
+    'loadbasis',
+    'loadcuts',
+    'loadlpsol',
+    'loadmipsol',
+    'loadprob',
+    'maximise',
+    'maximize',
+    'minimise',
+    'minimize',
+    'postsolve',
+    'readbasis',
+    'readdirs',
+    'readsol',
+    'refinemipsol',
+    'rejectintsol',
+    'repairinfeas',
+    'repairinfeas_deprec',
+    'resetbasis',
+    'resetiis',
+    'resetsol',
+    'savebasis',
+    'savemipsol',
+    'savesol',
+    'savestate',
+    'selectsol',
+    'setarchconsistency',
+    'setbstat',
+    'setcallback',
+    'setcbcutoff',
+    'setgndata',
+    'sethidden',
+    'setlb',
+    'setmipdir',
+    'setmodcut',
+    'setsol',
+    'setub',
+    'setucbdata',
+    'stopoptimise',
+    'stopoptimize',
+    'storecut',
+    'storecuts',
+    'unloadprob',
+    'uselastbarsol',
+    'writebasis',
+    'writedirs',
+    'writeprob',
+    'writesol',
+    'xor',
+    'xprs_addctr',
+    'xprs_addindic',
+
+    # mosel exam mmsystem | sed -n -e "s/ [pf][a-z]* \([a-zA-Z0-9_]*\).*/'\1',/p" | sort -u
+    'addmonths',
+    'copytext',
+    'cuttext',
+    'deltext',
+    'endswith',
+    'erase',
+    'expandpath',
+    'fcopy',
+    'fdelete',
+    'findfiles',
+    'findtext',
+    'fmove',
+    'formattext',
+    'getasnumber',
+    'getchar',
+    'getcwd',
+    'getdate',
+    'getday',
+    'getdaynum',
+    'getdays',
+    'getdirsep',
+    'getdsoparam',
+    'getendparse',
+    'getenv',
+    'getfsize',
+    'getfstat',
+    'getftime',
+    'gethour',
+    'getminute',
+    'getmonth',
+    'getmsec',
+    'getoserrmsg',
+    'getoserror',
+    'getpathsep',
+    'getqtype',
+    'getsecond',
+    'getsepchar',
+    'getsize',
+    'getstart',
+    'getsucc',
+    'getsysinfo',
+    'getsysstat',
+    'gettime',
+    'gettmpdir',
+    'gettrim',
+    'getweekday',
+    'getyear',
+    'inserttext',
+    'isvalid',
+    'jointext',
+    'makedir',
+    'makepath',
+    'newtar',
+    'newzip',
+    'nextfield',
+    'openpipe',
+    'parseextn',
+    'parseint',
+    'parsereal',
+    'parsetext',
+    'pastetext',
+    'pathmatch',
+    'pathsplit',
+    'qsort',
+    'quote',
+    'readtextline',
+    'regmatch',
+    'regreplace',
+    'removedir',
+    'removefiles',
+    'setchar',
+    'setdate',
+    'setday',
+    'setdsoparam',
+    'setendparse',
+    'setenv',
+    'sethour',
+    'setminute',
+    'setmonth',
+    'setmsec',
+    'setoserror',
+    'setqtype',
+    'setsecond',
+    'setsepchar',
+    'setstart',
+    'setsucc',
+    'settime',
+    'settrim',
+    'setyear',
+    'sleep',
+    'splittext',
+    'startswith',
+    'system',
+    'tarlist',
+    'textfmt',
+    'tolower',
+    'toupper',
+    'trim',
+    'untar',
+    'unzip',
+    'ziplist',
+
+    # mosel exam mmjobs | sed -n -e "s/ [pf][a-z]* \([a-zA-Z0-9_]*\).*/'\1',/p" | sort -u
+    'canceltimer',
+    'clearaliases',
+    'compile',
+    'connect',
+    'detach',
+    'disconnect',
+    'dropnextevent',
+    'findxsrvs',
+    'getaliases',
+    'getannidents',
+    'getannotations',
+    'getbanner',
+    'getclass',
+    'getdsoprop',
+    'getdsopropnum',
+    'getexitcode',
+    'getfromgid',
+    'getfromid',
+    'getfromuid',
+    'getgid',
+    'gethostalias',
+    'getid',
+    'getmodprop',
+    'getmodpropnum',
+    'getnextevent',
+    'getnode',
+    'getrmtid',
+    'getstatus',
+    'getsysinfo',
+    'gettimer',
+    'getuid',
+    'getvalue',
+    'isqueueempty',
+    'load',
+    'nullevent',
+    'peeknextevent',
+    'resetmodpar',
+    'run',
+    'send',
+    'setcontrol',
+    'setdefstream',
+    'setgid',
+    'sethostalias',
+    'setmodpar',
+    'settimer',
+    'setuid',
+    'setworkdir',
+    'stop',
+    'unload',
+    'wait',
+    'waitexpired',
+    'waitfor',
+    'waitforend',
+)
+
+
+class MoselLexer(RegexLexer):
+    """
+    For the Mosel optimization language.
+    """
+    name = 'Mosel'
+    aliases = ['mosel']
+    filenames = ['*.mos']
+    url = 'https://www.fico.com/fico-xpress-optimization/docs/latest/mosel/mosel_lang/dhtml/moselreflang.html'
+    version_added = '2.6'
+
+    tokens = {
+        'root': [
+            (r'\n', Text),
+            (r'\s+', Text.Whitespace),
+            (r'!.*?\n', Comment.Single),
+            (r'\(!(.|\n)*?!\)', Comment.Multiline),
+            (words((
+                'and', 'as', 'break', 'case', 'count', 'declarations', 'do',
+                'dynamic', 'elif', 'else', 'end-', 'end', 'evaluation', 'false',
+                'forall', 'forward', 'from', 'function', 'hashmap', 'if',
+                'imports', 'include', 'initialisations', 'initializations', 'inter',
+                'max', 'min', 'model', 'namespace', 'next', 'not', 'nsgroup',
+                'nssearch', 'of', 'options', 'or', 'package', 'parameters',
+                'procedure', 'public', 'prod', 'record', 'repeat', 'requirements',
+                'return', 'sum', 'then', 'to', 'true', 'union', 'until', 'uses',
+                'version', 'while', 'with'), prefix=r'\b', suffix=r'\b'),
+             Keyword.Builtin),
+            (words((
+                'range', 'array', 'set', 'list', 'mpvar', 'mpproblem', 'linctr',
+                'nlctr', 'integer', 'string', 'real', 'boolean', 'text', 'time',
+                'date', 'datetime', 'returned', 'Model', 'Mosel', 'counter',
+                'xmldoc', 'is_sos1', 'is_sos2', 'is_integer', 'is_binary',
+                'is_continuous', 'is_free', 'is_semcont', 'is_semint',
+                'is_partint'), prefix=r'\b', suffix=r'\b'),
+             Keyword.Type),
+            (r'(\+|\-|\*|/|=|<=|>=|\||\^|<|>|<>|\.\.|\.|:=|::|:|in|mod|div)',
+             Operator),
+            (r'[()\[\]{},;]+', Punctuation),
+            (words(FUNCTIONS,  prefix=r'\b', suffix=r'\b'), Name.Function),
+            (r'(\d+\.(?!\.)\d*|\.(?!.)\d+)([eE][+-]?\d+)?', Number.Float),
+            (r'\d+([eE][+-]?\d+)?', Number.Integer),
+            (r'[+-]?Infinity', Number.Integer),
+            (r'0[xX][0-9a-fA-F]+', Number),
+            (r'"', String.Double, 'double_quote'),
+            (r'\'', String.Single, 'single_quote'),
+            (r'(\w+|(\.(?!\.)))', Text),
+        ],
+        'single_quote': [
+            (r'\'', String.Single, '#pop'),
+            (r'[^\']+', String.Single),
+        ],
+        'double_quote': [
+            (r'(\\"|\\[0-7]{1,3}\D|\\[abfnrtv]|\\\\)', String.Escape),
+            (r'\"', String.Double, '#pop'),
+            (r'[^"\\]+', String.Double),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/ncl.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/ncl.py
new file mode 100644
index 00000000..d2f47608
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/ncl.py
@@ -0,0 +1,894 @@
+"""
+    pygments.lexers.ncl
+    ~~~~~~~~~~~~~~~~~~~
+
+    Lexers for NCAR Command Language.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, include, words
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation
+
+__all__ = ['NCLLexer']
+
+
+class NCLLexer(RegexLexer):
+    """
+    Lexer for NCL code.
+    """
+    name = 'NCL'
+    aliases = ['ncl']
+    filenames = ['*.ncl']
+    mimetypes = ['text/ncl']
+    url = 'https://www.ncl.ucar.edu'
+    version_added = '2.2'
+
+    flags = re.MULTILINE
+
+    tokens = {
+        'root': [
+            (r';.*\n', Comment),
+            include('strings'),
+            include('core'),
+            (r'[a-zA-Z_]\w*', Name),
+            include('nums'),
+            (r'[\s]+', Text),
+        ],
+        'core': [
+            # Statements
+            (words((
+                'begin', 'break', 'continue', 'create', 'defaultapp', 'do',
+                'else', 'end', 'external', 'exit', 'True', 'False', 'file', 'function',
+                'getvalues', 'graphic', 'group', 'if', 'list', 'load', 'local',
+                'new', '_Missing', 'Missing', 'noparent', 'procedure',
+                'quit', 'QUIT', 'Quit', 'record', 'return', 'setvalues', 'stop',
+                'then', 'while'), prefix=r'\b', suffix=r'\s*\b'),
+             Keyword),
+
+            # Data Types
+            (words((
+                'ubyte', 'uint', 'uint64', 'ulong', 'string', 'byte',
+                'character', 'double', 'float', 'integer', 'int64', 'logical',
+                'long', 'short', 'ushort', 'enumeric', 'numeric', 'snumeric'),
+                prefix=r'\b', suffix=r'\s*\b'),
+             Keyword.Type),
+
+            # Operators
+            (r'[\%^*+\-/<>]', Operator),
+
+            # punctuation:
+            (r'[\[\]():@$!&|.,\\{}]', Punctuation),
+            (r'[=:]', Punctuation),
+
+            # Intrinsics
+            (words((
+                'abs', 'acos', 'addfile', 'addfiles', 'all', 'angmom_atm', 'any',
+                'area_conserve_remap', 'area_hi2lores', 'area_poly_sphere',
+                'asciiread', 'asciiwrite', 'asin', 'atan', 'atan2', 'attsetvalues',
+                'avg', 'betainc', 'bin_avg', 'bin_sum', 'bw_bandpass_filter',
+                'cancor', 'cbinread', 'cbinwrite', 'cd_calendar', 'cd_inv_calendar',
+                'cdfbin_p', 'cdfbin_pr', 'cdfbin_s', 'cdfbin_xn', 'cdfchi_p',
+                'cdfchi_x', 'cdfgam_p', 'cdfgam_x', 'cdfnor_p', 'cdfnor_x',
+                'cdft_p', 'cdft_t', 'ceil', 'center_finite_diff',
+                'center_finite_diff_n', 'cfftb', 'cfftf', 'cfftf_frq_reorder',
+                'charactertodouble', 'charactertofloat', 'charactertointeger',
+                'charactertolong', 'charactertoshort', 'charactertostring',
+                'chartodouble', 'chartofloat', 'chartoint', 'chartointeger',
+                'chartolong', 'chartoshort', 'chartostring', 'chiinv', 'clear',
+                'color_index_to_rgba', 'conform', 'conform_dims', 'cos', 'cosh',
+                'count_unique_values', 'covcorm', 'covcorm_xy', 'craybinnumrec',
+                'craybinrecread', 'create_graphic', 'csa1', 'csa1d', 'csa1s',
+                'csa1x', 'csa1xd', 'csa1xs', 'csa2', 'csa2d', 'csa2l', 'csa2ld',
+                'csa2ls', 'csa2lx', 'csa2lxd', 'csa2lxs', 'csa2s', 'csa2x',
+                'csa2xd', 'csa2xs', 'csa3', 'csa3d', 'csa3l', 'csa3ld', 'csa3ls',
+                'csa3lx', 'csa3lxd', 'csa3lxs', 'csa3s', 'csa3x', 'csa3xd',
+                'csa3xs', 'csc2s', 'csgetp', 'css2c', 'cssetp', 'cssgrid', 'csstri',
+                'csvoro', 'cumsum', 'cz2ccm', 'datatondc', 'day_of_week',
+                'day_of_year', 'days_in_month', 'default_fillvalue', 'delete',
+                'depth_to_pres', 'destroy', 'determinant', 'dewtemp_trh',
+                'dgeevx_lapack', 'dim_acumrun_n', 'dim_avg', 'dim_avg_n',
+                'dim_avg_wgt', 'dim_avg_wgt_n', 'dim_cumsum', 'dim_cumsum_n',
+                'dim_gamfit_n', 'dim_gbits', 'dim_max', 'dim_max_n', 'dim_median',
+                'dim_median_n', 'dim_min', 'dim_min_n', 'dim_num', 'dim_num_n',
+                'dim_numrun_n', 'dim_pqsort', 'dim_pqsort_n', 'dim_product',
+                'dim_product_n', 'dim_rmsd', 'dim_rmsd_n', 'dim_rmvmean',
+                'dim_rmvmean_n', 'dim_rmvmed', 'dim_rmvmed_n', 'dim_spi_n',
+                'dim_standardize', 'dim_standardize_n', 'dim_stat4', 'dim_stat4_n',
+                'dim_stddev', 'dim_stddev_n', 'dim_sum', 'dim_sum_n', 'dim_sum_wgt',
+                'dim_sum_wgt_n', 'dim_variance', 'dim_variance_n', 'dimsizes',
+                'doubletobyte', 'doubletochar', 'doubletocharacter',
+                'doubletofloat', 'doubletoint', 'doubletointeger', 'doubletolong',
+                'doubletoshort', 'dpres_hybrid_ccm', 'dpres_plevel', 'draw',
+                'draw_color_palette', 'dsgetp', 'dsgrid2', 'dsgrid2d', 'dsgrid2s',
+                'dsgrid3', 'dsgrid3d', 'dsgrid3s', 'dspnt2', 'dspnt2d', 'dspnt2s',
+                'dspnt3', 'dspnt3d', 'dspnt3s', 'dssetp', 'dtrend', 'dtrend_msg',
+                'dtrend_msg_n', 'dtrend_n', 'dtrend_quadratic',
+                'dtrend_quadratic_msg_n', 'dv2uvf', 'dv2uvg', 'dz_height',
+                'echo_off', 'echo_on', 'eof2data', 'eof_varimax', 'eofcor',
+                'eofcor_pcmsg', 'eofcor_ts', 'eofcov', 'eofcov_pcmsg', 'eofcov_ts',
+                'eofunc', 'eofunc_ts', 'eofunc_varimax', 'equiv_sample_size', 'erf',
+                'erfc', 'esacr', 'esacv', 'esccr', 'esccv', 'escorc', 'escorc_n',
+                'escovc', 'exit', 'exp', 'exp_tapersh', 'exp_tapersh_wgts',
+                'exp_tapershC', 'ezfftb', 'ezfftb_n', 'ezfftf', 'ezfftf_n',
+                'f2fosh', 'f2foshv', 'f2fsh', 'f2fshv', 'f2gsh', 'f2gshv', 'fabs',
+                'fbindirread', 'fbindirwrite', 'fbinnumrec', 'fbinread',
+                'fbinrecread', 'fbinrecwrite', 'fbinwrite', 'fft2db', 'fft2df',
+                'fftshift', 'fileattdef', 'filechunkdimdef', 'filedimdef',
+                'fileexists', 'filegrpdef', 'filevarattdef', 'filevarchunkdef',
+                'filevarcompressleveldef', 'filevardef', 'filevardimsizes',
+                'filwgts_lancos', 'filwgts_lanczos', 'filwgts_normal',
+                'floattobyte', 'floattochar', 'floattocharacter', 'floattoint',
+                'floattointeger', 'floattolong', 'floattoshort', 'floor',
+                'fluxEddy', 'fo2fsh', 'fo2fshv', 'fourier_info', 'frame', 'fspan',
+                'ftcurv', 'ftcurvd', 'ftcurvi', 'ftcurvp', 'ftcurvpi', 'ftcurvps',
+                'ftcurvs', 'ftest', 'ftgetp', 'ftkurv', 'ftkurvd', 'ftkurvp',
+                'ftkurvpd', 'ftsetp', 'ftsurf', 'g2fsh', 'g2fshv', 'g2gsh',
+                'g2gshv', 'gamma', 'gammainc', 'gaus', 'gaus_lobat',
+                'gaus_lobat_wgt', 'gc_aangle', 'gc_clkwise', 'gc_dangle',
+                'gc_inout', 'gc_latlon', 'gc_onarc', 'gc_pnt2gc', 'gc_qarea',
+                'gc_tarea', 'generate_2d_array', 'get_color_index',
+                'get_color_rgba', 'get_cpu_time', 'get_isolines', 'get_ncl_version',
+                'get_script_name', 'get_script_prefix_name', 'get_sphere_radius',
+                'get_unique_values', 'getbitsone', 'getenv', 'getfiledimsizes',
+                'getfilegrpnames', 'getfilepath', 'getfilevaratts',
+                'getfilevarchunkdimsizes', 'getfilevardims', 'getfilevardimsizes',
+                'getfilevarnames', 'getfilevartypes', 'getvaratts', 'getvardims',
+                'gradsf', 'gradsg', 'greg2jul', 'grid2triple', 'hlsrgb', 'hsvrgb',
+                'hydro', 'hyi2hyo', 'idsfft', 'igradsf', 'igradsg', 'ilapsf',
+                'ilapsg', 'ilapvf', 'ilapvg', 'ind', 'ind_resolve', 'int2p',
+                'int2p_n', 'integertobyte', 'integertochar', 'integertocharacter',
+                'integertoshort', 'inttobyte', 'inttochar', 'inttoshort',
+                'inverse_matrix', 'isatt', 'isbigendian', 'isbyte', 'ischar',
+                'iscoord', 'isdefined', 'isdim', 'isdimnamed', 'isdouble',
+                'isenumeric', 'isfile', 'isfilepresent', 'isfilevar',
+                'isfilevaratt', 'isfilevarcoord', 'isfilevardim', 'isfloat',
+                'isfunc', 'isgraphic', 'isint', 'isint64', 'isinteger',
+                'isleapyear', 'islogical', 'islong', 'ismissing', 'isnan_ieee',
+                'isnumeric', 'ispan', 'isproc', 'isshort', 'issnumeric', 'isstring',
+                'isubyte', 'isuint', 'isuint64', 'isulong', 'isunlimited',
+                'isunsigned', 'isushort', 'isvar', 'jul2greg', 'kmeans_as136',
+                'kolsm2_n', 'kron_product', 'lapsf', 'lapsg', 'lapvf', 'lapvg',
+                'latlon2utm', 'lclvl', 'lderuvf', 'lderuvg', 'linint1', 'linint1_n',
+                'linint2', 'linint2_points', 'linmsg', 'linmsg_n', 'linrood_latwgt',
+                'linrood_wgt', 'list_files', 'list_filevars', 'list_hlus',
+                'list_procfuncs', 'list_vars', 'ListAppend', 'ListCount',
+                'ListGetType', 'ListIndex', 'ListIndexFromName', 'ListPop',
+                'ListPush', 'ListSetType', 'loadscript', 'local_max', 'local_min',
+                'log', 'log10', 'longtobyte', 'longtochar', 'longtocharacter',
+                'longtoint', 'longtointeger', 'longtoshort', 'lspoly', 'lspoly_n',
+                'mask', 'max', 'maxind', 'min', 'minind', 'mixed_layer_depth',
+                'mixhum_ptd', 'mixhum_ptrh', 'mjo_cross_coh2pha',
+                'mjo_cross_segment', 'moc_globe_atl', 'monthday', 'natgrid',
+                'natgridd', 'natgrids', 'ncargpath', 'ncargversion', 'ndctodata',
+                'ndtooned', 'new', 'NewList', 'ngezlogo', 'nggcog', 'nggetp',
+                'nglogo', 'ngsetp', 'NhlAddAnnotation', 'NhlAddData',
+                'NhlAddOverlay', 'NhlAddPrimitive', 'NhlAppGetDefaultParentId',
+                'NhlChangeWorkstation', 'NhlClassName', 'NhlClearWorkstation',
+                'NhlDataPolygon', 'NhlDataPolyline', 'NhlDataPolymarker',
+                'NhlDataToNDC', 'NhlDestroy', 'NhlDraw', 'NhlFrame', 'NhlFreeColor',
+                'NhlGetBB', 'NhlGetClassResources', 'NhlGetErrorObjectId',
+                'NhlGetNamedColorIndex', 'NhlGetParentId',
+                'NhlGetParentWorkstation', 'NhlGetWorkspaceObjectId',
+                'NhlIsAllocatedColor', 'NhlIsApp', 'NhlIsDataComm', 'NhlIsDataItem',
+                'NhlIsDataSpec', 'NhlIsTransform', 'NhlIsView', 'NhlIsWorkstation',
+                'NhlName', 'NhlNDCPolygon', 'NhlNDCPolyline', 'NhlNDCPolymarker',
+                'NhlNDCToData', 'NhlNewColor', 'NhlNewDashPattern', 'NhlNewMarker',
+                'NhlPalGetDefined', 'NhlRemoveAnnotation', 'NhlRemoveData',
+                'NhlRemoveOverlay', 'NhlRemovePrimitive', 'NhlSetColor',
+                'NhlSetDashPattern', 'NhlSetMarker', 'NhlUpdateData',
+                'NhlUpdateWorkstation', 'nice_mnmxintvl', 'nngetaspectd',
+                'nngetaspects', 'nngetp', 'nngetsloped', 'nngetslopes', 'nngetwts',
+                'nngetwtsd', 'nnpnt', 'nnpntd', 'nnpntend', 'nnpntendd',
+                'nnpntinit', 'nnpntinitd', 'nnpntinits', 'nnpnts', 'nnsetp', 'num',
+                'obj_anal_ic', 'omega_ccm', 'onedtond', 'overlay', 'paleo_outline',
+                'pdfxy_bin', 'poisson_grid_fill', 'pop_remap', 'potmp_insitu_ocn',
+                'prcwater_dp', 'pres2hybrid', 'pres_hybrid_ccm', 'pres_sigma',
+                'print', 'print_table', 'printFileVarSummary', 'printVarSummary',
+                'product', 'pslec', 'pslhor', 'pslhyp', 'qsort', 'rand',
+                'random_chi', 'random_gamma', 'random_normal', 'random_setallseed',
+                'random_uniform', 'rcm2points', 'rcm2rgrid', 'rdsstoi',
+                'read_colormap_file', 'reg_multlin', 'regcoef', 'regCoef_n',
+                'regline', 'relhum', 'replace_ieeenan', 'reshape', 'reshape_ind',
+                'rgba_to_color_index', 'rgbhls', 'rgbhsv', 'rgbyiq', 'rgrid2rcm',
+                'rhomb_trunc', 'rip_cape_2d', 'rip_cape_3d', 'round', 'rtest',
+                'runave', 'runave_n', 'set_default_fillvalue', 'set_sphere_radius',
+                'setfileoption', 'sfvp2uvf', 'sfvp2uvg', 'shaec', 'shagc',
+                'shgetnp', 'shgetp', 'shgrid', 'shorttobyte', 'shorttochar',
+                'shorttocharacter', 'show_ascii', 'shsec', 'shsetp', 'shsgc',
+                'shsgc_R42', 'sigma2hybrid', 'simpeq', 'simpne', 'sin',
+                'sindex_yrmo', 'sinh', 'sizeof', 'sleep', 'smth9', 'snindex_yrmo',
+                'solve_linsys', 'span_color_indexes', 'span_color_rgba',
+                'sparse_matrix_mult', 'spcorr', 'spcorr_n', 'specx_anal',
+                'specxy_anal', 'spei', 'sprintf', 'sprinti', 'sqrt', 'sqsort',
+                'srand', 'stat2', 'stat4', 'stat_medrng', 'stat_trim',
+                'status_exit', 'stdatmus_p2tdz', 'stdatmus_z2tdp', 'stddev',
+                'str_capital', 'str_concat', 'str_fields_count', 'str_get_cols',
+                'str_get_dq', 'str_get_field', 'str_get_nl', 'str_get_sq',
+                'str_get_tab', 'str_index_of_substr', 'str_insert', 'str_is_blank',
+                'str_join', 'str_left_strip', 'str_lower', 'str_match',
+                'str_match_ic', 'str_match_ic_regex', 'str_match_ind',
+                'str_match_ind_ic', 'str_match_ind_ic_regex', 'str_match_ind_regex',
+                'str_match_regex', 'str_right_strip', 'str_split',
+                'str_split_by_length', 'str_split_csv', 'str_squeeze', 'str_strip',
+                'str_sub_str', 'str_switch', 'str_upper', 'stringtochar',
+                'stringtocharacter', 'stringtodouble', 'stringtofloat',
+                'stringtoint', 'stringtointeger', 'stringtolong', 'stringtoshort',
+                'strlen', 'student_t', 'sum', 'svd_lapack', 'svdcov', 'svdcov_sv',
+                'svdstd', 'svdstd_sv', 'system', 'systemfunc', 'tan', 'tanh',
+                'taper', 'taper_n', 'tdclrs', 'tdctri', 'tdcudp', 'tdcurv',
+                'tddtri', 'tdez2d', 'tdez3d', 'tdgetp', 'tdgrds', 'tdgrid',
+                'tdgtrs', 'tdinit', 'tditri', 'tdlbla', 'tdlblp', 'tdlbls',
+                'tdline', 'tdlndp', 'tdlnpa', 'tdlpdp', 'tdmtri', 'tdotri',
+                'tdpara', 'tdplch', 'tdprpa', 'tdprpi', 'tdprpt', 'tdsetp',
+                'tdsort', 'tdstri', 'tdstrs', 'tdttri', 'thornthwaite', 'tobyte',
+                'tochar', 'todouble', 'tofloat', 'toint', 'toint64', 'tointeger',
+                'tolong', 'toshort', 'tosigned', 'tostring', 'tostring_with_format',
+                'totype', 'toubyte', 'touint', 'touint64', 'toulong', 'tounsigned',
+                'toushort', 'trend_manken', 'tri_trunc', 'triple2grid',
+                'triple2grid2d', 'trop_wmo', 'ttest', 'typeof', 'undef',
+                'unique_string', 'update', 'ushorttoint', 'ut_calendar',
+                'ut_inv_calendar', 'utm2latlon', 'uv2dv_cfd', 'uv2dvf', 'uv2dvg',
+                'uv2sfvpf', 'uv2sfvpg', 'uv2vr_cfd', 'uv2vrdvf', 'uv2vrdvg',
+                'uv2vrf', 'uv2vrg', 'v5d_close', 'v5d_create', 'v5d_setLowLev',
+                'v5d_setUnits', 'v5d_write', 'v5d_write_var', 'variance', 'vhaec',
+                'vhagc', 'vhsec', 'vhsgc', 'vibeta', 'vinth2p', 'vinth2p_ecmwf',
+                'vinth2p_ecmwf_nodes', 'vinth2p_nodes', 'vintp2p_ecmwf', 'vr2uvf',
+                'vr2uvg', 'vrdv2uvf', 'vrdv2uvg', 'wavelet', 'wavelet_default',
+                'weibull', 'wgt_area_smooth', 'wgt_areaave', 'wgt_areaave2',
+                'wgt_arearmse', 'wgt_arearmse2', 'wgt_areasum2', 'wgt_runave',
+                'wgt_runave_n', 'wgt_vert_avg_beta', 'wgt_volave', 'wgt_volave_ccm',
+                'wgt_volrmse', 'wgt_volrmse_ccm', 'where', 'wk_smooth121', 'wmbarb',
+                'wmbarbmap', 'wmdrft', 'wmgetp', 'wmlabs', 'wmsetp', 'wmstnm',
+                'wmvect', 'wmvectmap', 'wmvlbl', 'wrf_avo', 'wrf_cape_2d',
+                'wrf_cape_3d', 'wrf_dbz', 'wrf_eth', 'wrf_helicity', 'wrf_ij_to_ll',
+                'wrf_interp_1d', 'wrf_interp_2d_xy', 'wrf_interp_3d_z',
+                'wrf_latlon_to_ij', 'wrf_ll_to_ij', 'wrf_omega', 'wrf_pvo',
+                'wrf_rh', 'wrf_slp', 'wrf_smooth_2d', 'wrf_td', 'wrf_tk',
+                'wrf_updraft_helicity', 'wrf_uvmet', 'wrf_virtual_temp',
+                'wrf_wetbulb', 'wrf_wps_close_int', 'wrf_wps_open_int',
+                'wrf_wps_rddata_int', 'wrf_wps_rdhead_int', 'wrf_wps_read_int',
+                'wrf_wps_write_int', 'write_matrix', 'write_table', 'yiqrgb',
+                'z2geouv', 'zonal_mpsi', 'addfiles_GetVar', 'advect_variable',
+                'area_conserve_remap_Wrap', 'area_hi2lores_Wrap',
+                'array_append_record', 'assignFillValue', 'byte2flt',
+                'byte2flt_hdf', 'calcDayAnomTLL', 'calcMonAnomLLLT',
+                'calcMonAnomLLT', 'calcMonAnomTLL', 'calcMonAnomTLLL',
+                'calculate_monthly_values', 'cd_convert', 'changeCase',
+                'changeCaseChar', 'clmDayTLL', 'clmDayTLLL', 'clmMon2clmDay',
+                'clmMonLLLT', 'clmMonLLT', 'clmMonTLL', 'clmMonTLLL', 'closest_val',
+                'copy_VarAtts', 'copy_VarCoords', 'copy_VarCoords_1',
+                'copy_VarCoords_2', 'copy_VarMeta', 'copyatt', 'crossp3',
+                'cshstringtolist', 'cssgrid_Wrap', 'dble2flt', 'decimalPlaces',
+                'delete_VarAtts', 'dim_avg_n_Wrap', 'dim_avg_wgt_n_Wrap',
+                'dim_avg_wgt_Wrap', 'dim_avg_Wrap', 'dim_cumsum_n_Wrap',
+                'dim_cumsum_Wrap', 'dim_max_n_Wrap', 'dim_min_n_Wrap',
+                'dim_rmsd_n_Wrap', 'dim_rmsd_Wrap', 'dim_rmvmean_n_Wrap',
+                'dim_rmvmean_Wrap', 'dim_rmvmed_n_Wrap', 'dim_rmvmed_Wrap',
+                'dim_standardize_n_Wrap', 'dim_standardize_Wrap',
+                'dim_stddev_n_Wrap', 'dim_stddev_Wrap', 'dim_sum_n_Wrap',
+                'dim_sum_wgt_n_Wrap', 'dim_sum_wgt_Wrap', 'dim_sum_Wrap',
+                'dim_variance_n_Wrap', 'dim_variance_Wrap', 'dpres_plevel_Wrap',
+                'dtrend_leftdim', 'dv2uvF_Wrap', 'dv2uvG_Wrap', 'eof_north',
+                'eofcor_Wrap', 'eofcov_Wrap', 'eofunc_north', 'eofunc_ts_Wrap',
+                'eofunc_varimax_reorder', 'eofunc_varimax_Wrap', 'eofunc_Wrap',
+                'epsZero', 'f2fosh_Wrap', 'f2foshv_Wrap', 'f2fsh_Wrap',
+                'f2fshv_Wrap', 'f2gsh_Wrap', 'f2gshv_Wrap', 'fbindirSwap',
+                'fbinseqSwap1', 'fbinseqSwap2', 'flt2dble', 'flt2string',
+                'fo2fsh_Wrap', 'fo2fshv_Wrap', 'g2fsh_Wrap', 'g2fshv_Wrap',
+                'g2gsh_Wrap', 'g2gshv_Wrap', 'generate_resample_indices',
+                'generate_sample_indices', 'generate_unique_indices',
+                'genNormalDist', 'get1Dindex', 'get1Dindex_Collapse',
+                'get1Dindex_Exclude', 'get_file_suffix', 'GetFillColor',
+                'GetFillColorIndex', 'getFillValue', 'getind_latlon2d',
+                'getVarDimNames', 'getVarFillValue', 'grib_stime2itime',
+                'hyi2hyo_Wrap', 'ilapsF_Wrap', 'ilapsG_Wrap', 'ind_nearest_coord',
+                'indStrSubset', 'int2dble', 'int2flt', 'int2p_n_Wrap', 'int2p_Wrap',
+                'isMonotonic', 'isStrSubset', 'latGau', 'latGauWgt', 'latGlobeF',
+                'latGlobeFo', 'latRegWgt', 'linint1_n_Wrap', 'linint1_Wrap',
+                'linint2_points_Wrap', 'linint2_Wrap', 'local_max_1d',
+                'local_min_1d', 'lonFlip', 'lonGlobeF', 'lonGlobeFo', 'lonPivot',
+                'merge_levels_sfc', 'mod', 'month_to_annual',
+                'month_to_annual_weighted', 'month_to_season', 'month_to_season12',
+                'month_to_seasonN', 'monthly_total_to_daily_mean', 'nameDim',
+                'natgrid_Wrap', 'NewCosWeight', 'niceLatLon2D', 'NormCosWgtGlobe',
+                'numAsciiCol', 'numAsciiRow', 'numeric2int',
+                'obj_anal_ic_deprecated', 'obj_anal_ic_Wrap', 'omega_ccm_driver',
+                'omega_to_w', 'oneDtostring', 'pack_values', 'pattern_cor', 'pdfx',
+                'pdfxy', 'pdfxy_conform', 'pot_temp', 'pot_vort_hybrid',
+                'pot_vort_isobaric', 'pres2hybrid_Wrap', 'print_clock',
+                'printMinMax', 'quadroots', 'rcm2points_Wrap', 'rcm2rgrid_Wrap',
+                'readAsciiHead', 'readAsciiTable', 'reg_multlin_stats',
+                'region_ind', 'regline_stats', 'relhum_ttd', 'replaceSingleChar',
+                'RGBtoCmap', 'rgrid2rcm_Wrap', 'rho_mwjf', 'rm_single_dims',
+                'rmAnnCycle1D', 'rmInsufData', 'rmMonAnnCycLLLT', 'rmMonAnnCycLLT',
+                'rmMonAnnCycTLL', 'runave_n_Wrap', 'runave_Wrap', 'short2flt',
+                'short2flt_hdf', 'shsgc_R42_Wrap', 'sign_f90', 'sign_matlab',
+                'smth9_Wrap', 'smthClmDayTLL', 'smthClmDayTLLL', 'SqrtCosWeight',
+                'stat_dispersion', 'static_stability', 'stdMonLLLT', 'stdMonLLT',
+                'stdMonTLL', 'stdMonTLLL', 'symMinMaxPlt', 'table_attach_columns',
+                'table_attach_rows', 'time_to_newtime', 'transpose',
+                'triple2grid_Wrap', 'ut_convert', 'uv2dvF_Wrap', 'uv2dvG_Wrap',
+                'uv2vrF_Wrap', 'uv2vrG_Wrap', 'vr2uvF_Wrap', 'vr2uvG_Wrap',
+                'w_to_omega', 'wallClockElapseTime', 'wave_number_spc',
+                'wgt_areaave_Wrap', 'wgt_runave_leftdim', 'wgt_runave_n_Wrap',
+                'wgt_runave_Wrap', 'wgt_vertical_n', 'wind_component',
+                'wind_direction', 'yyyyddd_to_yyyymmdd', 'yyyymm_time',
+                'yyyymm_to_yyyyfrac', 'yyyymmdd_time', 'yyyymmdd_to_yyyyddd',
+                'yyyymmdd_to_yyyyfrac', 'yyyymmddhh_time', 'yyyymmddhh_to_yyyyfrac',
+                'zonal_mpsi_Wrap', 'zonalAve', 'calendar_decode2', 'cd_string',
+                'kf_filter', 'run_cor', 'time_axis_labels', 'ut_string',
+                'wrf_contour', 'wrf_map', 'wrf_map_overlay', 'wrf_map_overlays',
+                'wrf_map_resources', 'wrf_map_zoom', 'wrf_overlay', 'wrf_overlays',
+                'wrf_user_getvar', 'wrf_user_ij_to_ll', 'wrf_user_intrp2d',
+                'wrf_user_intrp3d', 'wrf_user_latlon_to_ij', 'wrf_user_list_times',
+                'wrf_user_ll_to_ij', 'wrf_user_unstagger', 'wrf_user_vert_interp',
+                'wrf_vector', 'gsn_add_annotation', 'gsn_add_polygon',
+                'gsn_add_polyline', 'gsn_add_polymarker',
+                'gsn_add_shapefile_polygons', 'gsn_add_shapefile_polylines',
+                'gsn_add_shapefile_polymarkers', 'gsn_add_text', 'gsn_attach_plots',
+                'gsn_blank_plot', 'gsn_contour', 'gsn_contour_map',
+                'gsn_contour_shade', 'gsn_coordinates', 'gsn_create_labelbar',
+                'gsn_create_legend', 'gsn_create_text',
+                'gsn_csm_attach_zonal_means', 'gsn_csm_blank_plot',
+                'gsn_csm_contour', 'gsn_csm_contour_map', 'gsn_csm_contour_map_ce',
+                'gsn_csm_contour_map_overlay', 'gsn_csm_contour_map_polar',
+                'gsn_csm_hov', 'gsn_csm_lat_time', 'gsn_csm_map', 'gsn_csm_map_ce',
+                'gsn_csm_map_polar', 'gsn_csm_pres_hgt',
+                'gsn_csm_pres_hgt_streamline', 'gsn_csm_pres_hgt_vector',
+                'gsn_csm_streamline', 'gsn_csm_streamline_contour_map',
+                'gsn_csm_streamline_contour_map_ce',
+                'gsn_csm_streamline_contour_map_polar', 'gsn_csm_streamline_map',
+                'gsn_csm_streamline_map_ce', 'gsn_csm_streamline_map_polar',
+                'gsn_csm_streamline_scalar', 'gsn_csm_streamline_scalar_map',
+                'gsn_csm_streamline_scalar_map_ce',
+                'gsn_csm_streamline_scalar_map_polar', 'gsn_csm_time_lat',
+                'gsn_csm_vector', 'gsn_csm_vector_map', 'gsn_csm_vector_map_ce',
+                'gsn_csm_vector_map_polar', 'gsn_csm_vector_scalar',
+                'gsn_csm_vector_scalar_map', 'gsn_csm_vector_scalar_map_ce',
+                'gsn_csm_vector_scalar_map_polar', 'gsn_csm_x2y', 'gsn_csm_x2y2',
+                'gsn_csm_xy', 'gsn_csm_xy2', 'gsn_csm_xy3', 'gsn_csm_y',
+                'gsn_define_colormap', 'gsn_draw_colormap', 'gsn_draw_named_colors',
+                'gsn_histogram', 'gsn_labelbar_ndc', 'gsn_legend_ndc', 'gsn_map',
+                'gsn_merge_colormaps', 'gsn_open_wks', 'gsn_panel', 'gsn_polygon',
+                'gsn_polygon_ndc', 'gsn_polyline', 'gsn_polyline_ndc',
+                'gsn_polymarker', 'gsn_polymarker_ndc', 'gsn_retrieve_colormap',
+                'gsn_reverse_colormap', 'gsn_streamline', 'gsn_streamline_map',
+                'gsn_streamline_scalar', 'gsn_streamline_scalar_map', 'gsn_table',
+                'gsn_text', 'gsn_text_ndc', 'gsn_vector', 'gsn_vector_map',
+                'gsn_vector_scalar', 'gsn_vector_scalar_map', 'gsn_xy', 'gsn_y',
+                'hsv2rgb', 'maximize_output', 'namedcolor2rgb', 'namedcolor2rgba',
+                'reset_device_coordinates', 'span_named_colors'), prefix=r'\b'),
+             Name.Builtin),
+
+            # Resources
+            (words((
+                'amDataXF', 'amDataYF', 'amJust', 'amOn', 'amOrthogonalPosF',
+                'amParallelPosF', 'amResizeNotify', 'amSide', 'amTrackData',
+                'amViewId', 'amZone', 'appDefaultParent', 'appFileSuffix',
+                'appResources', 'appSysDir', 'appUsrDir', 'caCopyArrays',
+                'caXArray', 'caXCast', 'caXMaxV', 'caXMinV', 'caXMissingV',
+                'caYArray', 'caYCast', 'caYMaxV', 'caYMinV', 'caYMissingV',
+                'cnCellFillEdgeColor', 'cnCellFillMissingValEdgeColor',
+                'cnConpackParams', 'cnConstFEnableFill', 'cnConstFLabelAngleF',
+                'cnConstFLabelBackgroundColor', 'cnConstFLabelConstantSpacingF',
+                'cnConstFLabelFont', 'cnConstFLabelFontAspectF',
+                'cnConstFLabelFontColor', 'cnConstFLabelFontHeightF',
+                'cnConstFLabelFontQuality', 'cnConstFLabelFontThicknessF',
+                'cnConstFLabelFormat', 'cnConstFLabelFuncCode', 'cnConstFLabelJust',
+                'cnConstFLabelOn', 'cnConstFLabelOrthogonalPosF',
+                'cnConstFLabelParallelPosF', 'cnConstFLabelPerimColor',
+                'cnConstFLabelPerimOn', 'cnConstFLabelPerimSpaceF',
+                'cnConstFLabelPerimThicknessF', 'cnConstFLabelSide',
+                'cnConstFLabelString', 'cnConstFLabelTextDirection',
+                'cnConstFLabelZone', 'cnConstFUseInfoLabelRes',
+                'cnExplicitLabelBarLabelsOn', 'cnExplicitLegendLabelsOn',
+                'cnExplicitLineLabelsOn', 'cnFillBackgroundColor', 'cnFillColor',
+                'cnFillColors', 'cnFillDotSizeF', 'cnFillDrawOrder', 'cnFillMode',
+                'cnFillOn', 'cnFillOpacityF', 'cnFillPalette', 'cnFillPattern',
+                'cnFillPatterns', 'cnFillScaleF', 'cnFillScales', 'cnFixFillBleed',
+                'cnGridBoundFillColor', 'cnGridBoundFillPattern',
+                'cnGridBoundFillScaleF', 'cnGridBoundPerimColor',
+                'cnGridBoundPerimDashPattern', 'cnGridBoundPerimOn',
+                'cnGridBoundPerimThicknessF', 'cnHighLabelAngleF',
+                'cnHighLabelBackgroundColor', 'cnHighLabelConstantSpacingF',
+                'cnHighLabelCount', 'cnHighLabelFont', 'cnHighLabelFontAspectF',
+                'cnHighLabelFontColor', 'cnHighLabelFontHeightF',
+                'cnHighLabelFontQuality', 'cnHighLabelFontThicknessF',
+                'cnHighLabelFormat', 'cnHighLabelFuncCode', 'cnHighLabelPerimColor',
+                'cnHighLabelPerimOn', 'cnHighLabelPerimSpaceF',
+                'cnHighLabelPerimThicknessF', 'cnHighLabelString', 'cnHighLabelsOn',
+                'cnHighLowLabelOverlapMode', 'cnHighUseLineLabelRes',
+                'cnInfoLabelAngleF', 'cnInfoLabelBackgroundColor',
+                'cnInfoLabelConstantSpacingF', 'cnInfoLabelFont',
+                'cnInfoLabelFontAspectF', 'cnInfoLabelFontColor',
+                'cnInfoLabelFontHeightF', 'cnInfoLabelFontQuality',
+                'cnInfoLabelFontThicknessF', 'cnInfoLabelFormat',
+                'cnInfoLabelFuncCode', 'cnInfoLabelJust', 'cnInfoLabelOn',
+                'cnInfoLabelOrthogonalPosF', 'cnInfoLabelParallelPosF',
+                'cnInfoLabelPerimColor', 'cnInfoLabelPerimOn',
+                'cnInfoLabelPerimSpaceF', 'cnInfoLabelPerimThicknessF',
+                'cnInfoLabelSide', 'cnInfoLabelString', 'cnInfoLabelTextDirection',
+                'cnInfoLabelZone', 'cnLabelBarEndLabelsOn', 'cnLabelBarEndStyle',
+                'cnLabelDrawOrder', 'cnLabelMasking', 'cnLabelScaleFactorF',
+                'cnLabelScaleValueF', 'cnLabelScalingMode', 'cnLegendLevelFlags',
+                'cnLevelCount', 'cnLevelFlag', 'cnLevelFlags', 'cnLevelSelectionMode',
+                'cnLevelSpacingF', 'cnLevels', 'cnLineColor', 'cnLineColors',
+                'cnLineDashPattern', 'cnLineDashPatterns', 'cnLineDashSegLenF',
+                'cnLineDrawOrder', 'cnLineLabelAngleF', 'cnLineLabelBackgroundColor',
+                'cnLineLabelConstantSpacingF', 'cnLineLabelCount',
+                'cnLineLabelDensityF', 'cnLineLabelFont', 'cnLineLabelFontAspectF',
+                'cnLineLabelFontColor', 'cnLineLabelFontColors',
+                'cnLineLabelFontHeightF', 'cnLineLabelFontQuality',
+                'cnLineLabelFontThicknessF', 'cnLineLabelFormat',
+                'cnLineLabelFuncCode', 'cnLineLabelInterval', 'cnLineLabelPerimColor',
+                'cnLineLabelPerimOn', 'cnLineLabelPerimSpaceF',
+                'cnLineLabelPerimThicknessF', 'cnLineLabelPlacementMode',
+                'cnLineLabelStrings', 'cnLineLabelsOn', 'cnLinePalette',
+                'cnLineThicknessF', 'cnLineThicknesses', 'cnLinesOn',
+                'cnLowLabelAngleF', 'cnLowLabelBackgroundColor',
+                'cnLowLabelConstantSpacingF', 'cnLowLabelCount', 'cnLowLabelFont',
+                'cnLowLabelFontAspectF', 'cnLowLabelFontColor',
+                'cnLowLabelFontHeightF', 'cnLowLabelFontQuality',
+                'cnLowLabelFontThicknessF', 'cnLowLabelFormat', 'cnLowLabelFuncCode',
+                'cnLowLabelPerimColor', 'cnLowLabelPerimOn', 'cnLowLabelPerimSpaceF',
+                'cnLowLabelPerimThicknessF', 'cnLowLabelString', 'cnLowLabelsOn',
+                'cnLowUseHighLabelRes', 'cnMaxDataValueFormat', 'cnMaxLevelCount',
+                'cnMaxLevelValF', 'cnMaxPointDistanceF', 'cnMinLevelValF',
+                'cnMissingValFillColor', 'cnMissingValFillPattern',
+                'cnMissingValFillScaleF', 'cnMissingValPerimColor',
+                'cnMissingValPerimDashPattern', 'cnMissingValPerimGridBoundOn',
+                'cnMissingValPerimOn', 'cnMissingValPerimThicknessF',
+                'cnMonoFillColor', 'cnMonoFillPattern', 'cnMonoFillScale',
+                'cnMonoLevelFlag', 'cnMonoLineColor', 'cnMonoLineDashPattern',
+                'cnMonoLineLabelFontColor', 'cnMonoLineThickness', 'cnNoDataLabelOn',
+                'cnNoDataLabelString', 'cnOutOfRangeFillColor',
+                'cnOutOfRangeFillPattern', 'cnOutOfRangeFillScaleF',
+                'cnOutOfRangePerimColor', 'cnOutOfRangePerimDashPattern',
+                'cnOutOfRangePerimOn', 'cnOutOfRangePerimThicknessF',
+                'cnRasterCellSizeF', 'cnRasterMinCellSizeF', 'cnRasterModeOn',
+                'cnRasterSampleFactorF', 'cnRasterSmoothingOn', 'cnScalarFieldData',
+                'cnSmoothingDistanceF', 'cnSmoothingOn', 'cnSmoothingTensionF',
+                'cnSpanFillPalette', 'cnSpanLinePalette', 'ctCopyTables',
+                'ctXElementSize', 'ctXMaxV', 'ctXMinV', 'ctXMissingV', 'ctXTable',
+                'ctXTableLengths', 'ctXTableType', 'ctYElementSize', 'ctYMaxV',
+                'ctYMinV', 'ctYMissingV', 'ctYTable', 'ctYTableLengths',
+                'ctYTableType', 'dcDelayCompute', 'errBuffer',
+                'errFileName', 'errFilePtr', 'errLevel', 'errPrint', 'errUnitNumber',
+                'gsClipOn', 'gsColors', 'gsEdgeColor', 'gsEdgeDashPattern',
+                'gsEdgeDashSegLenF', 'gsEdgeThicknessF', 'gsEdgesOn',
+                'gsFillBackgroundColor', 'gsFillColor', 'gsFillDotSizeF',
+                'gsFillIndex', 'gsFillLineThicknessF', 'gsFillOpacityF',
+                'gsFillScaleF', 'gsFont', 'gsFontAspectF', 'gsFontColor',
+                'gsFontHeightF', 'gsFontOpacityF', 'gsFontQuality',
+                'gsFontThicknessF', 'gsLineColor', 'gsLineDashPattern',
+                'gsLineDashSegLenF', 'gsLineLabelConstantSpacingF', 'gsLineLabelFont',
+                'gsLineLabelFontAspectF', 'gsLineLabelFontColor',
+                'gsLineLabelFontHeightF', 'gsLineLabelFontQuality',
+                'gsLineLabelFontThicknessF', 'gsLineLabelFuncCode',
+                'gsLineLabelString', 'gsLineOpacityF', 'gsLineThicknessF',
+                'gsMarkerColor', 'gsMarkerIndex', 'gsMarkerOpacityF', 'gsMarkerSizeF',
+                'gsMarkerThicknessF', 'gsSegments', 'gsTextAngleF',
+                'gsTextConstantSpacingF', 'gsTextDirection', 'gsTextFuncCode',
+                'gsTextJustification', 'gsnAboveYRefLineBarColors',
+                'gsnAboveYRefLineBarFillScales', 'gsnAboveYRefLineBarPatterns',
+                'gsnAboveYRefLineColor', 'gsnAddCyclic', 'gsnAttachBorderOn',
+                'gsnAttachPlotsXAxis', 'gsnBelowYRefLineBarColors',
+                'gsnBelowYRefLineBarFillScales', 'gsnBelowYRefLineBarPatterns',
+                'gsnBelowYRefLineColor', 'gsnBoxMargin', 'gsnCenterString',
+                'gsnCenterStringFontColor', 'gsnCenterStringFontHeightF',
+                'gsnCenterStringFuncCode', 'gsnCenterStringOrthogonalPosF',
+                'gsnCenterStringParallelPosF', 'gsnContourLineThicknessesScale',
+                'gsnContourNegLineDashPattern', 'gsnContourPosLineDashPattern',
+                'gsnContourZeroLineThicknessF', 'gsnDebugWriteFileName', 'gsnDraw',
+                'gsnFrame', 'gsnHistogramBarWidthPercent', 'gsnHistogramBinIntervals',
+                'gsnHistogramBinMissing', 'gsnHistogramBinWidth',
+                'gsnHistogramClassIntervals', 'gsnHistogramCompare',
+                'gsnHistogramComputePercentages',
+                'gsnHistogramComputePercentagesNoMissing',
+                'gsnHistogramDiscreteBinValues', 'gsnHistogramDiscreteClassValues',
+                'gsnHistogramHorizontal', 'gsnHistogramMinMaxBinsOn',
+                'gsnHistogramNumberOfBins', 'gsnHistogramPercentSign',
+                'gsnHistogramSelectNiceIntervals', 'gsnLeftString',
+                'gsnLeftStringFontColor', 'gsnLeftStringFontHeightF',
+                'gsnLeftStringFuncCode', 'gsnLeftStringOrthogonalPosF',
+                'gsnLeftStringParallelPosF', 'gsnMajorLatSpacing',
+                'gsnMajorLonSpacing', 'gsnMaskLambertConformal',
+                'gsnMaskLambertConformalOutlineOn', 'gsnMaximize',
+                'gsnMinorLatSpacing', 'gsnMinorLonSpacing', 'gsnPanelBottom',
+                'gsnPanelCenter', 'gsnPanelDebug', 'gsnPanelFigureStrings',
+                'gsnPanelFigureStringsBackgroundFillColor',
+                'gsnPanelFigureStringsFontHeightF', 'gsnPanelFigureStringsJust',
+                'gsnPanelFigureStringsPerimOn', 'gsnPanelLabelBar', 'gsnPanelLeft',
+                'gsnPanelMainFont', 'gsnPanelMainFontColor',
+                'gsnPanelMainFontHeightF', 'gsnPanelMainString', 'gsnPanelRight',
+                'gsnPanelRowSpec', 'gsnPanelScalePlotIndex', 'gsnPanelTop',
+                'gsnPanelXF', 'gsnPanelXWhiteSpacePercent', 'gsnPanelYF',
+                'gsnPanelYWhiteSpacePercent', 'gsnPaperHeight', 'gsnPaperMargin',
+                'gsnPaperOrientation', 'gsnPaperWidth', 'gsnPolar',
+                'gsnPolarLabelDistance', 'gsnPolarLabelFont',
+                'gsnPolarLabelFontHeightF', 'gsnPolarLabelSpacing', 'gsnPolarTime',
+                'gsnPolarUT', 'gsnRightString', 'gsnRightStringFontColor',
+                'gsnRightStringFontHeightF', 'gsnRightStringFuncCode',
+                'gsnRightStringOrthogonalPosF', 'gsnRightStringParallelPosF',
+                'gsnScalarContour', 'gsnScale', 'gsnShape', 'gsnSpreadColorEnd',
+                'gsnSpreadColorStart', 'gsnSpreadColors', 'gsnStringFont',
+                'gsnStringFontColor', 'gsnStringFontHeightF', 'gsnStringFuncCode',
+                'gsnTickMarksOn', 'gsnXAxisIrregular2Linear', 'gsnXAxisIrregular2Log',
+                'gsnXRefLine', 'gsnXRefLineColor', 'gsnXRefLineDashPattern',
+                'gsnXRefLineThicknessF', 'gsnXYAboveFillColors', 'gsnXYBarChart',
+                'gsnXYBarChartBarWidth', 'gsnXYBarChartColors',
+                'gsnXYBarChartColors2', 'gsnXYBarChartFillDotSizeF',
+                'gsnXYBarChartFillLineThicknessF', 'gsnXYBarChartFillOpacityF',
+                'gsnXYBarChartFillScaleF', 'gsnXYBarChartOutlineOnly',
+                'gsnXYBarChartOutlineThicknessF', 'gsnXYBarChartPatterns',
+                'gsnXYBarChartPatterns2', 'gsnXYBelowFillColors', 'gsnXYFillColors',
+                'gsnXYFillOpacities', 'gsnXYLeftFillColors', 'gsnXYRightFillColors',
+                'gsnYAxisIrregular2Linear', 'gsnYAxisIrregular2Log', 'gsnYRefLine',
+                'gsnYRefLineColor', 'gsnYRefLineColors', 'gsnYRefLineDashPattern',
+                'gsnYRefLineDashPatterns', 'gsnYRefLineThicknessF',
+                'gsnYRefLineThicknesses', 'gsnZonalMean', 'gsnZonalMeanXMaxF',
+                'gsnZonalMeanXMinF', 'gsnZonalMeanYRefLine', 'lbAutoManage',
+                'lbBottomMarginF', 'lbBoxCount', 'lbBoxEndCapStyle', 'lbBoxFractions',
+                'lbBoxLineColor', 'lbBoxLineDashPattern', 'lbBoxLineDashSegLenF',
+                'lbBoxLineThicknessF', 'lbBoxLinesOn', 'lbBoxMajorExtentF',
+                'lbBoxMinorExtentF', 'lbBoxSeparatorLinesOn', 'lbBoxSizing',
+                'lbFillBackground', 'lbFillColor', 'lbFillColors', 'lbFillDotSizeF',
+                'lbFillLineThicknessF', 'lbFillPattern', 'lbFillPatterns',
+                'lbFillScaleF', 'lbFillScales', 'lbJustification', 'lbLabelAlignment',
+                'lbLabelAngleF', 'lbLabelAutoStride', 'lbLabelBarOn',
+                'lbLabelConstantSpacingF', 'lbLabelDirection', 'lbLabelFont',
+                'lbLabelFontAspectF', 'lbLabelFontColor', 'lbLabelFontHeightF',
+                'lbLabelFontQuality', 'lbLabelFontThicknessF', 'lbLabelFuncCode',
+                'lbLabelJust', 'lbLabelOffsetF', 'lbLabelPosition', 'lbLabelStride',
+                'lbLabelStrings', 'lbLabelsOn', 'lbLeftMarginF', 'lbMaxLabelLenF',
+                'lbMinLabelSpacingF', 'lbMonoFillColor', 'lbMonoFillPattern',
+                'lbMonoFillScale', 'lbOrientation', 'lbPerimColor',
+                'lbPerimDashPattern', 'lbPerimDashSegLenF', 'lbPerimFill',
+                'lbPerimFillColor', 'lbPerimOn', 'lbPerimThicknessF',
+                'lbRasterFillOn', 'lbRightMarginF', 'lbTitleAngleF',
+                'lbTitleConstantSpacingF', 'lbTitleDirection', 'lbTitleExtentF',
+                'lbTitleFont', 'lbTitleFontAspectF', 'lbTitleFontColor',
+                'lbTitleFontHeightF', 'lbTitleFontQuality', 'lbTitleFontThicknessF',
+                'lbTitleFuncCode', 'lbTitleJust', 'lbTitleOffsetF', 'lbTitleOn',
+                'lbTitlePosition', 'lbTitleString', 'lbTopMarginF', 'lgAutoManage',
+                'lgBottomMarginF', 'lgBoxBackground', 'lgBoxLineColor',
+                'lgBoxLineDashPattern', 'lgBoxLineDashSegLenF', 'lgBoxLineThicknessF',
+                'lgBoxLinesOn', 'lgBoxMajorExtentF', 'lgBoxMinorExtentF',
+                'lgDashIndex', 'lgDashIndexes', 'lgItemCount', 'lgItemOrder',
+                'lgItemPlacement', 'lgItemPositions', 'lgItemType', 'lgItemTypes',
+                'lgJustification', 'lgLabelAlignment', 'lgLabelAngleF',
+                'lgLabelAutoStride', 'lgLabelConstantSpacingF', 'lgLabelDirection',
+                'lgLabelFont', 'lgLabelFontAspectF', 'lgLabelFontColor',
+                'lgLabelFontHeightF', 'lgLabelFontQuality', 'lgLabelFontThicknessF',
+                'lgLabelFuncCode', 'lgLabelJust', 'lgLabelOffsetF', 'lgLabelPosition',
+                'lgLabelStride', 'lgLabelStrings', 'lgLabelsOn', 'lgLeftMarginF',
+                'lgLegendOn', 'lgLineColor', 'lgLineColors', 'lgLineDashSegLenF',
+                'lgLineDashSegLens', 'lgLineLabelConstantSpacingF', 'lgLineLabelFont',
+                'lgLineLabelFontAspectF', 'lgLineLabelFontColor',
+                'lgLineLabelFontColors', 'lgLineLabelFontHeightF',
+                'lgLineLabelFontHeights', 'lgLineLabelFontQuality',
+                'lgLineLabelFontThicknessF', 'lgLineLabelFuncCode',
+                'lgLineLabelStrings', 'lgLineLabelsOn', 'lgLineThicknessF',
+                'lgLineThicknesses', 'lgMarkerColor', 'lgMarkerColors',
+                'lgMarkerIndex', 'lgMarkerIndexes', 'lgMarkerSizeF', 'lgMarkerSizes',
+                'lgMarkerThicknessF', 'lgMarkerThicknesses', 'lgMonoDashIndex',
+                'lgMonoItemType', 'lgMonoLineColor', 'lgMonoLineDashSegLen',
+                'lgMonoLineLabelFontColor', 'lgMonoLineLabelFontHeight',
+                'lgMonoLineThickness', 'lgMonoMarkerColor', 'lgMonoMarkerIndex',
+                'lgMonoMarkerSize', 'lgMonoMarkerThickness', 'lgOrientation',
+                'lgPerimColor', 'lgPerimDashPattern', 'lgPerimDashSegLenF',
+                'lgPerimFill', 'lgPerimFillColor', 'lgPerimOn', 'lgPerimThicknessF',
+                'lgRightMarginF', 'lgTitleAngleF', 'lgTitleConstantSpacingF',
+                'lgTitleDirection', 'lgTitleExtentF', 'lgTitleFont',
+                'lgTitleFontAspectF', 'lgTitleFontColor', 'lgTitleFontHeightF',
+                'lgTitleFontQuality', 'lgTitleFontThicknessF', 'lgTitleFuncCode',
+                'lgTitleJust', 'lgTitleOffsetF', 'lgTitleOn', 'lgTitlePosition',
+                'lgTitleString', 'lgTopMarginF', 'mpAreaGroupCount',
+                'mpAreaMaskingOn', 'mpAreaNames', 'mpAreaTypes', 'mpBottomAngleF',
+                'mpBottomMapPosF', 'mpBottomNDCF', 'mpBottomNPCF',
+                'mpBottomPointLatF', 'mpBottomPointLonF', 'mpBottomWindowF',
+                'mpCenterLatF', 'mpCenterLonF', 'mpCenterRotF', 'mpCountyLineColor',
+                'mpCountyLineDashPattern', 'mpCountyLineDashSegLenF',
+                'mpCountyLineThicknessF', 'mpDataBaseVersion', 'mpDataResolution',
+                'mpDataSetName', 'mpDefaultFillColor', 'mpDefaultFillPattern',
+                'mpDefaultFillScaleF', 'mpDynamicAreaGroups', 'mpEllipticalBoundary',
+                'mpFillAreaSpecifiers', 'mpFillBoundarySets', 'mpFillColor',
+                'mpFillColors', 'mpFillColors-default', 'mpFillDotSizeF',
+                'mpFillDrawOrder', 'mpFillOn', 'mpFillPatternBackground',
+                'mpFillPattern', 'mpFillPatterns', 'mpFillPatterns-default',
+                'mpFillScaleF', 'mpFillScales', 'mpFillScales-default',
+                'mpFixedAreaGroups', 'mpGeophysicalLineColor',
+                'mpGeophysicalLineDashPattern', 'mpGeophysicalLineDashSegLenF',
+                'mpGeophysicalLineThicknessF', 'mpGreatCircleLinesOn',
+                'mpGridAndLimbDrawOrder', 'mpGridAndLimbOn', 'mpGridLatSpacingF',
+                'mpGridLineColor', 'mpGridLineDashPattern', 'mpGridLineDashSegLenF',
+                'mpGridLineThicknessF', 'mpGridLonSpacingF', 'mpGridMaskMode',
+                'mpGridMaxLatF', 'mpGridPolarLonSpacingF', 'mpGridSpacingF',
+                'mpInlandWaterFillColor', 'mpInlandWaterFillPattern',
+                'mpInlandWaterFillScaleF', 'mpLabelDrawOrder', 'mpLabelFontColor',
+                'mpLabelFontHeightF', 'mpLabelsOn', 'mpLambertMeridianF',
+                'mpLambertParallel1F', 'mpLambertParallel2F', 'mpLandFillColor',
+                'mpLandFillPattern', 'mpLandFillScaleF', 'mpLeftAngleF',
+                'mpLeftCornerLatF', 'mpLeftCornerLonF', 'mpLeftMapPosF',
+                'mpLeftNDCF', 'mpLeftNPCF', 'mpLeftPointLatF',
+                'mpLeftPointLonF', 'mpLeftWindowF', 'mpLimbLineColor',
+                'mpLimbLineDashPattern', 'mpLimbLineDashSegLenF',
+                'mpLimbLineThicknessF', 'mpLimitMode', 'mpMaskAreaSpecifiers',
+                'mpMaskOutlineSpecifiers', 'mpMaxLatF', 'mpMaxLonF',
+                'mpMinLatF', 'mpMinLonF', 'mpMonoFillColor', 'mpMonoFillPattern',
+                'mpMonoFillScale', 'mpNationalLineColor', 'mpNationalLineDashPattern',
+                'mpNationalLineThicknessF', 'mpOceanFillColor', 'mpOceanFillPattern',
+                'mpOceanFillScaleF', 'mpOutlineBoundarySets', 'mpOutlineDrawOrder',
+                'mpOutlineMaskingOn', 'mpOutlineOn', 'mpOutlineSpecifiers',
+                'mpPerimDrawOrder', 'mpPerimLineColor', 'mpPerimLineDashPattern',
+                'mpPerimLineDashSegLenF', 'mpPerimLineThicknessF', 'mpPerimOn',
+                'mpPolyMode', 'mpProjection', 'mpProvincialLineColor',
+                'mpProvincialLineDashPattern', 'mpProvincialLineDashSegLenF',
+                'mpProvincialLineThicknessF', 'mpRelativeCenterLat',
+                'mpRelativeCenterLon', 'mpRightAngleF', 'mpRightCornerLatF',
+                'mpRightCornerLonF', 'mpRightMapPosF', 'mpRightNDCF',
+                'mpRightNPCF', 'mpRightPointLatF', 'mpRightPointLonF',
+                'mpRightWindowF', 'mpSatelliteAngle1F', 'mpSatelliteAngle2F',
+                'mpSatelliteDistF', 'mpShapeMode', 'mpSpecifiedFillColors',
+                'mpSpecifiedFillDirectIndexing', 'mpSpecifiedFillPatterns',
+                'mpSpecifiedFillPriority', 'mpSpecifiedFillScales',
+                'mpTopAngleF', 'mpTopMapPosF', 'mpTopNDCF', 'mpTopNPCF',
+                'mpTopPointLatF', 'mpTopPointLonF', 'mpTopWindowF',
+                'mpUSStateLineColor', 'mpUSStateLineDashPattern',
+                'mpUSStateLineDashSegLenF', 'mpUSStateLineThicknessF',
+                'pmAnnoManagers', 'pmAnnoViews', 'pmLabelBarDisplayMode',
+                'pmLabelBarHeightF', 'pmLabelBarKeepAspect', 'pmLabelBarOrthogonalPosF',
+                'pmLabelBarParallelPosF', 'pmLabelBarSide', 'pmLabelBarWidthF',
+                'pmLabelBarZone', 'pmLegendDisplayMode', 'pmLegendHeightF',
+                'pmLegendKeepAspect', 'pmLegendOrthogonalPosF',
+                'pmLegendParallelPosF', 'pmLegendSide', 'pmLegendWidthF',
+                'pmLegendZone', 'pmOverlaySequenceIds', 'pmTickMarkDisplayMode',
+                'pmTickMarkZone', 'pmTitleDisplayMode', 'pmTitleZone',
+                'prGraphicStyle', 'prPolyType', 'prXArray', 'prYArray',
+                'sfCopyData', 'sfDataArray', 'sfDataMaxV', 'sfDataMinV',
+                'sfElementNodes', 'sfExchangeDimensions', 'sfFirstNodeIndex',
+                'sfMissingValueV', 'sfXArray', 'sfXCActualEndF', 'sfXCActualStartF',
+                'sfXCEndIndex', 'sfXCEndSubsetV', 'sfXCEndV', 'sfXCStartIndex',
+                'sfXCStartSubsetV', 'sfXCStartV', 'sfXCStride', 'sfXCellBounds',
+                'sfYArray', 'sfYCActualEndF', 'sfYCActualStartF', 'sfYCEndIndex',
+                'sfYCEndSubsetV', 'sfYCEndV', 'sfYCStartIndex', 'sfYCStartSubsetV',
+                'sfYCStartV', 'sfYCStride', 'sfYCellBounds', 'stArrowLengthF',
+                'stArrowStride', 'stCrossoverCheckCount',
+                'stExplicitLabelBarLabelsOn', 'stLabelBarEndLabelsOn',
+                'stLabelFormat', 'stLengthCheckCount', 'stLevelColors',
+                'stLevelCount', 'stLevelPalette', 'stLevelSelectionMode',
+                'stLevelSpacingF', 'stLevels', 'stLineColor', 'stLineOpacityF',
+                'stLineStartStride', 'stLineThicknessF', 'stMapDirection',
+                'stMaxLevelCount', 'stMaxLevelValF', 'stMinArrowSpacingF',
+                'stMinDistanceF', 'stMinLevelValF', 'stMinLineSpacingF',
+                'stMinStepFactorF', 'stMonoLineColor', 'stNoDataLabelOn',
+                'stNoDataLabelString', 'stScalarFieldData', 'stScalarMissingValColor',
+                'stSpanLevelPalette', 'stStepSizeF', 'stStreamlineDrawOrder',
+                'stUseScalarArray', 'stVectorFieldData', 'stZeroFLabelAngleF',
+                'stZeroFLabelBackgroundColor', 'stZeroFLabelConstantSpacingF',
+                'stZeroFLabelFont', 'stZeroFLabelFontAspectF',
+                'stZeroFLabelFontColor', 'stZeroFLabelFontHeightF',
+                'stZeroFLabelFontQuality', 'stZeroFLabelFontThicknessF',
+                'stZeroFLabelFuncCode', 'stZeroFLabelJust', 'stZeroFLabelOn',
+                'stZeroFLabelOrthogonalPosF', 'stZeroFLabelParallelPosF',
+                'stZeroFLabelPerimColor', 'stZeroFLabelPerimOn',
+                'stZeroFLabelPerimSpaceF', 'stZeroFLabelPerimThicknessF',
+                'stZeroFLabelSide', 'stZeroFLabelString', 'stZeroFLabelTextDirection',
+                'stZeroFLabelZone', 'tfDoNDCOverlay', 'tfPlotManagerOn',
+                'tfPolyDrawList', 'tfPolyDrawOrder', 'tiDeltaF', 'tiMainAngleF',
+                'tiMainConstantSpacingF', 'tiMainDirection', 'tiMainFont',
+                'tiMainFontAspectF', 'tiMainFontColor', 'tiMainFontHeightF',
+                'tiMainFontQuality', 'tiMainFontThicknessF', 'tiMainFuncCode',
+                'tiMainJust', 'tiMainOffsetXF', 'tiMainOffsetYF', 'tiMainOn',
+                'tiMainPosition', 'tiMainSide', 'tiMainString', 'tiUseMainAttributes',
+                'tiXAxisAngleF', 'tiXAxisConstantSpacingF', 'tiXAxisDirection',
+                'tiXAxisFont', 'tiXAxisFontAspectF', 'tiXAxisFontColor',
+                'tiXAxisFontHeightF', 'tiXAxisFontQuality', 'tiXAxisFontThicknessF',
+                'tiXAxisFuncCode', 'tiXAxisJust', 'tiXAxisOffsetXF',
+                'tiXAxisOffsetYF', 'tiXAxisOn', 'tiXAxisPosition', 'tiXAxisSide',
+                'tiXAxisString', 'tiYAxisAngleF', 'tiYAxisConstantSpacingF',
+                'tiYAxisDirection', 'tiYAxisFont', 'tiYAxisFontAspectF',
+                'tiYAxisFontColor', 'tiYAxisFontHeightF', 'tiYAxisFontQuality',
+                'tiYAxisFontThicknessF', 'tiYAxisFuncCode', 'tiYAxisJust',
+                'tiYAxisOffsetXF', 'tiYAxisOffsetYF', 'tiYAxisOn', 'tiYAxisPosition',
+                'tiYAxisSide', 'tiYAxisString', 'tmBorderLineColor',
+                'tmBorderThicknessF', 'tmEqualizeXYSizes', 'tmLabelAutoStride',
+                'tmSciNoteCutoff', 'tmXBAutoPrecision', 'tmXBBorderOn',
+                'tmXBDataLeftF', 'tmXBDataRightF', 'tmXBFormat', 'tmXBIrrTensionF',
+                'tmXBIrregularPoints', 'tmXBLabelAngleF', 'tmXBLabelConstantSpacingF',
+                'tmXBLabelDeltaF', 'tmXBLabelDirection', 'tmXBLabelFont',
+                'tmXBLabelFontAspectF', 'tmXBLabelFontColor', 'tmXBLabelFontHeightF',
+                'tmXBLabelFontQuality', 'tmXBLabelFontThicknessF',
+                'tmXBLabelFuncCode', 'tmXBLabelJust', 'tmXBLabelStride', 'tmXBLabels',
+                'tmXBLabelsOn', 'tmXBMajorLengthF', 'tmXBMajorLineColor',
+                'tmXBMajorOutwardLengthF', 'tmXBMajorThicknessF', 'tmXBMaxLabelLenF',
+                'tmXBMaxTicks', 'tmXBMinLabelSpacingF', 'tmXBMinorLengthF',
+                'tmXBMinorLineColor', 'tmXBMinorOn', 'tmXBMinorOutwardLengthF',
+                'tmXBMinorPerMajor', 'tmXBMinorThicknessF', 'tmXBMinorValues',
+                'tmXBMode', 'tmXBOn', 'tmXBPrecision', 'tmXBStyle', 'tmXBTickEndF',
+                'tmXBTickSpacingF', 'tmXBTickStartF', 'tmXBValues', 'tmXMajorGrid',
+                'tmXMajorGridLineColor', 'tmXMajorGridLineDashPattern',
+                'tmXMajorGridThicknessF', 'tmXMinorGrid', 'tmXMinorGridLineColor',
+                'tmXMinorGridLineDashPattern', 'tmXMinorGridThicknessF',
+                'tmXTAutoPrecision', 'tmXTBorderOn', 'tmXTDataLeftF',
+                'tmXTDataRightF', 'tmXTFormat', 'tmXTIrrTensionF',
+                'tmXTIrregularPoints', 'tmXTLabelAngleF', 'tmXTLabelConstantSpacingF',
+                'tmXTLabelDeltaF', 'tmXTLabelDirection', 'tmXTLabelFont',
+                'tmXTLabelFontAspectF', 'tmXTLabelFontColor', 'tmXTLabelFontHeightF',
+                'tmXTLabelFontQuality', 'tmXTLabelFontThicknessF',
+                'tmXTLabelFuncCode', 'tmXTLabelJust', 'tmXTLabelStride', 'tmXTLabels',
+                'tmXTLabelsOn', 'tmXTMajorLengthF', 'tmXTMajorLineColor',
+                'tmXTMajorOutwardLengthF', 'tmXTMajorThicknessF', 'tmXTMaxLabelLenF',
+                'tmXTMaxTicks', 'tmXTMinLabelSpacingF', 'tmXTMinorLengthF',
+                'tmXTMinorLineColor', 'tmXTMinorOn', 'tmXTMinorOutwardLengthF',
+                'tmXTMinorPerMajor', 'tmXTMinorThicknessF', 'tmXTMinorValues',
+                'tmXTMode', 'tmXTOn', 'tmXTPrecision', 'tmXTStyle', 'tmXTTickEndF',
+                'tmXTTickSpacingF', 'tmXTTickStartF', 'tmXTValues', 'tmXUseBottom',
+                'tmYLAutoPrecision', 'tmYLBorderOn', 'tmYLDataBottomF',
+                'tmYLDataTopF', 'tmYLFormat', 'tmYLIrrTensionF',
+                'tmYLIrregularPoints', 'tmYLLabelAngleF', 'tmYLLabelConstantSpacingF',
+                'tmYLLabelDeltaF', 'tmYLLabelDirection', 'tmYLLabelFont',
+                'tmYLLabelFontAspectF', 'tmYLLabelFontColor', 'tmYLLabelFontHeightF',
+                'tmYLLabelFontQuality', 'tmYLLabelFontThicknessF',
+                'tmYLLabelFuncCode', 'tmYLLabelJust', 'tmYLLabelStride', 'tmYLLabels',
+                'tmYLLabelsOn', 'tmYLMajorLengthF', 'tmYLMajorLineColor',
+                'tmYLMajorOutwardLengthF', 'tmYLMajorThicknessF', 'tmYLMaxLabelLenF',
+                'tmYLMaxTicks', 'tmYLMinLabelSpacingF', 'tmYLMinorLengthF',
+                'tmYLMinorLineColor', 'tmYLMinorOn', 'tmYLMinorOutwardLengthF',
+                'tmYLMinorPerMajor', 'tmYLMinorThicknessF', 'tmYLMinorValues',
+                'tmYLMode', 'tmYLOn', 'tmYLPrecision', 'tmYLStyle', 'tmYLTickEndF',
+                'tmYLTickSpacingF', 'tmYLTickStartF', 'tmYLValues', 'tmYMajorGrid',
+                'tmYMajorGridLineColor', 'tmYMajorGridLineDashPattern',
+                'tmYMajorGridThicknessF', 'tmYMinorGrid', 'tmYMinorGridLineColor',
+                'tmYMinorGridLineDashPattern', 'tmYMinorGridThicknessF',
+                'tmYRAutoPrecision', 'tmYRBorderOn', 'tmYRDataBottomF',
+                'tmYRDataTopF', 'tmYRFormat', 'tmYRIrrTensionF',
+                'tmYRIrregularPoints', 'tmYRLabelAngleF', 'tmYRLabelConstantSpacingF',
+                'tmYRLabelDeltaF', 'tmYRLabelDirection', 'tmYRLabelFont',
+                'tmYRLabelFontAspectF', 'tmYRLabelFontColor', 'tmYRLabelFontHeightF',
+                'tmYRLabelFontQuality', 'tmYRLabelFontThicknessF',
+                'tmYRLabelFuncCode', 'tmYRLabelJust', 'tmYRLabelStride', 'tmYRLabels',
+                'tmYRLabelsOn', 'tmYRMajorLengthF', 'tmYRMajorLineColor',
+                'tmYRMajorOutwardLengthF', 'tmYRMajorThicknessF', 'tmYRMaxLabelLenF',
+                'tmYRMaxTicks', 'tmYRMinLabelSpacingF', 'tmYRMinorLengthF',
+                'tmYRMinorLineColor', 'tmYRMinorOn', 'tmYRMinorOutwardLengthF',
+                'tmYRMinorPerMajor', 'tmYRMinorThicknessF', 'tmYRMinorValues',
+                'tmYRMode', 'tmYROn', 'tmYRPrecision', 'tmYRStyle', 'tmYRTickEndF',
+                'tmYRTickSpacingF', 'tmYRTickStartF', 'tmYRValues', 'tmYUseLeft',
+                'trGridType', 'trLineInterpolationOn',
+                'trXAxisType', 'trXCoordPoints', 'trXInterPoints', 'trXLog',
+                'trXMaxF', 'trXMinF', 'trXReverse', 'trXSamples', 'trXTensionF',
+                'trYAxisType', 'trYCoordPoints', 'trYInterPoints', 'trYLog',
+                'trYMaxF', 'trYMinF', 'trYReverse', 'trYSamples', 'trYTensionF',
+                'txAngleF', 'txBackgroundFillColor', 'txConstantSpacingF', 'txDirection',
+                'txFont', 'HLU-Fonts', 'txFontAspectF', 'txFontColor',
+                'txFontHeightF', 'txFontOpacityF', 'txFontQuality',
+                'txFontThicknessF', 'txFuncCode', 'txJust', 'txPerimColor',
+                'txPerimDashLengthF', 'txPerimDashPattern', 'txPerimOn',
+                'txPerimSpaceF', 'txPerimThicknessF', 'txPosXF', 'txPosYF',
+                'txString', 'vcExplicitLabelBarLabelsOn', 'vcFillArrowEdgeColor',
+                'vcFillArrowEdgeThicknessF', 'vcFillArrowFillColor',
+                'vcFillArrowHeadInteriorXF', 'vcFillArrowHeadMinFracXF',
+                'vcFillArrowHeadMinFracYF', 'vcFillArrowHeadXF', 'vcFillArrowHeadYF',
+                'vcFillArrowMinFracWidthF', 'vcFillArrowWidthF', 'vcFillArrowsOn',
+                'vcFillOverEdge', 'vcGlyphOpacityF', 'vcGlyphStyle',
+                'vcLabelBarEndLabelsOn', 'vcLabelFontColor', 'vcLabelFontHeightF',
+                'vcLabelsOn', 'vcLabelsUseVectorColor', 'vcLevelColors',
+                'vcLevelCount', 'vcLevelPalette', 'vcLevelSelectionMode',
+                'vcLevelSpacingF', 'vcLevels', 'vcLineArrowColor',
+                'vcLineArrowHeadMaxSizeF', 'vcLineArrowHeadMinSizeF',
+                'vcLineArrowThicknessF', 'vcMagnitudeFormat',
+                'vcMagnitudeScaleFactorF', 'vcMagnitudeScaleValueF',
+                'vcMagnitudeScalingMode', 'vcMapDirection', 'vcMaxLevelCount',
+                'vcMaxLevelValF', 'vcMaxMagnitudeF', 'vcMinAnnoAngleF',
+                'vcMinAnnoArrowAngleF', 'vcMinAnnoArrowEdgeColor',
+                'vcMinAnnoArrowFillColor', 'vcMinAnnoArrowLineColor',
+                'vcMinAnnoArrowMinOffsetF', 'vcMinAnnoArrowSpaceF',
+                'vcMinAnnoArrowUseVecColor', 'vcMinAnnoBackgroundColor',
+                'vcMinAnnoConstantSpacingF', 'vcMinAnnoExplicitMagnitudeF',
+                'vcMinAnnoFont', 'vcMinAnnoFontAspectF', 'vcMinAnnoFontColor',
+                'vcMinAnnoFontHeightF', 'vcMinAnnoFontQuality',
+                'vcMinAnnoFontThicknessF', 'vcMinAnnoFuncCode', 'vcMinAnnoJust',
+                'vcMinAnnoOn', 'vcMinAnnoOrientation', 'vcMinAnnoOrthogonalPosF',
+                'vcMinAnnoParallelPosF', 'vcMinAnnoPerimColor', 'vcMinAnnoPerimOn',
+                'vcMinAnnoPerimSpaceF', 'vcMinAnnoPerimThicknessF', 'vcMinAnnoSide',
+                'vcMinAnnoString1', 'vcMinAnnoString1On', 'vcMinAnnoString2',
+                'vcMinAnnoString2On', 'vcMinAnnoTextDirection', 'vcMinAnnoZone',
+                'vcMinDistanceF', 'vcMinFracLengthF', 'vcMinLevelValF',
+                'vcMinMagnitudeF', 'vcMonoFillArrowEdgeColor',
+                'vcMonoFillArrowFillColor', 'vcMonoLineArrowColor',
+                'vcMonoWindBarbColor', 'vcNoDataLabelOn', 'vcNoDataLabelString',
+                'vcPositionMode', 'vcRefAnnoAngleF', 'vcRefAnnoArrowAngleF',
+                'vcRefAnnoArrowEdgeColor', 'vcRefAnnoArrowFillColor',
+                'vcRefAnnoArrowLineColor', 'vcRefAnnoArrowMinOffsetF',
+                'vcRefAnnoArrowSpaceF', 'vcRefAnnoArrowUseVecColor',
+                'vcRefAnnoBackgroundColor', 'vcRefAnnoConstantSpacingF',
+                'vcRefAnnoExplicitMagnitudeF', 'vcRefAnnoFont',
+                'vcRefAnnoFontAspectF', 'vcRefAnnoFontColor', 'vcRefAnnoFontHeightF',
+                'vcRefAnnoFontQuality', 'vcRefAnnoFontThicknessF',
+                'vcRefAnnoFuncCode', 'vcRefAnnoJust', 'vcRefAnnoOn',
+                'vcRefAnnoOrientation', 'vcRefAnnoOrthogonalPosF',
+                'vcRefAnnoParallelPosF', 'vcRefAnnoPerimColor', 'vcRefAnnoPerimOn',
+                'vcRefAnnoPerimSpaceF', 'vcRefAnnoPerimThicknessF', 'vcRefAnnoSide',
+                'vcRefAnnoString1', 'vcRefAnnoString1On', 'vcRefAnnoString2',
+                'vcRefAnnoString2On', 'vcRefAnnoTextDirection', 'vcRefAnnoZone',
+                'vcRefLengthF', 'vcRefMagnitudeF', 'vcScalarFieldData',
+                'vcScalarMissingValColor', 'vcScalarValueFormat',
+                'vcScalarValueScaleFactorF', 'vcScalarValueScaleValueF',
+                'vcScalarValueScalingMode', 'vcSpanLevelPalette', 'vcUseRefAnnoRes',
+                'vcUseScalarArray', 'vcVectorDrawOrder', 'vcVectorFieldData',
+                'vcWindBarbCalmCircleSizeF', 'vcWindBarbColor',
+                'vcWindBarbLineThicknessF', 'vcWindBarbScaleFactorF',
+                'vcWindBarbTickAngleF', 'vcWindBarbTickLengthF',
+                'vcWindBarbTickSpacingF', 'vcZeroFLabelAngleF',
+                'vcZeroFLabelBackgroundColor', 'vcZeroFLabelConstantSpacingF',
+                'vcZeroFLabelFont', 'vcZeroFLabelFontAspectF',
+                'vcZeroFLabelFontColor', 'vcZeroFLabelFontHeightF',
+                'vcZeroFLabelFontQuality', 'vcZeroFLabelFontThicknessF',
+                'vcZeroFLabelFuncCode', 'vcZeroFLabelJust', 'vcZeroFLabelOn',
+                'vcZeroFLabelOrthogonalPosF', 'vcZeroFLabelParallelPosF',
+                'vcZeroFLabelPerimColor', 'vcZeroFLabelPerimOn',
+                'vcZeroFLabelPerimSpaceF', 'vcZeroFLabelPerimThicknessF',
+                'vcZeroFLabelSide', 'vcZeroFLabelString', 'vcZeroFLabelTextDirection',
+                'vcZeroFLabelZone', 'vfCopyData', 'vfDataArray',
+                'vfExchangeDimensions', 'vfExchangeUVData', 'vfMagMaxV', 'vfMagMinV',
+                'vfMissingUValueV', 'vfMissingVValueV', 'vfPolarData',
+                'vfSingleMissingValue', 'vfUDataArray', 'vfUMaxV', 'vfUMinV',
+                'vfVDataArray', 'vfVMaxV', 'vfVMinV', 'vfXArray', 'vfXCActualEndF',
+                'vfXCActualStartF', 'vfXCEndIndex', 'vfXCEndSubsetV', 'vfXCEndV',
+                'vfXCStartIndex', 'vfXCStartSubsetV', 'vfXCStartV', 'vfXCStride',
+                'vfYArray', 'vfYCActualEndF', 'vfYCActualStartF', 'vfYCEndIndex',
+                'vfYCEndSubsetV', 'vfYCEndV', 'vfYCStartIndex', 'vfYCStartSubsetV',
+                'vfYCStartV', 'vfYCStride', 'vpAnnoManagerId', 'vpClipOn',
+                'vpHeightF', 'vpKeepAspect', 'vpOn', 'vpUseSegments', 'vpWidthF',
+                'vpXF', 'vpYF', 'wkAntiAlias', 'wkBackgroundColor', 'wkBackgroundOpacityF',
+                'wkColorMapLen', 'wkColorMap', 'wkColorModel', 'wkDashTableLength',
+                'wkDefGraphicStyleId', 'wkDeviceLowerX', 'wkDeviceLowerY',
+                'wkDeviceUpperX', 'wkDeviceUpperY', 'wkFileName', 'wkFillTableLength',
+                'wkForegroundColor', 'wkFormat', 'wkFullBackground', 'wkGksWorkId',
+                'wkHeight', 'wkMarkerTableLength', 'wkMetaName', 'wkOrientation',
+                'wkPDFFileName', 'wkPDFFormat', 'wkPDFResolution', 'wkPSFileName',
+                'wkPSFormat', 'wkPSResolution', 'wkPaperHeightF', 'wkPaperSize',
+                'wkPaperWidthF', 'wkPause', 'wkTopLevelViews', 'wkViews',
+                'wkVisualType', 'wkWidth', 'wkWindowId', 'wkXColorMode', 'wsCurrentSize',
+                'wsMaximumSize', 'wsThresholdSize', 'xyComputeXMax',
+                'xyComputeXMin', 'xyComputeYMax', 'xyComputeYMin', 'xyCoordData',
+                'xyCoordDataSpec', 'xyCurveDrawOrder', 'xyDashPattern',
+                'xyDashPatterns', 'xyExplicitLabels', 'xyExplicitLegendLabels',
+                'xyLabelMode', 'xyLineColor', 'xyLineColors', 'xyLineDashSegLenF',
+                'xyLineLabelConstantSpacingF', 'xyLineLabelFont',
+                'xyLineLabelFontAspectF', 'xyLineLabelFontColor',
+                'xyLineLabelFontColors', 'xyLineLabelFontHeightF',
+                'xyLineLabelFontQuality', 'xyLineLabelFontThicknessF',
+                'xyLineLabelFuncCode', 'xyLineThicknessF', 'xyLineThicknesses',
+                'xyMarkLineMode', 'xyMarkLineModes', 'xyMarker', 'xyMarkerColor',
+                'xyMarkerColors', 'xyMarkerSizeF', 'xyMarkerSizes',
+                'xyMarkerThicknessF', 'xyMarkerThicknesses', 'xyMarkers',
+                'xyMonoDashPattern', 'xyMonoLineColor', 'xyMonoLineLabelFontColor',
+                'xyMonoLineThickness', 'xyMonoMarkLineMode', 'xyMonoMarker',
+                'xyMonoMarkerColor', 'xyMonoMarkerSize', 'xyMonoMarkerThickness',
+                'xyXIrrTensionF', 'xyXIrregularPoints', 'xyXStyle', 'xyYIrrTensionF',
+                'xyYIrregularPoints', 'xyYStyle'), prefix=r'\b'),
+             Name.Builtin),
+
+            # Booleans
+            (r'\.(True|False)\.', Name.Builtin),
+            # Comparing Operators
+            (r'\.(eq|ne|lt|le|gt|ge|not|and|or|xor)\.', Operator.Word),
+        ],
+
+        'strings': [
+            (r'(?s)"(\\\\|\\[0-7]+|\\.|[^"\\])*"', String.Double),
+        ],
+
+        'nums': [
+            (r'\d+(?![.e])(_[a-z]\w+)?', Number.Integer),
+            (r'[+-]?\d*\.\d+(e[-+]?\d+)?(_[a-z]\w+)?', Number.Float),
+            (r'[+-]?\d+\.\d*(e[-+]?\d+)?(_[a-z]\w+)?', Number.Float),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/nimrod.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/nimrod.py
new file mode 100644
index 00000000..365a8dcc
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/nimrod.py
@@ -0,0 +1,199 @@
+"""
+    pygments.lexers.nimrod
+    ~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexer for the Nim language (formerly known as Nimrod).
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, include, default, bygroups
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation, Error
+
+__all__ = ['NimrodLexer']
+
+
+class NimrodLexer(RegexLexer):
+    """
+    For Nim source code.
+    """
+
+    name = 'Nimrod'
+    url = 'http://nim-lang.org/'
+    aliases = ['nimrod', 'nim']
+    filenames = ['*.nim', '*.nimrod']
+    mimetypes = ['text/x-nim']
+    version_added = '1.5'
+
+    flags = re.MULTILINE | re.IGNORECASE
+
+    def underscorize(words):
+        newWords = []
+        new = []
+        for word in words:
+            for ch in word:
+                new.append(ch)
+                new.append("_?")
+            newWords.append(''.join(new))
+            new = []
+        return "|".join(newWords)
+
+    keywords = [
+        'addr', 'and', 'as', 'asm', 'bind', 'block', 'break', 'case',
+        'cast', 'concept', 'const', 'continue', 'converter', 'defer', 'discard',
+        'distinct', 'div', 'do', 'elif', 'else', 'end', 'enum', 'except',
+        'export', 'finally', 'for', 'if', 'in', 'yield', 'interface',
+        'is', 'isnot', 'iterator', 'let', 'mixin', 'mod',
+        'not', 'notin', 'object', 'of', 'or', 'out', 'ptr', 'raise',
+        'ref', 'return', 'shl', 'shr', 'static', 'try',
+        'tuple', 'type', 'using', 'when', 'while', 'xor'
+    ]
+
+    keywordsPseudo = [
+        'nil', 'true', 'false'
+    ]
+
+    opWords = [
+        'and', 'or', 'not', 'xor', 'shl', 'shr', 'div', 'mod', 'in',
+        'notin', 'is', 'isnot'
+    ]
+
+    types = [
+        'int', 'int8', 'int16', 'int32', 'int64', 'float', 'float32', 'float64',
+        'bool', 'char', 'range', 'array', 'seq', 'set', 'string'
+    ]
+
+    tokens = {
+        'root': [
+            # Comments
+            (r'##\[', String.Doc, 'doccomment'),
+            (r'##.*$', String.Doc),
+            (r'#\[', Comment.Multiline, 'comment'),
+            (r'#.*$', Comment),
+
+            # Pragmas
+            (r'\{\.', String.Other, 'pragma'),
+
+            # Operators
+            (r'[*=><+\-/@$~&%!?|\\\[\]]', Operator),
+            (r'\.\.|\.|,|\[\.|\.\]|\{\.|\.\}|\(\.|\.\)|\{|\}|\(|\)|:|\^|`|;',
+             Punctuation),
+
+            # Case statement branch
+            (r'(\n\s*)(of)(\s)', bygroups(Text.Whitespace, Keyword,
+                                          Text.Whitespace), 'casebranch'),
+
+            # Strings
+            (r'(?:[\w]+)"', String, 'rdqs'),
+            (r'"""', String.Double, 'tdqs'),
+            ('"', String, 'dqs'),
+
+            # Char
+            ("'", String.Char, 'chars'),
+
+            # Keywords
+            (rf'({underscorize(opWords)})\b', Operator.Word),
+            (r'(proc|func|method|macro|template)(\s)(?![(\[\]])',
+             bygroups(Keyword, Text.Whitespace), 'funcname'),
+            (rf'({underscorize(keywords)})\b', Keyword),
+            (r'({})\b'.format(underscorize(['from', 'import', 'include', 'export'])),
+             Keyword.Namespace),
+            (r'(v_?a_?r)\b', Keyword.Declaration),
+            (rf'({underscorize(types)})\b', Name.Builtin),
+            (rf'({underscorize(keywordsPseudo)})\b', Keyword.Pseudo),
+
+            # Identifiers
+            (r'\b((?![_\d])\w)(((?!_)\w)|(_(?!_)\w))*', Name),
+
+            # Numbers
+            (r'[0-9][0-9_]*(?=([e.]|\'f(32|64)))',
+             Number.Float, ('float-suffix', 'float-number')),
+            (r'0x[a-f0-9][a-f0-9_]*', Number.Hex, 'int-suffix'),
+            (r'0b[01][01_]*', Number.Bin, 'int-suffix'),
+            (r'0o[0-7][0-7_]*', Number.Oct, 'int-suffix'),
+            (r'[0-9][0-9_]*', Number.Integer, 'int-suffix'),
+
+            # Whitespace
+            (r'\s+', Text.Whitespace),
+            (r'.+$', Error),
+        ],
+        'chars': [
+            (r'\\([\\abcefnrtvl"\']|x[a-f0-9]{2}|[0-9]{1,3})', String.Escape),
+            (r"'", String.Char, '#pop'),
+            (r".", String.Char)
+        ],
+        'strings': [
+            (r'(?|>=|>>|>|<=|<<|<|\+|-|=|/|\*|%|\+=|-=|!|@', Operator),
+            (r'\(|\)|\[|\]|,|\.\.\.|\.\.|\.|::|:', Punctuation),
+            (r'`\{[^`]*`\}', Text),  # Extern blocks won't be Lexed by Nit
+            (r'[\r\n\t ]+', Text),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/nix.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/nix.py
new file mode 100644
index 00000000..3fa88c65
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/nix.py
@@ -0,0 +1,144 @@
+"""
+    pygments.lexers.nix
+    ~~~~~~~~~~~~~~~~~~~
+
+    Lexers for the NixOS Nix language.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, include
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation, Literal
+
+__all__ = ['NixLexer']
+
+
+class NixLexer(RegexLexer):
+    """
+    For the Nix language.
+    """
+
+    name = 'Nix'
+    url = 'http://nixos.org/nix/'
+    aliases = ['nixos', 'nix']
+    filenames = ['*.nix']
+    mimetypes = ['text/x-nix']
+    version_added = '2.0'
+
+    keywords = ['rec', 'with', 'let', 'in', 'inherit', 'assert', 'if',
+                'else', 'then', '...']
+    builtins = ['import', 'abort', 'baseNameOf', 'dirOf', 'isNull', 'builtins',
+                'map', 'removeAttrs', 'throw', 'toString', 'derivation']
+    operators = ['++', '+', '?', '.', '!', '//', '==', '/',
+                 '!=', '&&', '||', '->', '=', '<', '>', '*', '-']
+
+    punctuations = ["(", ")", "[", "]", ";", "{", "}", ":", ",", "@"]
+
+    tokens = {
+        'root': [
+            # comments starting with #
+            (r'#.*$', Comment.Single),
+
+            # multiline comments
+            (r'/\*', Comment.Multiline, 'comment'),
+
+            # whitespace
+            (r'\s+', Text),
+
+            # keywords
+            ('({})'.format('|'.join(re.escape(entry) + '\\b' for entry in keywords)), Keyword),
+
+            # highlight the builtins
+            ('({})'.format('|'.join(re.escape(entry) + '\\b' for entry in builtins)),
+             Name.Builtin),
+
+            (r'\b(true|false|null)\b', Name.Constant),
+
+            # floats
+            (r'-?(\d+\.\d*|\.\d+)([eE][-+]?\d+)?', Number.Float),
+
+            # integers
+            (r'-?[0-9]+', Number.Integer),
+
+            # paths
+            (r'[\w.+-]*(\/[\w.+-]+)+', Literal),
+            (r'~(\/[\w.+-]+)+', Literal),
+            (r'\<[\w.+-]+(\/[\w.+-]+)*\>', Literal),
+
+            # operators
+            ('({})'.format('|'.join(re.escape(entry) for entry in operators)),
+             Operator),
+
+            # word operators
+            (r'\b(or|and)\b', Operator.Word),
+
+            (r'\{', Punctuation, 'block'),
+
+            # punctuations
+            ('({})'.format('|'.join(re.escape(entry) for entry in punctuations)), Punctuation),
+
+            # strings
+            (r'"', String.Double, 'doublequote'),
+            (r"''", String.Multiline, 'multiline'),
+
+            # urls
+            (r'[a-zA-Z][a-zA-Z0-9\+\-\.]*\:[\w%/?:@&=+$,\\.!~*\'-]+', Literal),
+
+            # names of variables
+            (r'[\w-]+(?=\s*=)', String.Symbol),
+            (r'[a-zA-Z_][\w\'-]*', Text),
+
+            (r"\$\{", String.Interpol, 'antiquote'),
+        ],
+        'comment': [
+            (r'[^/*]+', Comment.Multiline),
+            (r'/\*', Comment.Multiline, '#push'),
+            (r'\*/', Comment.Multiline, '#pop'),
+            (r'[*/]', Comment.Multiline),
+        ],
+        'multiline': [
+            (r"''(\$|'|\\n|\\r|\\t|\\)", String.Escape),
+            (r"''", String.Multiline, '#pop'),
+            (r'\$\{', String.Interpol, 'antiquote'),
+            (r"[^'\$]+", String.Multiline),
+            (r"\$[^\{']", String.Multiline),
+            (r"'[^']", String.Multiline),
+            (r"\$(?=')", String.Multiline),
+        ],
+        'doublequote': [
+            (r'\\(\\|"|\$|n)', String.Escape),
+            (r'"', String.Double, '#pop'),
+            (r'\$\{', String.Interpol, 'antiquote'),
+            (r'[^"\\\$]+', String.Double),
+            (r'\$[^\{"]', String.Double),
+            (r'\$(?=")', String.Double),
+            (r'\\', String.Double),
+        ],
+        'antiquote': [
+            (r"\}", String.Interpol, '#pop'),
+            # TODO: we should probably escape also here ''${ \${
+            (r"\$\{", String.Interpol, '#push'),
+            include('root'),
+        ],
+        'block': [
+            (r"\}", Punctuation, '#pop'),
+            include('root'),
+        ],
+    }
+
+    def analyse_text(text):
+        rv = 0.0
+        # TODO: let/in
+        if re.search(r'import.+?<[^>]+>', text):
+            rv += 0.4
+        if re.search(r'mkDerivation\s+(\(|\{|rec)', text):
+            rv += 0.4
+        if re.search(r'=\s+mkIf\s+', text):
+            rv += 0.4
+        if re.search(r'\{[a-zA-Z,\s]+\}:', text):
+            rv += 0.1
+        return rv
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/numbair.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/numbair.py
new file mode 100644
index 00000000..435863e1
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/numbair.py
@@ -0,0 +1,63 @@
+"""
+    pygments.lexers.numbair
+    ~~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexer for other Numba Intermediate Representation.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, include, bygroups, words
+from pygments.token import Whitespace, Name, String,  Punctuation, Keyword, \
+    Operator, Number
+
+__all__ = ["NumbaIRLexer"]
+
+class NumbaIRLexer(RegexLexer):
+    """
+    Lexer for Numba IR
+    """
+    name = 'Numba_IR'
+    url = "https://numba.readthedocs.io/en/stable/developer/architecture.html#stage-2-generate-the-numba-ir"
+    aliases = ['numba_ir', 'numbair']
+    filenames = ['*.numba_ir']
+    mimetypes = ['text/x-numba_ir', 'text/x-numbair']
+    version_added = '2.19'
+
+    identifier = r'\$[a-zA-Z0-9._]+'
+    fun_or_var = r'([a-zA-Z_]+[a-zA-Z0-9]*)'
+
+    tokens = {
+        'root' : [
+            (r'(label)(\ [0-9]+)(:)$',
+                bygroups(Keyword, Name.Label, Punctuation)),
+
+            (r'=', Operator),
+            include('whitespace'),
+            include('keyword'),
+
+            (identifier, Name.Variable),
+            (fun_or_var + r'(\()',
+                bygroups(Name.Function, Punctuation)),
+            (fun_or_var + r'(\=)',
+                bygroups(Name.Attribute, Punctuation)),
+            (fun_or_var, Name.Constant),
+            (r'[0-9]+', Number),
+
+            # 
+            (r'<[^>\n]*>', String),
+
+            (r'[=<>{}\[\]()*.,!\':]|x\b', Punctuation)
+        ],
+
+        'keyword':[
+            (words((
+                'del', 'jump', 'call', 'branch',
+            ), suffix=' '), Keyword),
+        ],
+
+        'whitespace': [
+            (r'(\n|\s)+', Whitespace),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/oberon.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/oberon.py
new file mode 100644
index 00000000..61f3c2d2
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/oberon.py
@@ -0,0 +1,120 @@
+"""
+    pygments.lexers.oberon
+    ~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for Oberon family languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, include, words
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation
+
+__all__ = ['ComponentPascalLexer']
+
+
+class ComponentPascalLexer(RegexLexer):
+    """
+    For Component Pascal source code.
+    """
+    name = 'Component Pascal'
+    aliases = ['componentpascal', 'cp']
+    filenames = ['*.cp', '*.cps']
+    mimetypes = ['text/x-component-pascal']
+    url = 'https://blackboxframework.org'
+    version_added = '2.1'
+
+    flags = re.MULTILINE | re.DOTALL
+
+    tokens = {
+        'root': [
+            include('whitespace'),
+            include('comments'),
+            include('punctuation'),
+            include('numliterals'),
+            include('strings'),
+            include('operators'),
+            include('builtins'),
+            include('identifiers'),
+        ],
+        'whitespace': [
+            (r'\n+', Text),  # blank lines
+            (r'\s+', Text),  # whitespace
+        ],
+        'comments': [
+            (r'\(\*([^$].*?)\*\)', Comment.Multiline),
+            # TODO: nested comments (* (* ... *) ... (* ... *) *) not supported!
+        ],
+        'punctuation': [
+            (r'[()\[\]{},.:;|]', Punctuation),
+        ],
+        'numliterals': [
+            (r'[0-9A-F]+X\b', Number.Hex),                 # char code
+            (r'[0-9A-F]+[HL]\b', Number.Hex),              # hexadecimal number
+            (r'[0-9]+\.[0-9]+E[+-][0-9]+', Number.Float),  # real number
+            (r'[0-9]+\.[0-9]+', Number.Float),             # real number
+            (r'[0-9]+', Number.Integer),                   # decimal whole number
+        ],
+        'strings': [
+            (r"'[^\n']*'", String),  # single quoted string
+            (r'"[^\n"]*"', String),  # double quoted string
+        ],
+        'operators': [
+            # Arithmetic Operators
+            (r'[+-]', Operator),
+            (r'[*/]', Operator),
+            # Relational Operators
+            (r'[=#<>]', Operator),
+            # Dereferencing Operator
+            (r'\^', Operator),
+            # Logical AND Operator
+            (r'&', Operator),
+            # Logical NOT Operator
+            (r'~', Operator),
+            # Assignment Symbol
+            (r':=', Operator),
+            # Range Constructor
+            (r'\.\.', Operator),
+            (r'\$', Operator),
+        ],
+        'identifiers': [
+            (r'([a-zA-Z_$][\w$]*)', Name),
+        ],
+        'builtins': [
+            (words((
+                'ANYPTR', 'ANYREC', 'BOOLEAN', 'BYTE', 'CHAR', 'INTEGER', 'LONGINT',
+                'REAL', 'SET', 'SHORTCHAR', 'SHORTINT', 'SHORTREAL'
+                ), suffix=r'\b'), Keyword.Type),
+            (words((
+                'ABS', 'ABSTRACT', 'ARRAY', 'ASH', 'ASSERT', 'BEGIN', 'BITS', 'BY',
+                'CAP', 'CASE', 'CHR', 'CLOSE', 'CONST', 'DEC', 'DIV', 'DO', 'ELSE',
+                'ELSIF', 'EMPTY', 'END', 'ENTIER', 'EXCL', 'EXIT', 'EXTENSIBLE', 'FOR',
+                'HALT', 'IF', 'IMPORT', 'IN', 'INC', 'INCL', 'IS', 'LEN', 'LIMITED',
+                'LONG', 'LOOP', 'MAX', 'MIN', 'MOD', 'MODULE', 'NEW', 'ODD', 'OF',
+                'OR', 'ORD', 'OUT', 'POINTER', 'PROCEDURE', 'RECORD', 'REPEAT', 'RETURN',
+                'SHORT', 'SHORTCHAR', 'SHORTINT', 'SIZE', 'THEN', 'TYPE', 'TO', 'UNTIL',
+                'VAR', 'WHILE', 'WITH'
+                ), suffix=r'\b'), Keyword.Reserved),
+            (r'(TRUE|FALSE|NIL|INF)\b', Keyword.Constant),
+        ]
+    }
+
+    def analyse_text(text):
+        """The only other lexer using .cp is the C++ one, so we check if for
+        a few common Pascal keywords here. Those are unfortunately quite
+        common across various business languages as well."""
+        result = 0
+        if 'BEGIN' in text:
+            result += 0.01
+        if 'END' in text:
+            result += 0.01
+        if 'PROCEDURE' in text:
+            result += 0.01
+        if 'END' in text:
+            result += 0.01
+
+        return result
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/objective.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/objective.py
new file mode 100644
index 00000000..899c2c44
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/objective.py
@@ -0,0 +1,513 @@
+"""
+    pygments.lexers.objective
+    ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for Objective-C family languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, include, bygroups, using, this, words, \
+    inherit, default
+from pygments.token import Text, Keyword, Name, String, Operator, \
+    Number, Punctuation, Literal, Comment, Whitespace
+
+from pygments.lexers.c_cpp import CLexer, CppLexer
+
+__all__ = ['ObjectiveCLexer', 'ObjectiveCppLexer', 'LogosLexer', 'SwiftLexer']
+
+
+def objective(baselexer):
+    """
+    Generate a subclass of baselexer that accepts the Objective-C syntax
+    extensions.
+    """
+
+    # Have to be careful not to accidentally match JavaDoc/Doxygen syntax here,
+    # since that's quite common in ordinary C/C++ files.  It's OK to match
+    # JavaDoc/Doxygen keywords that only apply to Objective-C, mind.
+    #
+    # The upshot of this is that we CANNOT match @class or @interface
+    _oc_keywords = re.compile(r'@(?:end|implementation|protocol)')
+
+    # Matches [ ? identifier  ( identifier ? ] |  identifier? : )
+    # (note the identifier is *optional* when there is a ':'!)
+    _oc_message = re.compile(r'\[\s*[a-zA-Z_]\w*\s+'
+                             r'(?:[a-zA-Z_]\w*\s*\]|'
+                             r'(?:[a-zA-Z_]\w*)?:)')
+
+    class GeneratedObjectiveCVariant(baselexer):
+        """
+        Implements Objective-C syntax on top of an existing C family lexer.
+        """
+
+        tokens = {
+            'statements': [
+                (r'@"', String, 'string'),
+                (r'@(YES|NO)', Number),
+                (r"@'(\\.|\\[0-7]{1,3}|\\x[a-fA-F0-9]{1,2}|[^\\\'\n])'", String.Char),
+                (r'@(\d+\.\d*|\.\d+|\d+)[eE][+-]?\d+[lL]?', Number.Float),
+                (r'@(\d+\.\d*|\.\d+|\d+[fF])[fF]?', Number.Float),
+                (r'@0x[0-9a-fA-F]+[Ll]?', Number.Hex),
+                (r'@0[0-7]+[Ll]?', Number.Oct),
+                (r'@\d+[Ll]?', Number.Integer),
+                (r'@\(', Literal, 'literal_number'),
+                (r'@\[', Literal, 'literal_array'),
+                (r'@\{', Literal, 'literal_dictionary'),
+                (words((
+                    '@selector', '@private', '@protected', '@public', '@encode',
+                    '@synchronized', '@try', '@throw', '@catch', '@finally',
+                    '@end', '@property', '@synthesize', '__bridge', '__bridge_transfer',
+                    '__autoreleasing', '__block', '__weak', '__strong', 'weak', 'strong',
+                    'copy', 'retain', 'assign', 'unsafe_unretained', 'atomic', 'nonatomic',
+                    'readonly', 'readwrite', 'setter', 'getter', 'typeof', 'in',
+                    'out', 'inout', 'release', 'class', '@dynamic', '@optional',
+                    '@required', '@autoreleasepool', '@import'), suffix=r'\b'),
+                 Keyword),
+                (words(('id', 'instancetype', 'Class', 'IMP', 'SEL', 'BOOL',
+                        'IBOutlet', 'IBAction', 'unichar'), suffix=r'\b'),
+                 Keyword.Type),
+                (r'@(true|false|YES|NO)\n', Name.Builtin),
+                (r'(YES|NO|nil|self|super)\b', Name.Builtin),
+                # Carbon types
+                (r'(Boolean|UInt8|SInt8|UInt16|SInt16|UInt32|SInt32)\b', Keyword.Type),
+                # Carbon built-ins
+                (r'(TRUE|FALSE)\b', Name.Builtin),
+                (r'(@interface|@implementation)(\s+)', bygroups(Keyword, Text),
+                 ('#pop', 'oc_classname')),
+                (r'(@class|@protocol)(\s+)', bygroups(Keyword, Text),
+                 ('#pop', 'oc_forward_classname')),
+                # @ can also prefix other expressions like @{...} or @(...)
+                (r'@', Punctuation),
+                inherit,
+            ],
+            'oc_classname': [
+                # interface definition that inherits
+                (r'([a-zA-Z$_][\w$]*)(\s*:\s*)([a-zA-Z$_][\w$]*)?(\s*)(\{)',
+                 bygroups(Name.Class, Text, Name.Class, Text, Punctuation),
+                 ('#pop', 'oc_ivars')),
+                (r'([a-zA-Z$_][\w$]*)(\s*:\s*)([a-zA-Z$_][\w$]*)?',
+                 bygroups(Name.Class, Text, Name.Class), '#pop'),
+                # interface definition for a category
+                (r'([a-zA-Z$_][\w$]*)(\s*)(\([a-zA-Z$_][\w$]*\))(\s*)(\{)',
+                 bygroups(Name.Class, Text, Name.Label, Text, Punctuation),
+                 ('#pop', 'oc_ivars')),
+                (r'([a-zA-Z$_][\w$]*)(\s*)(\([a-zA-Z$_][\w$]*\))',
+                 bygroups(Name.Class, Text, Name.Label), '#pop'),
+                # simple interface / implementation
+                (r'([a-zA-Z$_][\w$]*)(\s*)(\{)',
+                 bygroups(Name.Class, Text, Punctuation), ('#pop', 'oc_ivars')),
+                (r'([a-zA-Z$_][\w$]*)', Name.Class, '#pop')
+            ],
+            'oc_forward_classname': [
+                (r'([a-zA-Z$_][\w$]*)(\s*,\s*)',
+                 bygroups(Name.Class, Text), 'oc_forward_classname'),
+                (r'([a-zA-Z$_][\w$]*)(\s*;?)',
+                 bygroups(Name.Class, Text), '#pop')
+            ],
+            'oc_ivars': [
+                include('whitespace'),
+                include('statements'),
+                (';', Punctuation),
+                (r'\{', Punctuation, '#push'),
+                (r'\}', Punctuation, '#pop'),
+            ],
+            'root': [
+                # methods
+                (r'^([-+])(\s*)'                         # method marker
+                 r'(\(.*?\))?(\s*)'                      # return type
+                 r'([a-zA-Z$_][\w$]*:?)',        # begin of method name
+                 bygroups(Punctuation, Text, using(this),
+                          Text, Name.Function),
+                 'method'),
+                inherit,
+            ],
+            'method': [
+                include('whitespace'),
+                # TODO unsure if ellipses are allowed elsewhere, see
+                # discussion in Issue 789
+                (r',', Punctuation),
+                (r'\.\.\.', Punctuation),
+                (r'(\(.*?\))(\s*)([a-zA-Z$_][\w$]*)',
+                 bygroups(using(this), Text, Name.Variable)),
+                (r'[a-zA-Z$_][\w$]*:', Name.Function),
+                (';', Punctuation, '#pop'),
+                (r'\{', Punctuation, 'function'),
+                default('#pop'),
+            ],
+            'literal_number': [
+                (r'\(', Punctuation, 'literal_number_inner'),
+                (r'\)', Literal, '#pop'),
+                include('statement'),
+            ],
+            'literal_number_inner': [
+                (r'\(', Punctuation, '#push'),
+                (r'\)', Punctuation, '#pop'),
+                include('statement'),
+            ],
+            'literal_array': [
+                (r'\[', Punctuation, 'literal_array_inner'),
+                (r'\]', Literal, '#pop'),
+                include('statement'),
+            ],
+            'literal_array_inner': [
+                (r'\[', Punctuation, '#push'),
+                (r'\]', Punctuation, '#pop'),
+                include('statement'),
+            ],
+            'literal_dictionary': [
+                (r'\}', Literal, '#pop'),
+                include('statement'),
+            ],
+        }
+
+        def analyse_text(text):
+            if _oc_keywords.search(text):
+                return 1.0
+            elif '@"' in text:  # strings
+                return 0.8
+            elif re.search('@[0-9]+', text):
+                return 0.7
+            elif _oc_message.search(text):
+                return 0.8
+            return 0
+
+        def get_tokens_unprocessed(self, text, stack=('root',)):
+            from pygments.lexers._cocoa_builtins import COCOA_INTERFACES, \
+                COCOA_PROTOCOLS, COCOA_PRIMITIVES
+
+            for index, token, value in \
+                    baselexer.get_tokens_unprocessed(self, text, stack):
+                if token is Name or token is Name.Class:
+                    if value in COCOA_INTERFACES or value in COCOA_PROTOCOLS \
+                       or value in COCOA_PRIMITIVES:
+                        token = Name.Builtin.Pseudo
+
+                yield index, token, value
+
+    return GeneratedObjectiveCVariant
+
+
+class ObjectiveCLexer(objective(CLexer)):
+    """
+    For Objective-C source code with preprocessor directives.
+    """
+
+    name = 'Objective-C'
+    url = 'https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Introduction/Introduction.html'
+    aliases = ['objective-c', 'objectivec', 'obj-c', 'objc']
+    filenames = ['*.m', '*.h']
+    mimetypes = ['text/x-objective-c']
+    version_added = ''
+    priority = 0.05    # Lower than C
+
+
+class ObjectiveCppLexer(objective(CppLexer)):
+    """
+    For Objective-C++ source code with preprocessor directives.
+    """
+
+    name = 'Objective-C++'
+    aliases = ['objective-c++', 'objectivec++', 'obj-c++', 'objc++']
+    filenames = ['*.mm', '*.hh']
+    mimetypes = ['text/x-objective-c++']
+    version_added = ''
+    priority = 0.05    # Lower than C++
+
+
+class LogosLexer(ObjectiveCppLexer):
+    """
+    For Logos + Objective-C source code with preprocessor directives.
+    """
+
+    name = 'Logos'
+    aliases = ['logos']
+    filenames = ['*.x', '*.xi', '*.xm', '*.xmi']
+    mimetypes = ['text/x-logos']
+    version_added = '1.6'
+    priority = 0.25
+
+    tokens = {
+        'statements': [
+            (r'(%orig|%log)\b', Keyword),
+            (r'(%c)\b(\()(\s*)([a-zA-Z$_][\w$]*)(\s*)(\))',
+             bygroups(Keyword, Punctuation, Text, Name.Class, Text, Punctuation)),
+            (r'(%init)\b(\()',
+             bygroups(Keyword, Punctuation), 'logos_init_directive'),
+            (r'(%init)(?=\s*;)', bygroups(Keyword)),
+            (r'(%hook|%group)(\s+)([a-zA-Z$_][\w$]+)',
+             bygroups(Keyword, Text, Name.Class), '#pop'),
+            (r'(%subclass)(\s+)', bygroups(Keyword, Text),
+             ('#pop', 'logos_classname')),
+            inherit,
+        ],
+        'logos_init_directive': [
+            (r'\s+', Text),
+            (',', Punctuation, ('logos_init_directive', '#pop')),
+            (r'([a-zA-Z$_][\w$]*)(\s*)(=)(\s*)([^);]*)',
+             bygroups(Name.Class, Text, Punctuation, Text, Text)),
+            (r'([a-zA-Z$_][\w$]*)', Name.Class),
+            (r'\)', Punctuation, '#pop'),
+        ],
+        'logos_classname': [
+            (r'([a-zA-Z$_][\w$]*)(\s*:\s*)([a-zA-Z$_][\w$]*)?',
+             bygroups(Name.Class, Text, Name.Class), '#pop'),
+            (r'([a-zA-Z$_][\w$]*)', Name.Class, '#pop')
+        ],
+        'root': [
+            (r'(%subclass)(\s+)', bygroups(Keyword, Text),
+             'logos_classname'),
+            (r'(%hook|%group)(\s+)([a-zA-Z$_][\w$]+)',
+             bygroups(Keyword, Text, Name.Class)),
+            (r'(%config)(\s*\(\s*)(\w+)(\s*=)(.*?)(\)\s*)',
+             bygroups(Keyword, Text, Name.Variable, Text, String, Text)),
+            (r'(%ctor)(\s*)(\{)', bygroups(Keyword, Text, Punctuation),
+             'function'),
+            (r'(%new)(\s*)(\()(.*?)(\))',
+             bygroups(Keyword, Text, Keyword, String, Keyword)),
+            (r'(\s*)(%end)(\s*)', bygroups(Text, Keyword, Text)),
+            inherit,
+        ],
+    }
+
+    _logos_keywords = re.compile(r'%(?:hook|ctor|init|c\()')
+
+    def analyse_text(text):
+        if LogosLexer._logos_keywords.search(text):
+            return 1.0
+        return 0
+
+
+class SwiftLexer(RegexLexer):
+    """
+    For Swift source.
+    """
+    name = 'Swift'
+    url = 'https://www.swift.org/'
+    filenames = ['*.swift']
+    aliases = ['swift']
+    mimetypes = ['text/x-swift']
+    version_added = '2.0'
+
+    tokens = {
+        'root': [
+            # Whitespace and Comments
+            (r'\n', Text),
+            (r'\s+', Whitespace),
+            (r'//', Comment.Single, 'comment-single'),
+            (r'/\*', Comment.Multiline, 'comment-multi'),
+            (r'#(if|elseif|else|endif|available)\b', Comment.Preproc, 'preproc'),
+
+            # Keywords
+            include('keywords'),
+
+            # Global Types
+            (words((
+                'Array', 'AutoreleasingUnsafeMutablePointer', 'BidirectionalReverseView',
+                'Bit', 'Bool', 'CFunctionPointer', 'COpaquePointer', 'CVaListPointer',
+                'Character', 'ClosedInterval', 'CollectionOfOne', 'ContiguousArray',
+                'Dictionary', 'DictionaryGenerator', 'DictionaryIndex', 'Double',
+                'EmptyCollection', 'EmptyGenerator', 'EnumerateGenerator',
+                'EnumerateSequence', 'FilterCollectionView',
+                'FilterCollectionViewIndex', 'FilterGenerator', 'FilterSequenceView',
+                'Float', 'Float80', 'FloatingPointClassification', 'GeneratorOf',
+                'GeneratorOfOne', 'GeneratorSequence', 'HalfOpenInterval', 'HeapBuffer',
+                'HeapBufferStorage', 'ImplicitlyUnwrappedOptional', 'IndexingGenerator',
+                'Int', 'Int16', 'Int32', 'Int64', 'Int8', 'LazyBidirectionalCollection',
+                'LazyForwardCollection', 'LazyRandomAccessCollection',
+                'LazySequence', 'MapCollectionView', 'MapSequenceGenerator',
+                'MapSequenceView', 'MirrorDisposition', 'ObjectIdentifier', 'OnHeap',
+                'Optional', 'PermutationGenerator', 'QuickLookObject',
+                'RandomAccessReverseView', 'Range', 'RangeGenerator', 'RawByte', 'Repeat',
+                'ReverseBidirectionalIndex', 'ReverseRandomAccessIndex', 'SequenceOf',
+                'SinkOf', 'Slice', 'StaticString', 'StrideThrough', 'StrideThroughGenerator',
+                'StrideTo', 'StrideToGenerator', 'String', 'UInt', 'UInt16', 'UInt32',
+                'UInt64', 'UInt8', 'UTF16', 'UTF32', 'UTF8', 'UnicodeDecodingResult',
+                'UnicodeScalar', 'Unmanaged', 'UnsafeBufferPointer',
+                'UnsafeBufferPointerGenerator', 'UnsafeMutableBufferPointer',
+                'UnsafeMutablePointer', 'UnsafePointer', 'Zip2', 'ZipGenerator2',
+                # Protocols
+                'AbsoluteValuable', 'AnyObject', 'ArrayLiteralConvertible',
+                'BidirectionalIndexType', 'BitwiseOperationsType',
+                'BooleanLiteralConvertible', 'BooleanType', 'CVarArgType',
+                'CollectionType', 'Comparable', 'DebugPrintable',
+                'DictionaryLiteralConvertible', 'Equatable',
+                'ExtendedGraphemeClusterLiteralConvertible',
+                'ExtensibleCollectionType', 'FloatLiteralConvertible',
+                'FloatingPointType', 'ForwardIndexType', 'GeneratorType', 'Hashable',
+                'IntegerArithmeticType', 'IntegerLiteralConvertible', 'IntegerType',
+                'IntervalType', 'MirrorType', 'MutableCollectionType', 'MutableSliceable',
+                'NilLiteralConvertible', 'OutputStreamType', 'Printable',
+                'RandomAccessIndexType', 'RangeReplaceableCollectionType',
+                'RawOptionSetType', 'RawRepresentable', 'Reflectable', 'SequenceType',
+                'SignedIntegerType', 'SignedNumberType', 'SinkType', 'Sliceable',
+                'Streamable', 'Strideable', 'StringInterpolationConvertible',
+                'StringLiteralConvertible', 'UnicodeCodecType',
+                'UnicodeScalarLiteralConvertible', 'UnsignedIntegerType',
+                '_ArrayBufferType', '_BidirectionalIndexType', '_CocoaStringType',
+                '_CollectionType', '_Comparable', '_ExtensibleCollectionType',
+                '_ForwardIndexType', '_Incrementable', '_IntegerArithmeticType',
+                '_IntegerType', '_ObjectiveCBridgeable', '_RandomAccessIndexType',
+                '_RawOptionSetType', '_SequenceType', '_Sequence_Type',
+                '_SignedIntegerType', '_SignedNumberType', '_Sliceable', '_Strideable',
+                '_SwiftNSArrayRequiredOverridesType', '_SwiftNSArrayType',
+                '_SwiftNSCopyingType', '_SwiftNSDictionaryRequiredOverridesType',
+                '_SwiftNSDictionaryType', '_SwiftNSEnumeratorType',
+                '_SwiftNSFastEnumerationType', '_SwiftNSStringRequiredOverridesType',
+                '_SwiftNSStringType', '_UnsignedIntegerType',
+                # Variables
+                'C_ARGC', 'C_ARGV', 'Process',
+                # Typealiases
+                'Any', 'AnyClass', 'BooleanLiteralType', 'CBool', 'CChar', 'CChar16',
+                'CChar32', 'CDouble', 'CFloat', 'CInt', 'CLong', 'CLongLong', 'CShort',
+                'CSignedChar', 'CUnsignedInt', 'CUnsignedLong', 'CUnsignedShort',
+                'CWideChar', 'ExtendedGraphemeClusterType', 'Float32', 'Float64',
+                'FloatLiteralType', 'IntMax', 'IntegerLiteralType', 'StringLiteralType',
+                'UIntMax', 'UWord', 'UnicodeScalarType', 'Void', 'Word',
+                # Foundation/Cocoa
+                'NSErrorPointer', 'NSObjectProtocol', 'Selector'), suffix=r'\b'),
+             Name.Builtin),
+            # Functions
+            (words((
+                'abs', 'advance', 'alignof', 'alignofValue', 'assert', 'assertionFailure',
+                'contains', 'count', 'countElements', 'debugPrint', 'debugPrintln',
+                'distance', 'dropFirst', 'dropLast', 'dump', 'enumerate', 'equal',
+                'extend', 'fatalError', 'filter', 'find', 'first', 'getVaList', 'indices',
+                'insert', 'isEmpty', 'join', 'last', 'lazy', 'lexicographicalCompare',
+                'map', 'max', 'maxElement', 'min', 'minElement', 'numericCast', 'overlaps',
+                'partition', 'precondition', 'preconditionFailure', 'prefix', 'print',
+                'println', 'reduce', 'reflect', 'removeAll', 'removeAtIndex', 'removeLast',
+                'removeRange', 'reverse', 'sizeof', 'sizeofValue', 'sort', 'sorted',
+                'splice', 'split', 'startsWith', 'stride', 'strideof', 'strideofValue',
+                'suffix', 'swap', 'toDebugString', 'toString', 'transcode',
+                'underestimateCount', 'unsafeAddressOf', 'unsafeBitCast', 'unsafeDowncast',
+                'withExtendedLifetime', 'withUnsafeMutablePointer',
+                'withUnsafeMutablePointers', 'withUnsafePointer', 'withUnsafePointers',
+                'withVaList'), suffix=r'\b'),
+             Name.Builtin.Pseudo),
+
+            # Implicit Block Variables
+            (r'\$\d+', Name.Variable),
+
+            # Binary Literal
+            (r'0b[01_]+', Number.Bin),
+            # Octal Literal
+            (r'0o[0-7_]+', Number.Oct),
+            # Hexadecimal Literal
+            (r'0x[0-9a-fA-F_]+', Number.Hex),
+            # Decimal Literal
+            (r'[0-9][0-9_]*(\.[0-9_]+[eE][+\-]?[0-9_]+|'
+             r'\.[0-9_]*|[eE][+\-]?[0-9_]+)', Number.Float),
+            (r'[0-9][0-9_]*', Number.Integer),
+            # String Literal
+            (r'"""', String, 'string-multi'),
+            (r'"', String, 'string'),
+
+            # Operators and Punctuation
+            (r'[(){}\[\].,:;=@#`?]|->|[<&?](?=\w)|(?<=\w)[>!?]', Punctuation),
+            (r'[/=\-+!*%<>&|^?~]+', Operator),
+
+            # Identifier
+            (r'[a-zA-Z_]\w*', Name)
+        ],
+        'keywords': [
+            (words((
+                'as', 'async', 'await', 'break', 'case', 'catch', 'continue', 'default', 'defer',
+                'do', 'else', 'fallthrough', 'for', 'guard', 'if', 'in', 'is',
+                'repeat', 'return', '#selector', 'switch', 'throw', 'try',
+                'where', 'while'), suffix=r'\b'),
+             Keyword),
+            (r'@availability\([^)]+\)', Keyword.Reserved),
+            (words((
+                'associativity', 'convenience', 'dynamic', 'didSet', 'final',
+                'get', 'indirect', 'infix', 'inout', 'lazy', 'left', 'mutating',
+                'none', 'nonmutating', 'optional', 'override', 'postfix',
+                'precedence', 'prefix', 'Protocol', 'required', 'rethrows',
+                'right', 'set', 'throws', 'Type', 'unowned', 'weak', 'willSet',
+                '@availability', '@autoclosure', '@noreturn',
+                '@NSApplicationMain', '@NSCopying', '@NSManaged', '@objc',
+                '@UIApplicationMain', '@IBAction', '@IBDesignable',
+                '@IBInspectable', '@IBOutlet'), suffix=r'\b'),
+             Keyword.Reserved),
+            (r'(as|dynamicType|false|is|nil|self|Self|super|true|__COLUMN__'
+             r'|__FILE__|__FUNCTION__|__LINE__|_'
+             r'|#(?:file|line|column|function))\b', Keyword.Constant),
+            (r'import\b', Keyword.Declaration, 'module'),
+            (r'(class|enum|extension|struct|protocol)(\s+)([a-zA-Z_]\w*)',
+             bygroups(Keyword.Declaration, Whitespace, Name.Class)),
+            (r'(func)(\s+)([a-zA-Z_]\w*)',
+             bygroups(Keyword.Declaration, Whitespace, Name.Function)),
+            (r'(var|let)(\s+)([a-zA-Z_]\w*)', bygroups(Keyword.Declaration,
+             Whitespace, Name.Variable)),
+            (words((
+                'actor', 'associatedtype', 'class', 'deinit', 'enum', 'extension', 'func', 'import',
+                'init', 'internal', 'let', 'operator', 'private', 'protocol', 'public',
+                'static', 'struct', 'subscript', 'typealias', 'var'), suffix=r'\b'),
+             Keyword.Declaration)
+        ],
+        'comment': [
+            (r':param: [a-zA-Z_]\w*|:returns?:|(FIXME|MARK|TODO):',
+             Comment.Special)
+        ],
+
+        # Nested
+        'comment-single': [
+            (r'\n', Whitespace, '#pop'),
+            include('comment'),
+            (r'[^\n]+', Comment.Single)
+        ],
+        'comment-multi': [
+            include('comment'),
+            (r'[^*/]+', Comment.Multiline),
+            (r'/\*', Comment.Multiline, '#push'),
+            (r'\*/', Comment.Multiline, '#pop'),
+            (r'[*/]+', Comment.Multiline)
+        ],
+        'module': [
+            (r'\n', Whitespace, '#pop'),
+            (r'[a-zA-Z_]\w*', Name.Class),
+            include('root')
+        ],
+        'preproc': [
+            (r'\n', Whitespace, '#pop'),
+            include('keywords'),
+            (r'[A-Za-z]\w*', Comment.Preproc),
+            include('root')
+        ],
+        'string': [
+            (r'"', String, '#pop'),
+            include("string-common"),
+        ],
+        'string-multi': [
+            (r'"""', String, '#pop'),
+            include("string-common"),
+        ],
+        'string-common': [
+            (r'\\\(', String.Interpol, 'string-intp'),
+            (r"""\\['"\\nrt]|\\x[0-9a-fA-F]{2}|\\[0-7]{1,3}"""
+             r"""|\\u[0-9a-fA-F]{4}|\\U[0-9a-fA-F]{8}""", String.Escape),
+            (r'[^\\"]+', String),
+            (r'\\', String)
+        ],
+        'string-intp': [
+            (r'\(', String.Interpol, '#push'),
+            (r'\)', String.Interpol, '#pop'),
+            include('root')
+        ]
+    }
+
+    def get_tokens_unprocessed(self, text):
+        from pygments.lexers._cocoa_builtins import COCOA_INTERFACES, \
+            COCOA_PROTOCOLS, COCOA_PRIMITIVES
+
+        for index, token, value in \
+                RegexLexer.get_tokens_unprocessed(self, text):
+            if token is Name or token is Name.Class:
+                if value in COCOA_INTERFACES or value in COCOA_PROTOCOLS \
+                   or value in COCOA_PRIMITIVES:
+                    token = Name.Builtin.Pseudo
+
+            yield index, token, value
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/ooc.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/ooc.py
new file mode 100644
index 00000000..8a990801
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/ooc.py
@@ -0,0 +1,84 @@
+"""
+    pygments.lexers.ooc
+    ~~~~~~~~~~~~~~~~~~~
+
+    Lexers for the Ooc language.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, bygroups, words
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation
+
+__all__ = ['OocLexer']
+
+
+class OocLexer(RegexLexer):
+    """
+    For Ooc source code
+    """
+    name = 'Ooc'
+    url = 'https://ooc-lang.github.io/'
+    aliases = ['ooc']
+    filenames = ['*.ooc']
+    mimetypes = ['text/x-ooc']
+    version_added = '1.2'
+
+    tokens = {
+        'root': [
+            (words((
+                'class', 'interface', 'implement', 'abstract', 'extends', 'from',
+                'this', 'super', 'new', 'const', 'final', 'static', 'import',
+                'use', 'extern', 'inline', 'proto', 'break', 'continue',
+                'fallthrough', 'operator', 'if', 'else', 'for', 'while', 'do',
+                'switch', 'case', 'as', 'in', 'version', 'return', 'true',
+                'false', 'null'), prefix=r'\b', suffix=r'\b'),
+             Keyword),
+            (r'include\b', Keyword, 'include'),
+            (r'(cover)([ \t]+)(from)([ \t]+)(\w+[*@]?)',
+             bygroups(Keyword, Text, Keyword, Text, Name.Class)),
+            (r'(func)((?:[ \t]|\\\n)+)(~[a-z_]\w*)',
+             bygroups(Keyword, Text, Name.Function)),
+            (r'\bfunc\b', Keyword),
+            # Note: %= not listed on https://ooc-lang.github.io/docs/lang/operators/
+            (r'//.*', Comment),
+            (r'(?s)/\*.*?\*/', Comment.Multiline),
+            (r'(==?|\+=?|-[=>]?|\*=?|/=?|:=|!=?|%=?|\?|>{1,3}=?|<{1,3}=?|\.\.|'
+             r'&&?|\|\|?|\^=?)', Operator),
+            (r'(\.)([ \t]*)([a-z]\w*)', bygroups(Operator, Text,
+                                                 Name.Function)),
+            (r'[A-Z][A-Z0-9_]+', Name.Constant),
+            (r'[A-Z]\w*([@*]|\[[ \t]*\])?', Name.Class),
+
+            (r'([a-z]\w*(?:~[a-z]\w*)?)((?:[ \t]|\\\n)*)(?=\()',
+             bygroups(Name.Function, Text)),
+            (r'[a-z]\w*', Name.Variable),
+
+            # : introduces types
+            (r'[:(){}\[\];,]', Punctuation),
+
+            (r'0x[0-9a-fA-F]+', Number.Hex),
+            (r'0c[0-9]+', Number.Oct),
+            (r'0b[01]+', Number.Bin),
+            (r'[0-9_]\.[0-9_]*(?!\.)', Number.Float),
+            (r'[0-9_]+', Number.Decimal),
+
+            (r'"(?:\\.|\\[0-7]{1,3}|\\x[a-fA-F0-9]{1,2}|[^\\"])*"',
+             String.Double),
+            (r"'(?:\\.|\\[0-9]{1,3}|\\x[a-fA-F0-9]{1,2}|[^\\\'\n])'",
+             String.Char),
+            (r'@', Punctuation),  # pointer dereference
+            (r'\.', Punctuation),  # imports or chain operator
+
+            (r'\\[ \t\n]', Text),
+            (r'[ \t]+', Text),
+        ],
+        'include': [
+            (r'[\w/]+', Name),
+            (r',', Punctuation),
+            (r'[ \t]', Text),
+            (r'[;\n]', Text, '#pop'),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/openscad.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/openscad.py
new file mode 100644
index 00000000..b06de227
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/openscad.py
@@ -0,0 +1,96 @@
+"""
+    pygments.lexers.openscad
+    ~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for the OpenSCAD languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, bygroups, words, include
+from pygments.token import Text, Comment, Punctuation, Operator, Keyword, Name, Number, Whitespace, Literal, String
+
+__all__ = ['OpenScadLexer']
+
+
+class OpenScadLexer(RegexLexer):
+    """For openSCAD code.
+    """
+    name = "OpenSCAD"
+    url = "https://openscad.org/"
+    aliases = ["openscad"]
+    filenames = ["*.scad"]
+    mimetypes = ["application/x-openscad"]
+    version_added = '2.16'
+
+    tokens = {
+        "root": [
+            (r"[^\S\n]+", Whitespace),
+            (r'//', Comment.Single, 'comment-single'),
+            (r'/\*', Comment.Multiline, 'comment-multi'),
+            (r"[{}\[\]\(\),;:]", Punctuation),
+            (r"[*!#%\-+=?/]", Operator),
+            (r"<=|<|==|!=|>=|>|&&|\|\|", Operator),
+            (r"\$(f[asn]|t|vp[rtd]|children)", Operator),
+            (r"(undef|PI)\b", Keyword.Constant),
+            (
+                r"(use|include)((?:\s|\\\\s)+)",
+                bygroups(Keyword.Namespace, Text),
+                "includes",
+            ),
+            (r"(module)(\s*)([^\s\(]+)",
+             bygroups(Keyword.Namespace, Whitespace, Name.Namespace)),
+            (r"(function)(\s*)([^\s\(]+)",
+             bygroups(Keyword.Declaration, Whitespace, Name.Function)),
+            (words(("true", "false"), prefix=r"\b", suffix=r"\b"), Literal),
+            (words((
+                "function", "module", "include", "use", "for",
+                "intersection_for", "if", "else", "return"
+                ), prefix=r"\b", suffix=r"\b"), Keyword
+            ),
+            (words((
+                "circle", "square", "polygon", "text", "sphere", "cube",
+                "cylinder", "polyhedron", "translate", "rotate", "scale",
+                "resize", "mirror", "multmatrix", "color", "offset", "hull",
+                "minkowski", "union", "difference", "intersection", "abs",
+                "sign", "sin", "cos", "tan", "acos", "asin", "atan", "atan2",
+                "floor", "round", "ceil", "ln", "log", "pow", "sqrt", "exp",
+                "rands", "min", "max", "concat", "lookup", "str", "chr",
+                "search", "version", "version_num", "norm", "cross",
+                "parent_module", "echo", "import", "import_dxf",
+                "dxf_linear_extrude", "linear_extrude", "rotate_extrude",
+                "surface", "projection", "render", "dxf_cross",
+                "dxf_dim", "let", "assign", "len"
+                ), prefix=r"\b", suffix=r"\b"),
+                Name.Builtin
+            ),
+            (r"\bchildren\b", Name.Builtin.Pseudo),
+            (r'""".*?"""', String.Double),
+            (r'"(\\\\|\\[^\\]|[^"\\])*"', String.Double),
+            (r"-?\d+(\.\d+)?(e[+-]?\d+)?", Number),
+            (r"\w+", Name),
+        ],
+        "includes": [
+            (
+                r"(<)([^>]*)(>)",
+                bygroups(Punctuation, Comment.PreprocFile, Punctuation),
+            ),
+        ],
+        'comment': [
+            (r':param: [a-zA-Z_]\w*|:returns?:|(FIXME|MARK|TODO):',
+             Comment.Special)
+        ],
+        'comment-single': [
+            (r'\n', Text, '#pop'),
+            include('comment'),
+            (r'[^\n]+', Comment.Single)
+        ],
+        'comment-multi': [
+            include('comment'),
+            (r'[^*/]+', Comment.Multiline),
+            (r'/\*', Comment.Multiline, '#push'),
+            (r'\*/', Comment.Multiline, '#pop'),
+            (r'[*/]', Comment.Multiline)
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/other.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/other.py
new file mode 100644
index 00000000..2b7dfb4a
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/other.py
@@ -0,0 +1,41 @@
+"""
+    pygments.lexers.other
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    Just export lexer classes previously contained in this module.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+# ruff: noqa: F401
+from pygments.lexers.sql import SqlLexer, MySqlLexer, SqliteConsoleLexer
+from pygments.lexers.shell import BashLexer, BashSessionLexer, BatchLexer, \
+    TcshLexer
+from pygments.lexers.robotframework import RobotFrameworkLexer
+from pygments.lexers.testing import GherkinLexer
+from pygments.lexers.esoteric import BrainfuckLexer, BefungeLexer, RedcodeLexer
+from pygments.lexers.prolog import LogtalkLexer
+from pygments.lexers.snobol import SnobolLexer
+from pygments.lexers.rebol import RebolLexer
+from pygments.lexers.configs import KconfigLexer, Cfengine3Lexer
+from pygments.lexers.modeling import ModelicaLexer
+from pygments.lexers.scripting import AppleScriptLexer, MOOCodeLexer, \
+    HybrisLexer
+from pygments.lexers.graphics import PostScriptLexer, GnuplotLexer, \
+    AsymptoteLexer, PovrayLexer
+from pygments.lexers.business import ABAPLexer, OpenEdgeLexer, \
+    GoodDataCLLexer, MaqlLexer
+from pygments.lexers.automation import AutoItLexer, AutohotkeyLexer
+from pygments.lexers.dsls import ProtoBufLexer, BroLexer, PuppetLexer, \
+    MscgenLexer, VGLLexer
+from pygments.lexers.basic import CbmBasicV2Lexer
+from pygments.lexers.pawn import SourcePawnLexer, PawnLexer
+from pygments.lexers.ecl import ECLLexer
+from pygments.lexers.urbi import UrbiscriptLexer
+from pygments.lexers.smalltalk import SmalltalkLexer, NewspeakLexer
+from pygments.lexers.installers import NSISLexer, RPMSpecLexer
+from pygments.lexers.textedit import AwkLexer
+from pygments.lexers.smv import NuSMVLexer
+
+__all__ = []
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/parasail.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/parasail.py
new file mode 100644
index 00000000..150d6a9c
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/parasail.py
@@ -0,0 +1,78 @@
+"""
+    pygments.lexers.parasail
+    ~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexer for ParaSail.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, include
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation, Literal
+
+__all__ = ['ParaSailLexer']
+
+
+class ParaSailLexer(RegexLexer):
+    """
+    For ParaSail source code.
+    """
+
+    name = 'ParaSail'
+    url = 'http://www.parasail-lang.org'
+    aliases = ['parasail']
+    filenames = ['*.psi', '*.psl']
+    mimetypes = ['text/x-parasail']
+    version_added = '2.1'
+
+    flags = re.MULTILINE
+
+    tokens = {
+        'root': [
+            (r'[^\S\n]+', Text),
+            (r'//.*?\n', Comment.Single),
+            (r'\b(and|or|xor)=', Operator.Word),
+            (r'\b(and(\s+then)?|or(\s+else)?|xor|rem|mod|'
+             r'(is|not)\s+null)\b',
+             Operator.Word),
+            # Keywords
+            (r'\b(abs|abstract|all|block|class|concurrent|const|continue|'
+             r'each|end|exit|extends|exports|forward|func|global|implements|'
+             r'import|in|interface|is|lambda|locked|new|not|null|of|op|'
+             r'optional|private|queued|ref|return|reverse|separate|some|'
+             r'type|until|var|with|'
+             # Control flow
+             r'if|then|else|elsif|case|for|while|loop)\b',
+             Keyword.Reserved),
+            (r'(abstract\s+)?(interface|class|op|func|type)',
+             Keyword.Declaration),
+            # Literals
+            (r'"[^"]*"', String),
+            (r'\\[\'ntrf"0]', String.Escape),
+            (r'#[a-zA-Z]\w*', Literal),       # Enumeration
+            include('numbers'),
+            (r"'[^']'", String.Char),
+            (r'[a-zA-Z]\w*', Name),
+            # Operators and Punctuation
+            (r'(<==|==>|<=>|\*\*=|<\|=|<<=|>>=|==|!=|=\?|<=|>=|'
+             r'\*\*|<<|>>|=>|:=|\+=|-=|\*=|\|=|\||/=|\+|-|\*|/|'
+             r'\.\.|<\.\.|\.\.<|<\.\.<)',
+             Operator),
+            (r'(<|>|\[|\]|\(|\)|\||:|;|,|.|\{|\}|->)',
+             Punctuation),
+            (r'\n+', Text),
+        ],
+        'numbers': [
+            (r'\d[0-9_]*#[0-9a-fA-F][0-9a-fA-F_]*#', Number.Hex),  # any base
+            (r'0[xX][0-9a-fA-F][0-9a-fA-F_]*', Number.Hex),        # C-like hex
+            (r'0[bB][01][01_]*', Number.Bin),                      # C-like bin
+            (r'\d[0-9_]*\.\d[0-9_]*[eE][+-]\d[0-9_]*',             # float exp
+             Number.Float),
+            (r'\d[0-9_]*\.\d[0-9_]*', Number.Float),               # float
+            (r'\d[0-9_]*', Number.Integer),                        # integer
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/parsers.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/parsers.py
new file mode 100644
index 00000000..7a4ed9d1
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/parsers.py
@@ -0,0 +1,798 @@
+"""
+    pygments.lexers.parsers
+    ~~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for parser generators.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, DelegatingLexer, \
+    include, bygroups, using
+from pygments.token import Punctuation, Other, Text, Comment, Operator, \
+    Keyword, Name, String, Number, Whitespace
+from pygments.lexers.jvm import JavaLexer
+from pygments.lexers.c_cpp import CLexer, CppLexer
+from pygments.lexers.objective import ObjectiveCLexer
+from pygments.lexers.d import DLexer
+from pygments.lexers.dotnet import CSharpLexer
+from pygments.lexers.ruby import RubyLexer
+from pygments.lexers.python import PythonLexer
+from pygments.lexers.perl import PerlLexer
+
+__all__ = ['RagelLexer', 'RagelEmbeddedLexer', 'RagelCLexer', 'RagelDLexer',
+           'RagelCppLexer', 'RagelObjectiveCLexer', 'RagelRubyLexer',
+           'RagelJavaLexer', 'AntlrLexer', 'AntlrPythonLexer',
+           'AntlrPerlLexer', 'AntlrRubyLexer', 'AntlrCppLexer',
+           'AntlrCSharpLexer', 'AntlrObjectiveCLexer',
+           'AntlrJavaLexer', 'AntlrActionScriptLexer',
+           'TreetopLexer', 'EbnfLexer']
+
+
+class RagelLexer(RegexLexer):
+    """A pure `Ragel `_ lexer.  Use this
+    for fragments of Ragel.  For ``.rl`` files, use
+    :class:`RagelEmbeddedLexer` instead (or one of the
+    language-specific subclasses).
+
+    """
+
+    name = 'Ragel'
+    url = 'http://www.colm.net/open-source/ragel/'
+    aliases = ['ragel']
+    filenames = []
+    version_added = '1.1'
+
+    tokens = {
+        'whitespace': [
+            (r'\s+', Whitespace)
+        ],
+        'comments': [
+            (r'\#.*$', Comment),
+        ],
+        'keywords': [
+            (r'(access|action|alphtype)\b', Keyword),
+            (r'(getkey|write|machine|include)\b', Keyword),
+            (r'(any|ascii|extend|alpha|digit|alnum|lower|upper)\b', Keyword),
+            (r'(xdigit|cntrl|graph|print|punct|space|zlen|empty)\b', Keyword)
+        ],
+        'numbers': [
+            (r'0x[0-9A-Fa-f]+', Number.Hex),
+            (r'[+-]?[0-9]+', Number.Integer),
+        ],
+        'literals': [
+            (r'"(\\\\|\\[^\\]|[^"\\])*"', String.Double),
+            (r"'(\\\\|\\[^\\]|[^'\\])*'", String.Single),
+            (r'\[(\\\\|\\[^\\]|[^\\\]])*\]', String),          # square bracket literals
+            (r'/(?!\*)(\\\\|\\[^\\]|[^/\\])*/', String.Regex),  # regular expressions
+        ],
+        'identifiers': [
+            (r'[a-zA-Z_]\w*', Name.Variable),
+        ],
+        'operators': [
+            (r',', Operator),                           # Join
+            (r'\||&|--?', Operator),                    # Union, Intersection and Subtraction
+            (r'\.|<:|:>>?', Operator),                  # Concatention
+            (r':', Operator),                           # Label
+            (r'->', Operator),                          # Epsilon Transition
+            (r'(>|\$|%|<|@|<>)(/|eof\b)', Operator),    # EOF Actions
+            (r'(>|\$|%|<|@|<>)(!|err\b)', Operator),    # Global Error Actions
+            (r'(>|\$|%|<|@|<>)(\^|lerr\b)', Operator),  # Local Error Actions
+            (r'(>|\$|%|<|@|<>)(~|to\b)', Operator),     # To-State Actions
+            (r'(>|\$|%|<|@|<>)(\*|from\b)', Operator),  # From-State Actions
+            (r'>|@|\$|%', Operator),                    # Transition Actions and Priorities
+            (r'\*|\?|\+|\{[0-9]*,[0-9]*\}', Operator),  # Repetition
+            (r'!|\^', Operator),                        # Negation
+            (r'\(|\)', Operator),                       # Grouping
+        ],
+        'root': [
+            include('literals'),
+            include('whitespace'),
+            include('comments'),
+            include('keywords'),
+            include('numbers'),
+            include('identifiers'),
+            include('operators'),
+            (r'\{', Punctuation, 'host'),
+            (r'=', Operator),
+            (r';', Punctuation),
+        ],
+        'host': [
+            (r'(' + r'|'.join((  # keep host code in largest possible chunks
+                r'[^{}\'"/#]+',  # exclude unsafe characters
+                r'[^\\]\\[{}]',  # allow escaped { or }
+
+                # strings and comments may safely contain unsafe characters
+                r'"(\\\\|\\[^\\]|[^"\\])*"',
+                r"'(\\\\|\\[^\\]|[^'\\])*'",
+                r'//.*$\n?',            # single line comment
+                r'/\*(.|\n)*?\*/',      # multi-line javadoc-style comment
+                r'\#.*$\n?',            # ruby comment
+
+                # regular expression: There's no reason for it to start
+                # with a * and this stops confusion with comments.
+                r'/(?!\*)(\\\\|\\[^\\]|[^/\\])*/',
+
+                # / is safe now that we've handled regex and javadoc comments
+                r'/',
+            )) + r')+', Other),
+
+            (r'\{', Punctuation, '#push'),
+            (r'\}', Punctuation, '#pop'),
+        ],
+    }
+
+
+class RagelEmbeddedLexer(RegexLexer):
+    """
+    A lexer for Ragel embedded in a host language file.
+
+    This will only highlight Ragel statements. If you want host language
+    highlighting then call the language-specific Ragel lexer.
+    """
+
+    name = 'Embedded Ragel'
+    aliases = ['ragel-em']
+    filenames = ['*.rl']
+    url = 'http://www.colm.net/open-source/ragel/'
+    version_added = '1.1'
+
+    tokens = {
+        'root': [
+            (r'(' + r'|'.join((   # keep host code in largest possible chunks
+                r'[^%\'"/#]+',    # exclude unsafe characters
+                r'%(?=[^%]|$)',   # a single % sign is okay, just not 2 of them
+
+                # strings and comments may safely contain unsafe characters
+                r'"(\\\\|\\[^\\]|[^"\\])*"',
+                r"'(\\\\|\\[^\\]|[^'\\])*'",
+                r'/\*(.|\n)*?\*/',      # multi-line javadoc-style comment
+                r'//.*$\n?',  # single line comment
+                r'\#.*$\n?',  # ruby/ragel comment
+                r'/(?!\*)(\\\\|\\[^\\]|[^/\\])*/',  # regular expression
+
+                # / is safe now that we've handled regex and javadoc comments
+                r'/',
+            )) + r')+', Other),
+
+            # Single Line FSM.
+            # Please don't put a quoted newline in a single line FSM.
+            # That's just mean. It will break this.
+            (r'(%%)(?![{%])(.*)($|;)(\n?)', bygroups(Punctuation,
+                                                     using(RagelLexer),
+                                                     Punctuation, Text)),
+
+            # Multi Line FSM.
+            (r'(%%%%|%%)\{', Punctuation, 'multi-line-fsm'),
+        ],
+        'multi-line-fsm': [
+            (r'(' + r'|'.join((  # keep ragel code in largest possible chunks.
+                r'(' + r'|'.join((
+                    r'[^}\'"\[/#]',   # exclude unsafe characters
+                    r'\}(?=[^%]|$)',   # } is okay as long as it's not followed by %
+                    r'\}%(?=[^%]|$)',  # ...well, one %'s okay, just not two...
+                    r'[^\\]\\[{}]',   # ...and } is okay if it's escaped
+
+                    # allow / if it's preceded with one of these symbols
+                    # (ragel EOF actions)
+                    r'(>|\$|%|<|@|<>)/',
+
+                    # specifically allow regex followed immediately by *
+                    # so it doesn't get mistaken for a comment
+                    r'/(?!\*)(\\\\|\\[^\\]|[^/\\])*/\*',
+
+                    # allow / as long as it's not followed by another / or by a *
+                    r'/(?=[^/*]|$)',
+
+                    # We want to match as many of these as we can in one block.
+                    # Not sure if we need the + sign here,
+                    # does it help performance?
+                )) + r')+',
+
+                # strings and comments may safely contain unsafe characters
+                r'"(\\\\|\\[^\\]|[^"\\])*"',
+                r"'(\\\\|\\[^\\]|[^'\\])*'",
+                r"\[(\\\\|\\[^\\]|[^\]\\])*\]",  # square bracket literal
+                r'/\*(.|\n)*?\*/',          # multi-line javadoc-style comment
+                r'//.*$\n?',                # single line comment
+                r'\#.*$\n?',                # ruby/ragel comment
+            )) + r')+', using(RagelLexer)),
+
+            (r'\}%%', Punctuation, '#pop'),
+        ]
+    }
+
+    def analyse_text(text):
+        return '@LANG: indep' in text
+
+
+class RagelRubyLexer(DelegatingLexer):
+    """
+    A lexer for Ragel in a Ruby host file.
+    """
+
+    name = 'Ragel in Ruby Host'
+    aliases = ['ragel-ruby', 'ragel-rb']
+    filenames = ['*.rl']
+    url = 'http://www.colm.net/open-source/ragel/'
+    version_added = '1.1'
+
+    def __init__(self, **options):
+        super().__init__(RubyLexer, RagelEmbeddedLexer, **options)
+
+    def analyse_text(text):
+        return '@LANG: ruby' in text
+
+
+class RagelCLexer(DelegatingLexer):
+    """
+    A lexer for Ragel in a C host file.
+    """
+
+    name = 'Ragel in C Host'
+    aliases = ['ragel-c']
+    filenames = ['*.rl']
+    url = 'http://www.colm.net/open-source/ragel/'
+    version_added = '1.1'
+
+    def __init__(self, **options):
+        super().__init__(CLexer, RagelEmbeddedLexer, **options)
+
+    def analyse_text(text):
+        return '@LANG: c' in text
+
+
+class RagelDLexer(DelegatingLexer):
+    """
+    A lexer for Ragel in a D host file.
+    """
+
+    name = 'Ragel in D Host'
+    aliases = ['ragel-d']
+    filenames = ['*.rl']
+    url = 'http://www.colm.net/open-source/ragel/'
+    version_added = '1.1'
+
+    def __init__(self, **options):
+        super().__init__(DLexer, RagelEmbeddedLexer, **options)
+
+    def analyse_text(text):
+        return '@LANG: d' in text
+
+
+class RagelCppLexer(DelegatingLexer):
+    """
+    A lexer for Ragel in a C++ host file.
+    """
+
+    name = 'Ragel in CPP Host'
+    aliases = ['ragel-cpp']
+    filenames = ['*.rl']
+    url = 'http://www.colm.net/open-source/ragel/'
+    version_added = '1.1'
+
+    def __init__(self, **options):
+        super().__init__(CppLexer, RagelEmbeddedLexer, **options)
+
+    def analyse_text(text):
+        return '@LANG: c++' in text
+
+
+class RagelObjectiveCLexer(DelegatingLexer):
+    """
+    A lexer for Ragel in an Objective C host file.
+    """
+
+    name = 'Ragel in Objective C Host'
+    aliases = ['ragel-objc']
+    filenames = ['*.rl']
+    url = 'http://www.colm.net/open-source/ragel/'
+    version_added = '1.1'
+
+    def __init__(self, **options):
+        super().__init__(ObjectiveCLexer, RagelEmbeddedLexer, **options)
+
+    def analyse_text(text):
+        return '@LANG: objc' in text
+
+
+class RagelJavaLexer(DelegatingLexer):
+    """
+    A lexer for Ragel in a Java host file.
+    """
+
+    name = 'Ragel in Java Host'
+    aliases = ['ragel-java']
+    filenames = ['*.rl']
+    url = 'http://www.colm.net/open-source/ragel/'
+    version_added = '1.1'
+
+    def __init__(self, **options):
+        super().__init__(JavaLexer, RagelEmbeddedLexer, **options)
+
+    def analyse_text(text):
+        return '@LANG: java' in text
+
+
+class AntlrLexer(RegexLexer):
+    """
+    Generic ANTLR Lexer.
+    Should not be called directly, instead
+    use DelegatingLexer for your target language.
+    """
+
+    name = 'ANTLR'
+    aliases = ['antlr']
+    filenames = []
+    url = 'https://www.antlr.org'
+    version_added = '1.1'
+
+    _id = r'[A-Za-z]\w*'
+    _TOKEN_REF = r'[A-Z]\w*'
+    _RULE_REF = r'[a-z]\w*'
+    _STRING_LITERAL = r'\'(?:\\\\|\\\'|[^\']*)\''
+    _INT = r'[0-9]+'
+
+    tokens = {
+        'whitespace': [
+            (r'\s+', Whitespace),
+        ],
+        'comments': [
+            (r'//.*$', Comment),
+            (r'/\*(.|\n)*?\*/', Comment),
+        ],
+        'root': [
+            include('whitespace'),
+            include('comments'),
+
+            (r'(lexer|parser|tree)?(\s*)(grammar\b)(\s*)(' + _id + ')(;)',
+             bygroups(Keyword, Whitespace, Keyword, Whitespace, Name.Class,
+                      Punctuation)),
+            # optionsSpec
+            (r'options\b', Keyword, 'options'),
+            # tokensSpec
+            (r'tokens\b', Keyword, 'tokens'),
+            # attrScope
+            (r'(scope)(\s*)(' + _id + r')(\s*)(\{)',
+             bygroups(Keyword, Whitespace, Name.Variable, Whitespace,
+                      Punctuation), 'action'),
+            # exception
+            (r'(catch|finally)\b', Keyword, 'exception'),
+            # action
+            (r'(@' + _id + r')(\s*)(::)?(\s*)(' + _id + r')(\s*)(\{)',
+             bygroups(Name.Label, Whitespace, Punctuation, Whitespace,
+                      Name.Label, Whitespace, Punctuation), 'action'),
+            # rule
+            (r'((?:protected|private|public|fragment)\b)?(\s*)(' + _id + ')(!)?',
+             bygroups(Keyword, Whitespace, Name.Label, Punctuation),
+             ('rule-alts', 'rule-prelims')),
+        ],
+        'exception': [
+            (r'\n', Whitespace, '#pop'),
+            (r'\s', Whitespace),
+            include('comments'),
+
+            (r'\[', Punctuation, 'nested-arg-action'),
+            (r'\{', Punctuation, 'action'),
+        ],
+        'rule-prelims': [
+            include('whitespace'),
+            include('comments'),
+
+            (r'returns\b', Keyword),
+            (r'\[', Punctuation, 'nested-arg-action'),
+            (r'\{', Punctuation, 'action'),
+            # throwsSpec
+            (r'(throws)(\s+)(' + _id + ')',
+             bygroups(Keyword, Whitespace, Name.Label)),
+            (r'(,)(\s*)(' + _id + ')',
+             bygroups(Punctuation, Whitespace, Name.Label)),  # Additional throws
+            # optionsSpec
+            (r'options\b', Keyword, 'options'),
+            # ruleScopeSpec - scope followed by target language code or name of action
+            # TODO finish implementing other possibilities for scope
+            # L173 ANTLRv3.g from ANTLR book
+            (r'(scope)(\s+)(\{)', bygroups(Keyword, Whitespace, Punctuation),
+             'action'),
+            (r'(scope)(\s+)(' + _id + r')(\s*)(;)',
+             bygroups(Keyword, Whitespace, Name.Label, Whitespace, Punctuation)),
+            # ruleAction
+            (r'(@' + _id + r')(\s*)(\{)',
+             bygroups(Name.Label, Whitespace, Punctuation), 'action'),
+            # finished prelims, go to rule alts!
+            (r':', Punctuation, '#pop')
+        ],
+        'rule-alts': [
+            include('whitespace'),
+            include('comments'),
+
+            # These might need to go in a separate 'block' state triggered by (
+            (r'options\b', Keyword, 'options'),
+            (r':', Punctuation),
+
+            # literals
+            (r'"(\\\\|\\[^\\]|[^"\\])*"', String.Double),
+            (r"'(\\\\|\\[^\\]|[^'\\])*'", String.Single),
+            (r'<<([^>]|>[^>])>>', String),
+            # identifiers
+            # Tokens start with capital letter.
+            (r'\$?[A-Z_]\w*', Name.Constant),
+            # Rules start with small letter.
+            (r'\$?[a-z_]\w*', Name.Variable),
+            # operators
+            (r'(\+|\||->|=>|=|\(|\)|\.\.|\.|\?|\*|\^|!|\#|~)', Operator),
+            (r',', Punctuation),
+            (r'\[', Punctuation, 'nested-arg-action'),
+            (r'\{', Punctuation, 'action'),
+            (r';', Punctuation, '#pop')
+        ],
+        'tokens': [
+            include('whitespace'),
+            include('comments'),
+            (r'\{', Punctuation),
+            (r'(' + _TOKEN_REF + r')(\s*)(=)?(\s*)(' + _STRING_LITERAL
+             + r')?(\s*)(;)',
+             bygroups(Name.Label, Whitespace, Punctuation, Whitespace,
+                      String, Whitespace, Punctuation)),
+            (r'\}', Punctuation, '#pop'),
+        ],
+        'options': [
+            include('whitespace'),
+            include('comments'),
+            (r'\{', Punctuation),
+            (r'(' + _id + r')(\s*)(=)(\s*)(' +
+             '|'.join((_id, _STRING_LITERAL, _INT, r'\*')) + r')(\s*)(;)',
+             bygroups(Name.Variable, Whitespace, Punctuation, Whitespace,
+                      Text, Whitespace, Punctuation)),
+            (r'\}', Punctuation, '#pop'),
+        ],
+        'action': [
+            (r'(' + r'|'.join((    # keep host code in largest possible chunks
+                r'[^${}\'"/\\]+',  # exclude unsafe characters
+
+                # strings and comments may safely contain unsafe characters
+                r'"(\\\\|\\[^\\]|[^"\\])*"',
+                r"'(\\\\|\\[^\\]|[^'\\])*'",
+                r'//.*$\n?',            # single line comment
+                r'/\*(.|\n)*?\*/',      # multi-line javadoc-style comment
+
+                # regular expression: There's no reason for it to start
+                # with a * and this stops confusion with comments.
+                r'/(?!\*)(\\\\|\\[^\\]|[^/\\])*/',
+
+                # backslashes are okay, as long as we are not backslashing a %
+                r'\\(?!%)',
+
+                # Now that we've handled regex and javadoc comments
+                # it's safe to let / through.
+                r'/',
+            )) + r')+', Other),
+            (r'(\\)(%)', bygroups(Punctuation, Other)),
+            (r'(\$[a-zA-Z]+)(\.?)(text|value)?',
+             bygroups(Name.Variable, Punctuation, Name.Property)),
+            (r'\{', Punctuation, '#push'),
+            (r'\}', Punctuation, '#pop'),
+        ],
+        'nested-arg-action': [
+            (r'(' + r'|'.join((    # keep host code in largest possible chunks.
+                r'[^$\[\]\'"/]+',  # exclude unsafe characters
+
+                # strings and comments may safely contain unsafe characters
+                r'"(\\\\|\\[^\\]|[^"\\])*"',
+                r"'(\\\\|\\[^\\]|[^'\\])*'",
+                r'//.*$\n?',            # single line comment
+                r'/\*(.|\n)*?\*/',      # multi-line javadoc-style comment
+
+                # regular expression: There's no reason for it to start
+                # with a * and this stops confusion with comments.
+                r'/(?!\*)(\\\\|\\[^\\]|[^/\\])*/',
+
+                # Now that we've handled regex and javadoc comments
+                # it's safe to let / through.
+                r'/',
+            )) + r')+', Other),
+
+
+            (r'\[', Punctuation, '#push'),
+            (r'\]', Punctuation, '#pop'),
+            (r'(\$[a-zA-Z]+)(\.?)(text|value)?',
+             bygroups(Name.Variable, Punctuation, Name.Property)),
+            (r'(\\\\|\\\]|\\\[|[^\[\]])+', Other),
+        ]
+    }
+
+    def analyse_text(text):
+        return re.search(r'^\s*grammar\s+[a-zA-Z0-9]+\s*;', text, re.M)
+
+
+# http://www.antlr.org/wiki/display/ANTLR3/Code+Generation+Targets
+
+class AntlrCppLexer(DelegatingLexer):
+    """
+    ANTLR with C++ Target
+    """
+
+    name = 'ANTLR With CPP Target'
+    aliases = ['antlr-cpp']
+    filenames = ['*.G', '*.g']
+    url = 'https://www.antlr.org'
+    version_added = '1.1'
+
+    def __init__(self, **options):
+        super().__init__(CppLexer, AntlrLexer, **options)
+
+    def analyse_text(text):
+        return AntlrLexer.analyse_text(text) and \
+            re.search(r'^\s*language\s*=\s*C\s*;', text, re.M)
+
+
+class AntlrObjectiveCLexer(DelegatingLexer):
+    """
+    ANTLR with Objective-C Target
+    """
+
+    name = 'ANTLR With ObjectiveC Target'
+    aliases = ['antlr-objc']
+    filenames = ['*.G', '*.g']
+    url = 'https://www.antlr.org'
+    version_added = '1.1'
+
+    def __init__(self, **options):
+        super().__init__(ObjectiveCLexer, AntlrLexer, **options)
+
+    def analyse_text(text):
+        return AntlrLexer.analyse_text(text) and \
+            re.search(r'^\s*language\s*=\s*ObjC\s*;', text)
+
+
+class AntlrCSharpLexer(DelegatingLexer):
+    """
+    ANTLR with C# Target
+    """
+
+    name = 'ANTLR With C# Target'
+    aliases = ['antlr-csharp', 'antlr-c#']
+    filenames = ['*.G', '*.g']
+    url = 'https://www.antlr.org'
+    version_added = '1.1'
+
+    def __init__(self, **options):
+        super().__init__(CSharpLexer, AntlrLexer, **options)
+
+    def analyse_text(text):
+        return AntlrLexer.analyse_text(text) and \
+            re.search(r'^\s*language\s*=\s*CSharp2\s*;', text, re.M)
+
+
+class AntlrPythonLexer(DelegatingLexer):
+    """
+    ANTLR with Python Target
+    """
+
+    name = 'ANTLR With Python Target'
+    aliases = ['antlr-python']
+    filenames = ['*.G', '*.g']
+    url = 'https://www.antlr.org'
+    version_added = '1.1'
+
+    def __init__(self, **options):
+        super().__init__(PythonLexer, AntlrLexer, **options)
+
+    def analyse_text(text):
+        return AntlrLexer.analyse_text(text) and \
+            re.search(r'^\s*language\s*=\s*Python\s*;', text, re.M)
+
+
+class AntlrJavaLexer(DelegatingLexer):
+    """
+    ANTLR with Java Target
+    """
+
+    name = 'ANTLR With Java Target'
+    aliases = ['antlr-java']
+    filenames = ['*.G', '*.g']
+    url = 'https://www.antlr.org'
+    version_added = '1.1'
+
+    def __init__(self, **options):
+        super().__init__(JavaLexer, AntlrLexer, **options)
+
+    def analyse_text(text):
+        # Antlr language is Java by default
+        return AntlrLexer.analyse_text(text) and 0.9
+
+
+class AntlrRubyLexer(DelegatingLexer):
+    """
+    ANTLR with Ruby Target
+    """
+
+    name = 'ANTLR With Ruby Target'
+    aliases = ['antlr-ruby', 'antlr-rb']
+    filenames = ['*.G', '*.g']
+    url = 'https://www.antlr.org'
+    version_added = '1.1'
+
+    def __init__(self, **options):
+        super().__init__(RubyLexer, AntlrLexer, **options)
+
+    def analyse_text(text):
+        return AntlrLexer.analyse_text(text) and \
+            re.search(r'^\s*language\s*=\s*Ruby\s*;', text, re.M)
+
+
+class AntlrPerlLexer(DelegatingLexer):
+    """
+    ANTLR with Perl Target
+    """
+
+    name = 'ANTLR With Perl Target'
+    aliases = ['antlr-perl']
+    filenames = ['*.G', '*.g']
+    url = 'https://www.antlr.org'
+    version_added = '1.1'
+
+    def __init__(self, **options):
+        super().__init__(PerlLexer, AntlrLexer, **options)
+
+    def analyse_text(text):
+        return AntlrLexer.analyse_text(text) and \
+            re.search(r'^\s*language\s*=\s*Perl5\s*;', text, re.M)
+
+
+class AntlrActionScriptLexer(DelegatingLexer):
+    """
+    ANTLR with ActionScript Target
+    """
+
+    name = 'ANTLR With ActionScript Target'
+    aliases = ['antlr-actionscript', 'antlr-as']
+    filenames = ['*.G', '*.g']
+    url = 'https://www.antlr.org'
+    version_added = '1.1'
+
+    def __init__(self, **options):
+        from pygments.lexers.actionscript import ActionScriptLexer
+        super().__init__(ActionScriptLexer, AntlrLexer, **options)
+
+    def analyse_text(text):
+        return AntlrLexer.analyse_text(text) and \
+            re.search(r'^\s*language\s*=\s*ActionScript\s*;', text, re.M)
+
+
+class TreetopBaseLexer(RegexLexer):
+    """
+    A base lexer for `Treetop `_ grammars.
+    Not for direct use; use :class:`TreetopLexer` instead.
+
+    .. versionadded:: 1.6
+    """
+
+    tokens = {
+        'root': [
+            include('space'),
+            (r'require[ \t]+[^\n\r]+[\n\r]', Other),
+            (r'module\b', Keyword.Namespace, 'module'),
+            (r'grammar\b', Keyword, 'grammar'),
+        ],
+        'module': [
+            include('space'),
+            include('end'),
+            (r'module\b', Keyword, '#push'),
+            (r'grammar\b', Keyword, 'grammar'),
+            (r'[A-Z]\w*(?:::[A-Z]\w*)*', Name.Namespace),
+        ],
+        'grammar': [
+            include('space'),
+            include('end'),
+            (r'rule\b', Keyword, 'rule'),
+            (r'include\b', Keyword, 'include'),
+            (r'[A-Z]\w*', Name),
+        ],
+        'include': [
+            include('space'),
+            (r'[A-Z]\w*(?:::[A-Z]\w*)*', Name.Class, '#pop'),
+        ],
+        'rule': [
+            include('space'),
+            include('end'),
+            (r'"(\\\\|\\[^\\]|[^"\\])*"', String.Double),
+            (r"'(\\\\|\\[^\\]|[^'\\])*'", String.Single),
+            (r'([A-Za-z_]\w*)(:)', bygroups(Name.Label, Punctuation)),
+            (r'[A-Za-z_]\w*', Name),
+            (r'[()]', Punctuation),
+            (r'[?+*/&!~]', Operator),
+            (r'\[(?:\\.|\[:\^?[a-z]+:\]|[^\\\]])+\]', String.Regex),
+            (r'([0-9]*)(\.\.)([0-9]*)',
+             bygroups(Number.Integer, Operator, Number.Integer)),
+            (r'(<)([^>]+)(>)', bygroups(Punctuation, Name.Class, Punctuation)),
+            (r'\{', Punctuation, 'inline_module'),
+            (r'\.', String.Regex),
+        ],
+        'inline_module': [
+            (r'\{', Other, 'ruby'),
+            (r'\}', Punctuation, '#pop'),
+            (r'[^{}]+', Other),
+        ],
+        'ruby': [
+            (r'\{', Other, '#push'),
+            (r'\}', Other, '#pop'),
+            (r'[^{}]+', Other),
+        ],
+        'space': [
+            (r'[ \t\n\r]+', Whitespace),
+            (r'#[^\n]*', Comment.Single),
+        ],
+        'end': [
+            (r'end\b', Keyword, '#pop'),
+        ],
+    }
+
+
+class TreetopLexer(DelegatingLexer):
+    """
+    A lexer for Treetop grammars.
+    """
+
+    name = 'Treetop'
+    aliases = ['treetop']
+    filenames = ['*.treetop', '*.tt']
+    url = 'https://cjheath.github.io/treetop'
+    version_added = '1.6'
+
+    def __init__(self, **options):
+        super().__init__(RubyLexer, TreetopBaseLexer, **options)
+
+
+class EbnfLexer(RegexLexer):
+    """
+    Lexer for `ISO/IEC 14977 EBNF
+    `_
+    grammars.
+    """
+
+    name = 'EBNF'
+    aliases = ['ebnf']
+    filenames = ['*.ebnf']
+    mimetypes = ['text/x-ebnf']
+    url = 'https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_Form'
+    version_added = '2.0'
+
+    tokens = {
+        'root': [
+            include('whitespace'),
+            include('comment_start'),
+            include('identifier'),
+            (r'=', Operator, 'production'),
+        ],
+        'production': [
+            include('whitespace'),
+            include('comment_start'),
+            include('identifier'),
+            (r'"[^"]*"', String.Double),
+            (r"'[^']*'", String.Single),
+            (r'(\?[^?]*\?)', Name.Entity),
+            (r'[\[\]{}(),|]', Punctuation),
+            (r'-', Operator),
+            (r';', Punctuation, '#pop'),
+            (r'\.', Punctuation, '#pop'),
+        ],
+        'whitespace': [
+            (r'\s+', Text),
+        ],
+        'comment_start': [
+            (r'\(\*', Comment.Multiline, 'comment'),
+        ],
+        'comment': [
+            (r'[^*)]', Comment.Multiline),
+            include('comment_start'),
+            (r'\*\)', Comment.Multiline, '#pop'),
+            (r'[*)]', Comment.Multiline),
+        ],
+        'identifier': [
+            (r'([a-zA-Z][\w \-]*)', Keyword),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/pascal.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/pascal.py
new file mode 100644
index 00000000..5f40dcc8
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/pascal.py
@@ -0,0 +1,644 @@
+"""
+    pygments.lexers.pascal
+    ~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for Pascal family languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import Lexer
+from pygments.util import get_bool_opt, get_list_opt
+from pygments.token import Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation, Error, Whitespace
+from pygments.scanner import Scanner
+
+# compatibility import
+from pygments.lexers.modula2 import Modula2Lexer # noqa: F401
+
+__all__ = ['DelphiLexer', 'PortugolLexer']
+
+
+class PortugolLexer(Lexer):
+    """For Portugol, a Pascal dialect with keywords in Portuguese."""
+    name = 'Portugol'
+    aliases = ['portugol']
+    filenames = ['*.alg', '*.portugol']
+    mimetypes = []
+    url = "https://www.apoioinformatica.inf.br/produtos/visualg/linguagem"
+    version_added = ''
+
+    def __init__(self, **options):
+        Lexer.__init__(self, **options)
+        self.lexer = DelphiLexer(**options, portugol=True)
+
+    def get_tokens_unprocessed(self, text):
+        return self.lexer.get_tokens_unprocessed(text)
+
+
+class DelphiLexer(Lexer):
+    """
+    For Delphi (Borland Object Pascal),
+    Turbo Pascal and Free Pascal source code.
+
+    Additional options accepted:
+
+    `turbopascal`
+        Highlight Turbo Pascal specific keywords (default: ``True``).
+    `delphi`
+        Highlight Borland Delphi specific keywords (default: ``True``).
+    `freepascal`
+        Highlight Free Pascal specific keywords (default: ``True``).
+    `units`
+        A list of units that should be considered builtin, supported are
+        ``System``, ``SysUtils``, ``Classes`` and ``Math``.
+        Default is to consider all of them builtin.
+    """
+    name = 'Delphi'
+    aliases = ['delphi', 'pas', 'pascal', 'objectpascal']
+    filenames = ['*.pas', '*.dpr']
+    mimetypes = ['text/x-pascal']
+    url = 'https://www.embarcadero.com/products/delphi'
+    version_added = ''
+
+    TURBO_PASCAL_KEYWORDS = (
+        'absolute', 'and', 'array', 'asm', 'begin', 'break', 'case',
+        'const', 'constructor', 'continue', 'destructor', 'div', 'do',
+        'downto', 'else', 'end', 'file', 'for', 'function', 'goto',
+        'if', 'implementation', 'in', 'inherited', 'inline', 'interface',
+        'label', 'mod', 'nil', 'not', 'object', 'of', 'on', 'operator',
+        'or', 'packed', 'procedure', 'program', 'record', 'reintroduce',
+        'repeat', 'self', 'set', 'shl', 'shr', 'string', 'then', 'to',
+        'type', 'unit', 'until', 'uses', 'var', 'while', 'with', 'xor'
+    )
+
+    DELPHI_KEYWORDS = (
+        'as', 'class', 'except', 'exports', 'finalization', 'finally',
+        'initialization', 'is', 'library', 'on', 'property', 'raise',
+        'threadvar', 'try'
+    )
+
+    FREE_PASCAL_KEYWORDS = (
+        'dispose', 'exit', 'false', 'new', 'true'
+    )
+
+    BLOCK_KEYWORDS = {
+        'begin', 'class', 'const', 'constructor', 'destructor', 'end',
+        'finalization', 'function', 'implementation', 'initialization',
+        'label', 'library', 'operator', 'procedure', 'program', 'property',
+        'record', 'threadvar', 'type', 'unit', 'uses', 'var'
+    }
+
+    FUNCTION_MODIFIERS = {
+        'alias', 'cdecl', 'export', 'inline', 'interrupt', 'nostackframe',
+        'pascal', 'register', 'safecall', 'softfloat', 'stdcall',
+        'varargs', 'name', 'dynamic', 'near', 'virtual', 'external',
+        'override', 'assembler'
+    }
+
+    # XXX: those aren't global. but currently we know no way for defining
+    #      them just for the type context.
+    DIRECTIVES = {
+        'absolute', 'abstract', 'assembler', 'cppdecl', 'default', 'far',
+        'far16', 'forward', 'index', 'oldfpccall', 'private', 'protected',
+        'published', 'public'
+    }
+
+    BUILTIN_TYPES = {
+        'ansichar', 'ansistring', 'bool', 'boolean', 'byte', 'bytebool',
+        'cardinal', 'char', 'comp', 'currency', 'double', 'dword',
+        'extended', 'int64', 'integer', 'iunknown', 'longbool', 'longint',
+        'longword', 'pansichar', 'pansistring', 'pbool', 'pboolean',
+        'pbyte', 'pbytearray', 'pcardinal', 'pchar', 'pcomp', 'pcurrency',
+        'pdate', 'pdatetime', 'pdouble', 'pdword', 'pextended', 'phandle',
+        'pint64', 'pinteger', 'plongint', 'plongword', 'pointer',
+        'ppointer', 'pshortint', 'pshortstring', 'psingle', 'psmallint',
+        'pstring', 'pvariant', 'pwidechar', 'pwidestring', 'pword',
+        'pwordarray', 'pwordbool', 'real', 'real48', 'shortint',
+        'shortstring', 'single', 'smallint', 'string', 'tclass', 'tdate',
+        'tdatetime', 'textfile', 'thandle', 'tobject', 'ttime', 'variant',
+        'widechar', 'widestring', 'word', 'wordbool'
+    }
+
+    BUILTIN_UNITS = {
+        'System': (
+            'abs', 'acquireexceptionobject', 'addr', 'ansitoutf8',
+            'append', 'arctan', 'assert', 'assigned', 'assignfile',
+            'beginthread', 'blockread', 'blockwrite', 'break', 'chdir',
+            'chr', 'close', 'closefile', 'comptocurrency', 'comptodouble',
+            'concat', 'continue', 'copy', 'cos', 'dec', 'delete',
+            'dispose', 'doubletocomp', 'endthread', 'enummodules',
+            'enumresourcemodules', 'eof', 'eoln', 'erase', 'exceptaddr',
+            'exceptobject', 'exclude', 'exit', 'exp', 'filepos', 'filesize',
+            'fillchar', 'finalize', 'findclasshinstance', 'findhinstance',
+            'findresourcehinstance', 'flush', 'frac', 'freemem',
+            'get8087cw', 'getdir', 'getlasterror', 'getmem',
+            'getmemorymanager', 'getmodulefilename', 'getvariantmanager',
+            'halt', 'hi', 'high', 'inc', 'include', 'initialize', 'insert',
+            'int', 'ioresult', 'ismemorymanagerset', 'isvariantmanagerset',
+            'length', 'ln', 'lo', 'low', 'mkdir', 'move', 'new', 'odd',
+            'olestrtostring', 'olestrtostrvar', 'ord', 'paramcount',
+            'paramstr', 'pi', 'pos', 'pred', 'ptr', 'pucs4chars', 'random',
+            'randomize', 'read', 'readln', 'reallocmem',
+            'releaseexceptionobject', 'rename', 'reset', 'rewrite', 'rmdir',
+            'round', 'runerror', 'seek', 'seekeof', 'seekeoln',
+            'set8087cw', 'setlength', 'setlinebreakstyle',
+            'setmemorymanager', 'setstring', 'settextbuf',
+            'setvariantmanager', 'sin', 'sizeof', 'slice', 'sqr', 'sqrt',
+            'str', 'stringofchar', 'stringtoolestr', 'stringtowidechar',
+            'succ', 'swap', 'trunc', 'truncate', 'typeinfo',
+            'ucs4stringtowidestring', 'unicodetoutf8', 'uniquestring',
+            'upcase', 'utf8decode', 'utf8encode', 'utf8toansi',
+            'utf8tounicode', 'val', 'vararrayredim', 'varclear',
+            'widecharlentostring', 'widecharlentostrvar',
+            'widechartostring', 'widechartostrvar',
+            'widestringtoucs4string', 'write', 'writeln'
+        ),
+        'SysUtils': (
+            'abort', 'addexitproc', 'addterminateproc', 'adjustlinebreaks',
+            'allocmem', 'ansicomparefilename', 'ansicomparestr',
+            'ansicomparetext', 'ansidequotedstr', 'ansiextractquotedstr',
+            'ansilastchar', 'ansilowercase', 'ansilowercasefilename',
+            'ansipos', 'ansiquotedstr', 'ansisamestr', 'ansisametext',
+            'ansistrcomp', 'ansistricomp', 'ansistrlastchar', 'ansistrlcomp',
+            'ansistrlicomp', 'ansistrlower', 'ansistrpos', 'ansistrrscan',
+            'ansistrscan', 'ansistrupper', 'ansiuppercase',
+            'ansiuppercasefilename', 'appendstr', 'assignstr', 'beep',
+            'booltostr', 'bytetocharindex', 'bytetocharlen', 'bytetype',
+            'callterminateprocs', 'changefileext', 'charlength',
+            'chartobyteindex', 'chartobytelen', 'comparemem', 'comparestr',
+            'comparetext', 'createdir', 'createguid', 'currentyear',
+            'currtostr', 'currtostrf', 'date', 'datetimetofiledate',
+            'datetimetostr', 'datetimetostring', 'datetimetosystemtime',
+            'datetimetotimestamp', 'datetostr', 'dayofweek', 'decodedate',
+            'decodedatefully', 'decodetime', 'deletefile', 'directoryexists',
+            'diskfree', 'disksize', 'disposestr', 'encodedate', 'encodetime',
+            'exceptionerrormessage', 'excludetrailingbackslash',
+            'excludetrailingpathdelimiter', 'expandfilename',
+            'expandfilenamecase', 'expanduncfilename', 'extractfiledir',
+            'extractfiledrive', 'extractfileext', 'extractfilename',
+            'extractfilepath', 'extractrelativepath', 'extractshortpathname',
+            'fileage', 'fileclose', 'filecreate', 'filedatetodatetime',
+            'fileexists', 'filegetattr', 'filegetdate', 'fileisreadonly',
+            'fileopen', 'fileread', 'filesearch', 'fileseek', 'filesetattr',
+            'filesetdate', 'filesetreadonly', 'filewrite', 'finalizepackage',
+            'findclose', 'findcmdlineswitch', 'findfirst', 'findnext',
+            'floattocurr', 'floattodatetime', 'floattodecimal', 'floattostr',
+            'floattostrf', 'floattotext', 'floattotextfmt', 'fmtloadstr',
+            'fmtstr', 'forcedirectories', 'format', 'formatbuf', 'formatcurr',
+            'formatdatetime', 'formatfloat', 'freeandnil', 'getcurrentdir',
+            'getenvironmentvariable', 'getfileversion', 'getformatsettings',
+            'getlocaleformatsettings', 'getmodulename', 'getpackagedescription',
+            'getpackageinfo', 'gettime', 'guidtostring', 'incamonth',
+            'includetrailingbackslash', 'includetrailingpathdelimiter',
+            'incmonth', 'initializepackage', 'interlockeddecrement',
+            'interlockedexchange', 'interlockedexchangeadd',
+            'interlockedincrement', 'inttohex', 'inttostr', 'isdelimiter',
+            'isequalguid', 'isleapyear', 'ispathdelimiter', 'isvalidident',
+            'languages', 'lastdelimiter', 'loadpackage', 'loadstr',
+            'lowercase', 'msecstotimestamp', 'newstr', 'nextcharindex', 'now',
+            'outofmemoryerror', 'quotedstr', 'raiselastoserror',
+            'raiselastwin32error', 'removedir', 'renamefile', 'replacedate',
+            'replacetime', 'safeloadlibrary', 'samefilename', 'sametext',
+            'setcurrentdir', 'showexception', 'sleep', 'stralloc', 'strbufsize',
+            'strbytetype', 'strcat', 'strcharlength', 'strcomp', 'strcopy',
+            'strdispose', 'strecopy', 'strend', 'strfmt', 'stricomp',
+            'stringreplace', 'stringtoguid', 'strlcat', 'strlcomp', 'strlcopy',
+            'strlen', 'strlfmt', 'strlicomp', 'strlower', 'strmove', 'strnew',
+            'strnextchar', 'strpas', 'strpcopy', 'strplcopy', 'strpos',
+            'strrscan', 'strscan', 'strtobool', 'strtobooldef', 'strtocurr',
+            'strtocurrdef', 'strtodate', 'strtodatedef', 'strtodatetime',
+            'strtodatetimedef', 'strtofloat', 'strtofloatdef', 'strtoint',
+            'strtoint64', 'strtoint64def', 'strtointdef', 'strtotime',
+            'strtotimedef', 'strupper', 'supports', 'syserrormessage',
+            'systemtimetodatetime', 'texttofloat', 'time', 'timestamptodatetime',
+            'timestamptomsecs', 'timetostr', 'trim', 'trimleft', 'trimright',
+            'tryencodedate', 'tryencodetime', 'tryfloattocurr', 'tryfloattodatetime',
+            'trystrtobool', 'trystrtocurr', 'trystrtodate', 'trystrtodatetime',
+            'trystrtofloat', 'trystrtoint', 'trystrtoint64', 'trystrtotime',
+            'unloadpackage', 'uppercase', 'widecomparestr', 'widecomparetext',
+            'widefmtstr', 'wideformat', 'wideformatbuf', 'widelowercase',
+            'widesamestr', 'widesametext', 'wideuppercase', 'win32check',
+            'wraptext'
+        ),
+        'Classes': (
+            'activateclassgroup', 'allocatehwnd', 'bintohex', 'checksynchronize',
+            'collectionsequal', 'countgenerations', 'deallocatehwnd', 'equalrect',
+            'extractstrings', 'findclass', 'findglobalcomponent', 'getclass',
+            'groupdescendantswith', 'hextobin', 'identtoint',
+            'initinheritedcomponent', 'inttoident', 'invalidpoint',
+            'isuniqueglobalcomponentname', 'linestart', 'objectbinarytotext',
+            'objectresourcetotext', 'objecttexttobinary', 'objecttexttoresource',
+            'pointsequal', 'readcomponentres', 'readcomponentresex',
+            'readcomponentresfile', 'rect', 'registerclass', 'registerclassalias',
+            'registerclasses', 'registercomponents', 'registerintegerconsts',
+            'registernoicon', 'registernonactivex', 'smallpoint', 'startclassgroup',
+            'teststreamformat', 'unregisterclass', 'unregisterclasses',
+            'unregisterintegerconsts', 'unregistermoduleclasses',
+            'writecomponentresfile'
+        ),
+        'Math': (
+            'arccos', 'arccosh', 'arccot', 'arccoth', 'arccsc', 'arccsch', 'arcsec',
+            'arcsech', 'arcsin', 'arcsinh', 'arctan2', 'arctanh', 'ceil',
+            'comparevalue', 'cosecant', 'cosh', 'cot', 'cotan', 'coth', 'csc',
+            'csch', 'cycletodeg', 'cycletograd', 'cycletorad', 'degtocycle',
+            'degtograd', 'degtorad', 'divmod', 'doubledecliningbalance',
+            'ensurerange', 'floor', 'frexp', 'futurevalue', 'getexceptionmask',
+            'getprecisionmode', 'getroundmode', 'gradtocycle', 'gradtodeg',
+            'gradtorad', 'hypot', 'inrange', 'interestpayment', 'interestrate',
+            'internalrateofreturn', 'intpower', 'isinfinite', 'isnan', 'iszero',
+            'ldexp', 'lnxp1', 'log10', 'log2', 'logn', 'max', 'maxintvalue',
+            'maxvalue', 'mean', 'meanandstddev', 'min', 'minintvalue', 'minvalue',
+            'momentskewkurtosis', 'netpresentvalue', 'norm', 'numberofperiods',
+            'payment', 'periodpayment', 'poly', 'popnstddev', 'popnvariance',
+            'power', 'presentvalue', 'radtocycle', 'radtodeg', 'radtograd',
+            'randg', 'randomrange', 'roundto', 'samevalue', 'sec', 'secant',
+            'sech', 'setexceptionmask', 'setprecisionmode', 'setroundmode',
+            'sign', 'simpleroundto', 'sincos', 'sinh', 'slndepreciation', 'stddev',
+            'sum', 'sumint', 'sumofsquares', 'sumsandsquares', 'syddepreciation',
+            'tan', 'tanh', 'totalvariance', 'variance'
+        )
+    }
+
+    ASM_REGISTERS = {
+        'ah', 'al', 'ax', 'bh', 'bl', 'bp', 'bx', 'ch', 'cl', 'cr0',
+        'cr1', 'cr2', 'cr3', 'cr4', 'cs', 'cx', 'dh', 'di', 'dl', 'dr0',
+        'dr1', 'dr2', 'dr3', 'dr4', 'dr5', 'dr6', 'dr7', 'ds', 'dx',
+        'eax', 'ebp', 'ebx', 'ecx', 'edi', 'edx', 'es', 'esi', 'esp',
+        'fs', 'gs', 'mm0', 'mm1', 'mm2', 'mm3', 'mm4', 'mm5', 'mm6',
+        'mm7', 'si', 'sp', 'ss', 'st0', 'st1', 'st2', 'st3', 'st4', 'st5',
+        'st6', 'st7', 'xmm0', 'xmm1', 'xmm2', 'xmm3', 'xmm4', 'xmm5',
+        'xmm6', 'xmm7'
+    }
+
+    ASM_INSTRUCTIONS = {
+        'aaa', 'aad', 'aam', 'aas', 'adc', 'add', 'and', 'arpl', 'bound',
+        'bsf', 'bsr', 'bswap', 'bt', 'btc', 'btr', 'bts', 'call', 'cbw',
+        'cdq', 'clc', 'cld', 'cli', 'clts', 'cmc', 'cmova', 'cmovae',
+        'cmovb', 'cmovbe', 'cmovc', 'cmovcxz', 'cmove', 'cmovg',
+        'cmovge', 'cmovl', 'cmovle', 'cmovna', 'cmovnae', 'cmovnb',
+        'cmovnbe', 'cmovnc', 'cmovne', 'cmovng', 'cmovnge', 'cmovnl',
+        'cmovnle', 'cmovno', 'cmovnp', 'cmovns', 'cmovnz', 'cmovo',
+        'cmovp', 'cmovpe', 'cmovpo', 'cmovs', 'cmovz', 'cmp', 'cmpsb',
+        'cmpsd', 'cmpsw', 'cmpxchg', 'cmpxchg486', 'cmpxchg8b', 'cpuid',
+        'cwd', 'cwde', 'daa', 'das', 'dec', 'div', 'emms', 'enter', 'hlt',
+        'ibts', 'icebp', 'idiv', 'imul', 'in', 'inc', 'insb', 'insd',
+        'insw', 'int', 'int01', 'int03', 'int1', 'int3', 'into', 'invd',
+        'invlpg', 'iret', 'iretd', 'iretw', 'ja', 'jae', 'jb', 'jbe',
+        'jc', 'jcxz', 'jcxz', 'je', 'jecxz', 'jg', 'jge', 'jl', 'jle',
+        'jmp', 'jna', 'jnae', 'jnb', 'jnbe', 'jnc', 'jne', 'jng', 'jnge',
+        'jnl', 'jnle', 'jno', 'jnp', 'jns', 'jnz', 'jo', 'jp', 'jpe',
+        'jpo', 'js', 'jz', 'lahf', 'lar', 'lcall', 'lds', 'lea', 'leave',
+        'les', 'lfs', 'lgdt', 'lgs', 'lidt', 'ljmp', 'lldt', 'lmsw',
+        'loadall', 'loadall286', 'lock', 'lodsb', 'lodsd', 'lodsw',
+        'loop', 'loope', 'loopne', 'loopnz', 'loopz', 'lsl', 'lss', 'ltr',
+        'mov', 'movd', 'movq', 'movsb', 'movsd', 'movsw', 'movsx',
+        'movzx', 'mul', 'neg', 'nop', 'not', 'or', 'out', 'outsb', 'outsd',
+        'outsw', 'pop', 'popa', 'popad', 'popaw', 'popf', 'popfd', 'popfw',
+        'push', 'pusha', 'pushad', 'pushaw', 'pushf', 'pushfd', 'pushfw',
+        'rcl', 'rcr', 'rdmsr', 'rdpmc', 'rdshr', 'rdtsc', 'rep', 'repe',
+        'repne', 'repnz', 'repz', 'ret', 'retf', 'retn', 'rol', 'ror',
+        'rsdc', 'rsldt', 'rsm', 'sahf', 'sal', 'salc', 'sar', 'sbb',
+        'scasb', 'scasd', 'scasw', 'seta', 'setae', 'setb', 'setbe',
+        'setc', 'setcxz', 'sete', 'setg', 'setge', 'setl', 'setle',
+        'setna', 'setnae', 'setnb', 'setnbe', 'setnc', 'setne', 'setng',
+        'setnge', 'setnl', 'setnle', 'setno', 'setnp', 'setns', 'setnz',
+        'seto', 'setp', 'setpe', 'setpo', 'sets', 'setz', 'sgdt', 'shl',
+        'shld', 'shr', 'shrd', 'sidt', 'sldt', 'smi', 'smint', 'smintold',
+        'smsw', 'stc', 'std', 'sti', 'stosb', 'stosd', 'stosw', 'str',
+        'sub', 'svdc', 'svldt', 'svts', 'syscall', 'sysenter', 'sysexit',
+        'sysret', 'test', 'ud1', 'ud2', 'umov', 'verr', 'verw', 'wait',
+        'wbinvd', 'wrmsr', 'wrshr', 'xadd', 'xbts', 'xchg', 'xlat',
+        'xlatb', 'xor'
+    }
+
+    PORTUGOL_KEYWORDS = (
+        'aleatorio',
+        'algoritmo',
+        'arquivo',
+        'ate',
+        'caso',
+        'cronometro',
+        'debug',
+        'e',
+        'eco',
+        'enquanto',
+        'entao',
+        'escolha',
+        'escreva',
+        'escreval',
+        'faca',
+        'falso',
+        'fimalgoritmo',
+        'fimenquanto',
+        'fimescolha',
+        'fimfuncao',
+        'fimpara',
+        'fimprocedimento',
+        'fimrepita',
+        'fimse',
+        'funcao',
+        'inicio',
+        'int',
+        'interrompa',
+        'leia',
+        'limpatela',
+        'mod',
+        'nao',
+        'ou',
+        'outrocaso',
+        'para',
+        'passo',
+        'pausa',
+        'procedimento',
+        'repita',
+        'retorne',
+        'se',
+        'senao',
+        'timer',
+        'var',
+        'vetor',
+        'verdadeiro',
+        'xou',
+        'div',
+        'mod',
+        'abs',
+        'arccos',
+        'arcsen',
+        'arctan',
+        'cos',
+        'cotan',
+        'Exp',
+        'grauprad',
+        'int',
+        'log',
+        'logn',
+        'pi',
+        'quad',
+        'radpgrau',
+        'raizq',
+        'rand',
+        'randi',
+        'sen',
+        'Tan',
+        'asc',
+        'carac',
+        'caracpnum',
+        'compr',
+        'copia',
+        'maiusc',
+        'minusc',
+        'numpcarac',
+        'pos',
+    )
+
+    PORTUGOL_BUILTIN_TYPES = {
+        'inteiro', 'real', 'caractere', 'logico'
+    }
+
+    def __init__(self, **options):
+        Lexer.__init__(self, **options)
+        self.keywords = set()
+        self.builtins = set()
+        if get_bool_opt(options, 'portugol', False):
+            self.keywords.update(self.PORTUGOL_KEYWORDS)
+            self.builtins.update(self.PORTUGOL_BUILTIN_TYPES)
+            self.is_portugol = True
+        else:
+            self.is_portugol = False
+
+            if get_bool_opt(options, 'turbopascal', True):
+                self.keywords.update(self.TURBO_PASCAL_KEYWORDS)
+            if get_bool_opt(options, 'delphi', True):
+                self.keywords.update(self.DELPHI_KEYWORDS)
+            if get_bool_opt(options, 'freepascal', True):
+                self.keywords.update(self.FREE_PASCAL_KEYWORDS)
+            for unit in get_list_opt(options, 'units', list(self.BUILTIN_UNITS)):
+                self.builtins.update(self.BUILTIN_UNITS[unit])
+
+    def get_tokens_unprocessed(self, text):
+        scanner = Scanner(text, re.DOTALL | re.MULTILINE | re.IGNORECASE)
+        stack = ['initial']
+        in_function_block = False
+        in_property_block = False
+        was_dot = False
+        next_token_is_function = False
+        next_token_is_property = False
+        collect_labels = False
+        block_labels = set()
+        brace_balance = [0, 0]
+
+        while not scanner.eos:
+            token = Error
+
+            if stack[-1] == 'initial':
+                if scanner.scan(r'\s+'):
+                    token = Whitespace
+                elif not self.is_portugol and scanner.scan(r'\{.*?\}|\(\*.*?\*\)'):
+                    if scanner.match.startswith('$'):
+                        token = Comment.Preproc
+                    else:
+                        token = Comment.Multiline
+                elif scanner.scan(r'//.*?$'):
+                    token = Comment.Single
+                elif self.is_portugol and scanner.scan(r'(<\-)|(>=)|(<=)|%|<|>|-|\+|\*|\=|(<>)|\/|\.|:|,'):
+                    token = Operator
+                elif not self.is_portugol and scanner.scan(r'[-+*\/=<>:;,.@\^]'):
+                    token = Operator
+                    # stop label highlighting on next ";"
+                    if collect_labels and scanner.match == ';':
+                        collect_labels = False
+                elif scanner.scan(r'[\(\)\[\]]+'):
+                    token = Punctuation
+                    # abort function naming ``foo = Function(...)``
+                    next_token_is_function = False
+                    # if we are in a function block we count the open
+                    # braces because ootherwise it's impossible to
+                    # determine the end of the modifier context
+                    if in_function_block or in_property_block:
+                        if scanner.match == '(':
+                            brace_balance[0] += 1
+                        elif scanner.match == ')':
+                            brace_balance[0] -= 1
+                        elif scanner.match == '[':
+                            brace_balance[1] += 1
+                        elif scanner.match == ']':
+                            brace_balance[1] -= 1
+                elif scanner.scan(r'[A-Za-z_][A-Za-z_0-9]*'):
+                    lowercase_name = scanner.match.lower()
+                    if lowercase_name == 'result':
+                        token = Name.Builtin.Pseudo
+                    elif lowercase_name in self.keywords:
+                        token = Keyword
+                        # if we are in a special block and a
+                        # block ending keyword occurs (and the parenthesis
+                        # is balanced) we end the current block context
+                        if self.is_portugol:
+                            if lowercase_name in ('funcao', 'procedimento'):
+                                in_function_block = True
+                                next_token_is_function = True
+                        else:
+                            if (in_function_block or in_property_block) and \
+                                    lowercase_name in self.BLOCK_KEYWORDS and \
+                                    brace_balance[0] <= 0 and \
+                                    brace_balance[1] <= 0:
+                                in_function_block = False
+                                in_property_block = False
+                                brace_balance = [0, 0]
+                                block_labels = set()
+                            if lowercase_name in ('label', 'goto'):
+                                collect_labels = True
+                            elif lowercase_name == 'asm':
+                                stack.append('asm')
+                            elif lowercase_name == 'property':
+                                in_property_block = True
+                                next_token_is_property = True
+                            elif lowercase_name in ('procedure', 'operator',
+                                                    'function', 'constructor',
+                                                    'destructor'):
+                                in_function_block = True
+                                next_token_is_function = True
+                    # we are in a function block and the current name
+                    # is in the set of registered modifiers. highlight
+                    # it as pseudo keyword
+                    elif not self.is_portugol and in_function_block and \
+                            lowercase_name in self.FUNCTION_MODIFIERS:
+                        token = Keyword.Pseudo
+                    # if we are in a property highlight some more
+                    # modifiers
+                    elif not self.is_portugol and in_property_block and \
+                            lowercase_name in ('read', 'write'):
+                        token = Keyword.Pseudo
+                        next_token_is_function = True
+                    # if the last iteration set next_token_is_function
+                    # to true we now want this name highlighted as
+                    # function. so do that and reset the state
+                    elif next_token_is_function:
+                        # Look if the next token is a dot. If yes it's
+                        # not a function, but a class name and the
+                        # part after the dot a function name
+                        if not self.is_portugol and scanner.test(r'\s*\.\s*'):
+                            token = Name.Class
+                        # it's not a dot, our job is done
+                        else:
+                            token = Name.Function
+                            next_token_is_function = False
+
+                            if self.is_portugol:
+                                block_labels.add(scanner.match.lower())
+
+                    # same for properties
+                    elif not self.is_portugol and next_token_is_property:
+                        token = Name.Property
+                        next_token_is_property = False
+                    # Highlight this token as label and add it
+                    # to the list of known labels
+                    elif not self.is_portugol and collect_labels:
+                        token = Name.Label
+                        block_labels.add(scanner.match.lower())
+                    # name is in list of known labels
+                    elif lowercase_name in block_labels:
+                        token = Name.Label
+                    elif self.is_portugol and lowercase_name in self.PORTUGOL_BUILTIN_TYPES:
+                        token = Keyword.Type
+                    elif not self.is_portugol and lowercase_name in self.BUILTIN_TYPES:
+                        token = Keyword.Type
+                    elif not self.is_portugol and lowercase_name in self.DIRECTIVES:
+                        token = Keyword.Pseudo
+                    # builtins are just builtins if the token
+                    # before isn't a dot
+                    elif not self.is_portugol and not was_dot and lowercase_name in self.builtins:
+                        token = Name.Builtin
+                    else:
+                        token = Name
+                elif self.is_portugol and scanner.scan(r"\""):
+                    token = String
+                    stack.append('string')
+                elif not self.is_portugol and scanner.scan(r"'"):
+                    token = String
+                    stack.append('string')
+                elif not self.is_portugol and scanner.scan(r'\#(\d+|\$[0-9A-Fa-f]+)'):
+                    token = String.Char
+                elif not self.is_portugol and scanner.scan(r'\$[0-9A-Fa-f]+'):
+                    token = Number.Hex
+                elif scanner.scan(r'\d+(?![eE]|\.[^.])'):
+                    token = Number.Integer
+                elif scanner.scan(r'\d+(\.\d+([eE][+-]?\d+)?|[eE][+-]?\d+)'):
+                    token = Number.Float
+                else:
+                    # if the stack depth is deeper than once, pop
+                    if len(stack) > 1:
+                        stack.pop()
+                    scanner.get_char()
+
+            elif stack[-1] == 'string':
+                if self.is_portugol:
+                    if scanner.scan(r"''"):
+                        token = String.Escape
+                    elif scanner.scan(r"\""):
+                        token = String
+                        stack.pop()
+                    elif scanner.scan(r"[^\"]*"):
+                        token = String
+                    else:
+                        scanner.get_char()
+                        stack.pop()
+                else:
+                    if scanner.scan(r"''"):
+                        token = String.Escape
+                    elif scanner.scan(r"'"):
+                        token = String
+                        stack.pop()
+                    elif scanner.scan(r"[^']*"):
+                        token = String
+                    else:
+                        scanner.get_char()
+                        stack.pop()
+            elif not self.is_portugol and stack[-1] == 'asm':
+                if scanner.scan(r'\s+'):
+                    token = Whitespace
+                elif scanner.scan(r'end'):
+                    token = Keyword
+                    stack.pop()
+                elif scanner.scan(r'\{.*?\}|\(\*.*?\*\)'):
+                    if scanner.match.startswith('$'):
+                        token = Comment.Preproc
+                    else:
+                        token = Comment.Multiline
+                elif scanner.scan(r'//.*?$'):
+                    token = Comment.Single
+                elif scanner.scan(r"'"):
+                    token = String
+                    stack.append('string')
+                elif scanner.scan(r'@@[A-Za-z_][A-Za-z_0-9]*'):
+                    token = Name.Label
+                elif scanner.scan(r'[A-Za-z_][A-Za-z_0-9]*'):
+                    lowercase_name = scanner.match.lower()
+                    if lowercase_name in self.ASM_INSTRUCTIONS:
+                        token = Keyword
+                    elif lowercase_name in self.ASM_REGISTERS:
+                        token = Name.Builtin
+                    else:
+                        token = Name
+                elif scanner.scan(r'[-+*\/=<>:;,.@\^]+'):
+                    token = Operator
+                elif scanner.scan(r'[\(\)\[\]]+'):
+                    token = Punctuation
+                elif scanner.scan(r'\$[0-9A-Fa-f]+'):
+                    token = Number.Hex
+                elif scanner.scan(r'\d+(?![eE]|\.[^.])'):
+                    token = Number.Integer
+                elif scanner.scan(r'\d+(\.\d+([eE][+-]?\d+)?|[eE][+-]?\d+)'):
+                    token = Number.Float
+                else:
+                    scanner.get_char()
+                    stack.pop()
+
+            # save the dot!!!11
+            if not self.is_portugol and scanner.match.strip():
+                was_dot = scanner.match == '.'
+
+            yield scanner.start_pos, token, scanner.match or ''
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/pawn.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/pawn.py
new file mode 100644
index 00000000..99d9c963
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/pawn.py
@@ -0,0 +1,202 @@
+"""
+    pygments.lexers.pawn
+    ~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for the Pawn languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation
+from pygments.util import get_bool_opt
+
+__all__ = ['SourcePawnLexer', 'PawnLexer']
+
+
+class SourcePawnLexer(RegexLexer):
+    """
+    For SourcePawn source code with preprocessor directives.
+    """
+    name = 'SourcePawn'
+    aliases = ['sp']
+    filenames = ['*.sp']
+    mimetypes = ['text/x-sourcepawn']
+    url = 'https://github.com/alliedmodders/sourcepawn'
+    version_added = '1.6'
+
+    #: optional Comment or Whitespace
+    _ws = r'(?:\s|//.*?\n|/\*.*?\*/)+'
+    #: only one /* */ style comment
+    _ws1 = r'\s*(?:/[*].*?[*]/\s*)*'
+
+    tokens = {
+        'root': [
+            # preprocessor directives: without whitespace
+            (r'^#if\s+0', Comment.Preproc, 'if0'),
+            ('^#', Comment.Preproc, 'macro'),
+            # or with whitespace
+            ('^' + _ws1 + r'#if\s+0', Comment.Preproc, 'if0'),
+            ('^' + _ws1 + '#', Comment.Preproc, 'macro'),
+            (r'\n', Text),
+            (r'\s+', Text),
+            (r'\\\n', Text),  # line continuation
+            (r'/(\\\n)?/(\n|(.|\n)*?[^\\]\n)', Comment.Single),
+            (r'/(\\\n)?\*(.|\n)*?\*(\\\n)?/', Comment.Multiline),
+            (r'[{}]', Punctuation),
+            (r'L?"', String, 'string'),
+            (r"L?'(\\.|\\[0-7]{1,3}|\\x[a-fA-F0-9]{1,2}|[^\\\'\n])'", String.Char),
+            (r'(\d+\.\d*|\.\d+|\d+)[eE][+-]?\d+[LlUu]*', Number.Float),
+            (r'(\d+\.\d*|\.\d+|\d+[fF])[fF]?', Number.Float),
+            (r'0x[0-9a-fA-F]+[LlUu]*', Number.Hex),
+            (r'0[0-7]+[LlUu]*', Number.Oct),
+            (r'\d+[LlUu]*', Number.Integer),
+            (r'[~!%^&*+=|?:<>/-]', Operator),
+            (r'[()\[\],.;]', Punctuation),
+            (r'(case|const|continue|native|'
+             r'default|else|enum|for|if|new|operator|'
+             r'public|return|sizeof|static|decl|struct|switch)\b', Keyword),
+            (r'(bool|Float)\b', Keyword.Type),
+            (r'(true|false)\b', Keyword.Constant),
+            (r'[a-zA-Z_]\w*', Name),
+        ],
+        'string': [
+            (r'"', String, '#pop'),
+            (r'\\([\\abfnrtv"\']|x[a-fA-F0-9]{2,4}|[0-7]{1,3})', String.Escape),
+            (r'[^\\"\n]+', String),  # all other characters
+            (r'\\\n', String),       # line continuation
+            (r'\\', String),         # stray backslash
+        ],
+        'macro': [
+            (r'[^/\n]+', Comment.Preproc),
+            (r'/\*(.|\n)*?\*/', Comment.Multiline),
+            (r'//.*?\n', Comment.Single, '#pop'),
+            (r'/', Comment.Preproc),
+            (r'(?<=\\)\n', Comment.Preproc),
+            (r'\n', Comment.Preproc, '#pop'),
+        ],
+        'if0': [
+            (r'^\s*#if.*?(?/-]', Operator),
+            (r'[()\[\],.;]', Punctuation),
+            (r'(switch|case|default|const|new|static|char|continue|break|'
+             r'if|else|for|while|do|operator|enum|'
+             r'public|return|sizeof|tagof|state|goto)\b', Keyword),
+            (r'(bool|Float)\b', Keyword.Type),
+            (r'(true|false)\b', Keyword.Constant),
+            (r'[a-zA-Z_]\w*', Name),
+        ],
+        'string': [
+            (r'"', String, '#pop'),
+            (r'\\([\\abfnrtv"\']|x[a-fA-F0-9]{2,4}|[0-7]{1,3})', String.Escape),
+            (r'[^\\"\n]+', String),  # all other characters
+            (r'\\\n', String),       # line continuation
+            (r'\\', String),         # stray backslash
+        ],
+        'macro': [
+            (r'[^/\n]+', Comment.Preproc),
+            (r'/\*(.|\n)*?\*/', Comment.Multiline),
+            (r'//.*?\n', Comment.Single, '#pop'),
+            (r'/', Comment.Preproc),
+            (r'(?<=\\)\n', Comment.Preproc),
+            (r'\n', Comment.Preproc, '#pop'),
+        ],
+        'if0': [
+            (r'^\s*#if.*?(?<-]', Operator),
+            (r'[a-zA-Z][a-zA-Z0-9_-]*', Name),
+            (r'\?[a-zA-Z][a-zA-Z0-9_-]*', Name.Variable),
+            (r'[0-9]+\.[0-9]+', Number.Float),
+            (r'[0-9]+', Number.Integer),
+        ],
+        'keywords': [
+            (words((
+                ':requirements', ':types', ':constants',
+                ':predicates', ':functions', ':action', ':agent',
+                ':parameters', ':precondition', ':effect',
+                ':durative-action', ':duration', ':condition',
+                ':derived', ':domain', ':objects', ':init',
+                ':goal', ':metric', ':length', ':serial', ':parallel',
+                # the following are requirements
+                ':strips', ':typing', ':negative-preconditions',
+                ':disjunctive-preconditions', ':equality',
+                ':existential-preconditions', ':universal-preconditions',
+                ':conditional-effects', ':fluents', ':numeric-fluents',
+                ':object-fluents', ':adl', ':durative-actions',
+                ':continuous-effects', ':derived-predicates',
+                ':time-intial-literals', ':preferences',
+                ':constraints', ':action-costs', ':multi-agent',
+                ':unfactored-privacy', ':factored-privacy',
+                ':non-deterministic'
+                ), suffix=r'\b'), Keyword)
+        ],
+        'builtins': [
+            (words((
+                'define', 'domain', 'object', 'either', 'and',
+                'forall', 'preference', 'imply', 'or', 'exists',
+                'not', 'when', 'assign', 'scale-up', 'scale-down',
+                'increase', 'decrease', 'at', 'over', 'start',
+                'end', 'all', 'problem', 'always', 'sometime',
+                'within', 'at-most-once', 'sometime-after',
+                'sometime-before', 'always-within', 'hold-during',
+                'hold-after', 'minimize', 'maximize',
+                'total-time', 'is-violated'), suffix=r'\b'),
+                Name.Builtin)
+        ]
+    }
+
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/perl.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/perl.py
new file mode 100644
index 00000000..33f91f58
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/perl.py
@@ -0,0 +1,733 @@
+"""
+    pygments.lexers.perl
+    ~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for Perl, Raku and related languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, ExtendedRegexLexer, include, bygroups, \
+    using, this, default, words
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation, Whitespace
+from pygments.util import shebang_matches
+
+__all__ = ['PerlLexer', 'Perl6Lexer']
+
+
+class PerlLexer(RegexLexer):
+    """
+    For Perl source code.
+    """
+
+    name = 'Perl'
+    url = 'https://www.perl.org'
+    aliases = ['perl', 'pl']
+    filenames = ['*.pl', '*.pm', '*.t', '*.perl']
+    mimetypes = ['text/x-perl', 'application/x-perl']
+    version_added = ''
+
+    flags = re.DOTALL | re.MULTILINE
+    # TODO: give this to a perl guy who knows how to parse perl...
+    tokens = {
+        'balanced-regex': [
+            (r'/(\\\\|\\[^\\]|[^\\/])*/[egimosx]*', String.Regex, '#pop'),
+            (r'!(\\\\|\\[^\\]|[^\\!])*![egimosx]*', String.Regex, '#pop'),
+            (r'\\(\\\\|[^\\])*\\[egimosx]*', String.Regex, '#pop'),
+            (r'\{(\\\\|\\[^\\]|[^\\}])*\}[egimosx]*', String.Regex, '#pop'),
+            (r'<(\\\\|\\[^\\]|[^\\>])*>[egimosx]*', String.Regex, '#pop'),
+            (r'\[(\\\\|\\[^\\]|[^\\\]])*\][egimosx]*', String.Regex, '#pop'),
+            (r'\((\\\\|\\[^\\]|[^\\)])*\)[egimosx]*', String.Regex, '#pop'),
+            (r'@(\\\\|\\[^\\]|[^\\@])*@[egimosx]*', String.Regex, '#pop'),
+            (r'%(\\\\|\\[^\\]|[^\\%])*%[egimosx]*', String.Regex, '#pop'),
+            (r'\$(\\\\|\\[^\\]|[^\\$])*\$[egimosx]*', String.Regex, '#pop'),
+        ],
+        'root': [
+            (r'\A\#!.+?$', Comment.Hashbang),
+            (r'\#.*?$', Comment.Single),
+            (r'^=[a-zA-Z0-9]+\s+.*?\n=cut', Comment.Multiline),
+            (words((
+                'case', 'continue', 'do', 'else', 'elsif', 'for', 'foreach',
+                'if', 'last', 'my', 'next', 'our', 'redo', 'reset', 'then',
+                'unless', 'until', 'while', 'print', 'new', 'BEGIN',
+                'CHECK', 'INIT', 'END', 'return'), suffix=r'\b'),
+             Keyword),
+            (r'(format)(\s+)(\w+)(\s*)(=)(\s*\n)',
+             bygroups(Keyword, Whitespace, Name, Whitespace, Punctuation, Whitespace), 'format'),
+            (r'(eq|lt|gt|le|ge|ne|not|and|or|cmp)\b', Operator.Word),
+            # common delimiters
+            (r's/(\\\\|\\[^\\]|[^\\/])*/(\\\\|\\[^\\]|[^\\/])*/[egimosx]*',
+                String.Regex),
+            (r's!(\\\\|\\!|[^!])*!(\\\\|\\!|[^!])*![egimosx]*', String.Regex),
+            (r's\\(\\\\|[^\\])*\\(\\\\|[^\\])*\\[egimosx]*', String.Regex),
+            (r's@(\\\\|\\[^\\]|[^\\@])*@(\\\\|\\[^\\]|[^\\@])*@[egimosx]*',
+                String.Regex),
+            (r's%(\\\\|\\[^\\]|[^\\%])*%(\\\\|\\[^\\]|[^\\%])*%[egimosx]*',
+                String.Regex),
+            # balanced delimiters
+            (r's\{(\\\\|\\[^\\]|[^\\}])*\}\s*', String.Regex, 'balanced-regex'),
+            (r's<(\\\\|\\[^\\]|[^\\>])*>\s*', String.Regex, 'balanced-regex'),
+            (r's\[(\\\\|\\[^\\]|[^\\\]])*\]\s*', String.Regex,
+                'balanced-regex'),
+            (r's\((\\\\|\\[^\\]|[^\\)])*\)\s*', String.Regex,
+                'balanced-regex'),
+
+            (r'm?/(\\\\|\\[^\\]|[^\\/\n])*/[gcimosx]*', String.Regex),
+            (r'm(?=[/!\\{<\[(@%$])', String.Regex, 'balanced-regex'),
+            (r'((?<==~)|(?<=\())\s*/(\\\\|\\[^\\]|[^\\/])*/[gcimosx]*',
+                String.Regex),
+            (r'\s+', Whitespace),
+            (words((
+                'abs', 'accept', 'alarm', 'atan2', 'bind', 'binmode', 'bless', 'caller', 'chdir',
+                'chmod', 'chomp', 'chop', 'chown', 'chr', 'chroot', 'close', 'closedir', 'connect',
+                'continue', 'cos', 'crypt', 'dbmclose', 'dbmopen', 'defined', 'delete', 'die',
+                'dump', 'each', 'endgrent', 'endhostent', 'endnetent', 'endprotoent',
+                'endpwent', 'endservent', 'eof', 'eval', 'exec', 'exists', 'exit', 'exp', 'fcntl',
+                'fileno', 'flock', 'fork', 'format', 'formline', 'getc', 'getgrent', 'getgrgid',
+                'getgrnam', 'gethostbyaddr', 'gethostbyname', 'gethostent', 'getlogin',
+                'getnetbyaddr', 'getnetbyname', 'getnetent', 'getpeername', 'getpgrp',
+                'getppid', 'getpriority', 'getprotobyname', 'getprotobynumber',
+                'getprotoent', 'getpwent', 'getpwnam', 'getpwuid', 'getservbyname',
+                'getservbyport', 'getservent', 'getsockname', 'getsockopt', 'glob', 'gmtime',
+                'goto', 'grep', 'hex', 'import', 'index', 'int', 'ioctl', 'join', 'keys', 'kill', 'last',
+                'lc', 'lcfirst', 'length', 'link', 'listen', 'local', 'localtime', 'log', 'lstat',
+                'map', 'mkdir', 'msgctl', 'msgget', 'msgrcv', 'msgsnd', 'my', 'next', 'oct', 'open',
+                'opendir', 'ord', 'our', 'pack', 'pipe', 'pop', 'pos', 'printf',
+                'prototype', 'push', 'quotemeta', 'rand', 'read', 'readdir',
+                'readline', 'readlink', 'readpipe', 'recv', 'redo', 'ref', 'rename',
+                'reverse', 'rewinddir', 'rindex', 'rmdir', 'scalar', 'seek', 'seekdir',
+                'select', 'semctl', 'semget', 'semop', 'send', 'setgrent', 'sethostent', 'setnetent',
+                'setpgrp', 'setpriority', 'setprotoent', 'setpwent', 'setservent',
+                'setsockopt', 'shift', 'shmctl', 'shmget', 'shmread', 'shmwrite', 'shutdown',
+                'sin', 'sleep', 'socket', 'socketpair', 'sort', 'splice', 'split', 'sprintf', 'sqrt',
+                'srand', 'stat', 'study', 'substr', 'symlink', 'syscall', 'sysopen', 'sysread',
+                'sysseek', 'system', 'syswrite', 'tell', 'telldir', 'tie', 'tied', 'time', 'times', 'tr',
+                'truncate', 'uc', 'ucfirst', 'umask', 'undef', 'unlink', 'unpack', 'unshift', 'untie',
+                'utime', 'values', 'vec', 'wait', 'waitpid', 'wantarray', 'warn', 'write'), suffix=r'\b'),
+             Name.Builtin),
+            (r'((__(DATA|DIE|WARN)__)|(STD(IN|OUT|ERR)))\b', Name.Builtin.Pseudo),
+            (r'(<<)([\'"]?)([a-zA-Z_]\w*)(\2;?\n.*?\n)(\3)(\n)',
+             bygroups(String, String, String.Delimiter, String, String.Delimiter, Whitespace)),
+            (r'__END__', Comment.Preproc, 'end-part'),
+            (r'\$\^[ADEFHILMOPSTWX]', Name.Variable.Global),
+            (r"\$[\\\"\[\]'&`+*.,;=%~?@$!<>(^|/-](?!\w)", Name.Variable.Global),
+            (r'[$@%#]+', Name.Variable, 'varname'),
+            (r'0_?[0-7]+(_[0-7]+)*', Number.Oct),
+            (r'0x[0-9A-Fa-f]+(_[0-9A-Fa-f]+)*', Number.Hex),
+            (r'0b[01]+(_[01]+)*', Number.Bin),
+            (r'(?i)(\d*(_\d*)*\.\d+(_\d*)*|\d+(_\d*)*\.\d+(_\d*)*)(e[+-]?\d+)?',
+             Number.Float),
+            (r'(?i)\d+(_\d*)*e[+-]?\d+(_\d*)*', Number.Float),
+            (r'\d+(_\d+)*', Number.Integer),
+            (r"'(\\\\|\\[^\\]|[^'\\])*'", String),
+            (r'"(\\\\|\\[^\\]|[^"\\])*"', String),
+            (r'`(\\\\|\\[^\\]|[^`\\])*`', String.Backtick),
+            (r'<([^\s>]+)>', String.Regex),
+            (r'(q|qq|qw|qr|qx)\{', String.Other, 'cb-string'),
+            (r'(q|qq|qw|qr|qx)\(', String.Other, 'rb-string'),
+            (r'(q|qq|qw|qr|qx)\[', String.Other, 'sb-string'),
+            (r'(q|qq|qw|qr|qx)\<', String.Other, 'lt-string'),
+            (r'(q|qq|qw|qr|qx)([\W_])(.|\n)*?\2', String.Other),
+            (r'(package)(\s+)([a-zA-Z_]\w*(?:::[a-zA-Z_]\w*)*)',
+             bygroups(Keyword, Whitespace, Name.Namespace)),
+            (r'(use|require|no)(\s+)([a-zA-Z_]\w*(?:::[a-zA-Z_]\w*)*)',
+             bygroups(Keyword, Whitespace, Name.Namespace)),
+            (r'(sub)(\s+)', bygroups(Keyword, Whitespace), 'funcname'),
+            (words((
+                'no', 'package', 'require', 'use'), suffix=r'\b'),
+             Keyword),
+            (r'(\[\]|\*\*|::|<<|>>|>=|<=>|<=|={3}|!=|=~|'
+             r'!~|&&?|\|\||\.{1,3})', Operator),
+            (r'[-+/*%=<>&^|!\\~]=?', Operator),
+            (r'[()\[\]:;,<>/?{}]', Punctuation),  # yes, there's no shortage
+                                                  # of punctuation in Perl!
+            (r'(?=\w)', Name, 'name'),
+        ],
+        'format': [
+            (r'\.\n', String.Interpol, '#pop'),
+            (r'[^\n]*\n', String.Interpol),
+        ],
+        'varname': [
+            (r'\s+', Whitespace),
+            (r'\{', Punctuation, '#pop'),    # hash syntax?
+            (r'\)|,', Punctuation, '#pop'),  # argument specifier
+            (r'\w+::', Name.Namespace),
+            (r'[\w:]+', Name.Variable, '#pop'),
+        ],
+        'name': [
+            (r'[a-zA-Z_]\w*(::[a-zA-Z_]\w*)*(::)?(?=\s*->)', Name.Namespace, '#pop'),
+            (r'[a-zA-Z_]\w*(::[a-zA-Z_]\w*)*::', Name.Namespace, '#pop'),
+            (r'[\w:]+', Name, '#pop'),
+            (r'[A-Z_]+(?=\W)', Name.Constant, '#pop'),
+            (r'(?=\W)', Text, '#pop'),
+        ],
+        'funcname': [
+            (r'[a-zA-Z_]\w*[!?]?', Name.Function),
+            (r'\s+', Whitespace),
+            # argument declaration
+            (r'(\([$@%]*\))(\s*)', bygroups(Punctuation, Whitespace)),
+            (r';', Punctuation, '#pop'),
+            (r'.*?\{', Punctuation, '#pop'),
+        ],
+        'cb-string': [
+            (r'\\[{}\\]', String.Other),
+            (r'\\', String.Other),
+            (r'\{', String.Other, 'cb-string'),
+            (r'\}', String.Other, '#pop'),
+            (r'[^{}\\]+', String.Other)
+        ],
+        'rb-string': [
+            (r'\\[()\\]', String.Other),
+            (r'\\', String.Other),
+            (r'\(', String.Other, 'rb-string'),
+            (r'\)', String.Other, '#pop'),
+            (r'[^()]+', String.Other)
+        ],
+        'sb-string': [
+            (r'\\[\[\]\\]', String.Other),
+            (r'\\', String.Other),
+            (r'\[', String.Other, 'sb-string'),
+            (r'\]', String.Other, '#pop'),
+            (r'[^\[\]]+', String.Other)
+        ],
+        'lt-string': [
+            (r'\\[<>\\]', String.Other),
+            (r'\\', String.Other),
+            (r'\<', String.Other, 'lt-string'),
+            (r'\>', String.Other, '#pop'),
+            (r'[^<>]+', String.Other)
+        ],
+        'end-part': [
+            (r'.+', Comment.Preproc, '#pop')
+        ]
+    }
+
+    def analyse_text(text):
+        if shebang_matches(text, r'perl'):
+            return True
+
+        result = 0
+
+        if re.search(r'(?:my|our)\s+[$@%(]', text):
+            result += 0.9
+
+        if ':=' in text:
+            # := is not valid Perl, but it appears in unicon, so we should
+            # become less confident if we think we found Perl with :=
+            result /= 2
+
+        return result
+
+
+class Perl6Lexer(ExtendedRegexLexer):
+    """
+    For Raku (a.k.a. Perl 6) source code.
+    """
+
+    name = 'Perl6'
+    url = 'https://www.raku.org'
+    aliases = ['perl6', 'pl6', 'raku']
+    filenames = ['*.pl', '*.pm', '*.nqp', '*.p6', '*.6pl', '*.p6l', '*.pl6',
+                 '*.6pm', '*.p6m', '*.pm6', '*.t', '*.raku', '*.rakumod',
+                 '*.rakutest', '*.rakudoc']
+    mimetypes = ['text/x-perl6', 'application/x-perl6']
+    version_added = '2.0'
+    flags = re.MULTILINE | re.DOTALL
+
+    PERL6_IDENTIFIER_RANGE = r"['\w:-]"
+
+    PERL6_KEYWORDS = (
+        #Phasers
+        'BEGIN','CATCH','CHECK','CLOSE','CONTROL','DOC','END','ENTER','FIRST',
+        'INIT','KEEP','LAST','LEAVE','NEXT','POST','PRE','QUIT','UNDO',
+        #Keywords
+        'anon','augment','but','class','constant','default','does','else',
+        'elsif','enum','for','gather','given','grammar','has','if','import',
+        'is','let','loop','made','make','method','module','multi','my','need',
+        'orwith','our','proceed','proto','repeat','require','return',
+        'return-rw','returns','role','rule','state','sub','submethod','subset',
+        'succeed','supersede','token','try','unit','unless','until','use',
+        'when','while','with','without',
+        #Traits
+        'export','native','repr','required','rw','symbol',
+    )
+
+    PERL6_BUILTINS = (
+        'ACCEPTS','abs','abs2rel','absolute','accept','accessed','acos',
+        'acosec','acosech','acosh','acotan','acotanh','acquire','act','action',
+        'actions','add','add_attribute','add_enum_value','add_fallback',
+        'add_method','add_parent','add_private_method','add_role','add_trustee',
+        'adverb','after','all','allocate','allof','allowed','alternative-names',
+        'annotations','antipair','antipairs','any','anyof','app_lifetime',
+        'append','arch','archname','args','arity','Array','asec','asech','asin',
+        'asinh','ASSIGN-KEY','ASSIGN-POS','assuming','ast','at','atan','atan2',
+        'atanh','AT-KEY','atomic-assign','atomic-dec-fetch','atomic-fetch',
+        'atomic-fetch-add','atomic-fetch-dec','atomic-fetch-inc',
+        'atomic-fetch-sub','atomic-inc-fetch','AT-POS','attributes','auth',
+        'await','backtrace','Bag','BagHash','bail-out','base','basename',
+        'base-repeating','batch','BIND-KEY','BIND-POS','bind-stderr',
+        'bind-stdin','bind-stdout','bind-udp','bits','bless','block','Bool',
+        'bool-only','bounds','break','Bridge','broken','BUILD','build-date',
+        'bytes','cache','callframe','calling-package','CALL-ME','callsame',
+        'callwith','can','cancel','candidates','cando','can-ok','canonpath',
+        'caps','caption','Capture','cas','catdir','categorize','categorize-list',
+        'catfile','catpath','cause','ceiling','cglobal','changed','Channel',
+        'chars','chdir','child','child-name','child-typename','chmod','chomp',
+        'chop','chr','chrs','chunks','cis','classify','classify-list','cleanup',
+        'clone','close','closed','close-stdin','cmp-ok','code','codes','collate',
+        'column','comb','combinations','command','comment','compiler','Complex',
+        'compose','compose_type','composer','condition','config',
+        'configure_destroy','configure_type_checking','conj','connect',
+        'constraints','construct','contains','contents','copy','cos','cosec',
+        'cosech','cosh','cotan','cotanh','count','count-only','cpu-cores',
+        'cpu-usage','CREATE','create_type','cross','cue','curdir','curupdir','d',
+        'Date','DateTime','day','daycount','day-of-month','day-of-week',
+        'day-of-year','days-in-month','declaration','decode','decoder','deepmap',
+        'default','defined','DEFINITE','delayed','DELETE-KEY','DELETE-POS',
+        'denominator','desc','DESTROY','destroyers','devnull','diag',
+        'did-you-mean','die','dies-ok','dir','dirname','dir-sep','DISTROnames',
+        'do','does','does-ok','done','done-testing','duckmap','dynamic','e',
+        'eager','earlier','elems','emit','enclosing','encode','encoder',
+        'encoding','end','ends-with','enum_from_value','enum_value_list',
+        'enum_values','enums','eof','EVAL','eval-dies-ok','EVALFILE',
+        'eval-lives-ok','exception','excludes-max','excludes-min','EXISTS-KEY',
+        'EXISTS-POS','exit','exitcode','exp','expected','explicitly-manage',
+        'expmod','extension','f','fail','fails-like','fc','feature','file',
+        'filename','find_method','find_method_qualified','finish','first','flat',
+        'flatmap','flip','floor','flunk','flush','fmt','format','formatter',
+        'freeze','from','from-list','from-loop','from-posix','full',
+        'full-barrier','get','get_value','getc','gist','got','grab','grabpairs',
+        'grep','handle','handled','handles','hardware','has_accessor','Hash',
+        'head','headers','hh-mm-ss','hidden','hides','hour','how','hyper','id',
+        'illegal','im','in','indent','index','indices','indir','infinite',
+        'infix','infix:<+>','infix:<->','install_method_cache','Instant',
+        'instead','Int','int-bounds','interval','in-timezone','invalid-str',
+        'invert','invocant','IO','IO::Notification.watch-path','is_trusted',
+        'is_type','isa','is-absolute','isa-ok','is-approx','is-deeply',
+        'is-hidden','is-initial-thread','is-int','is-lazy','is-leap-year',
+        'isNaN','isnt','is-prime','is-relative','is-routine','is-setting',
+        'is-win','item','iterator','join','keep','kept','KERNELnames','key',
+        'keyof','keys','kill','kv','kxxv','l','lang','last','lastcall','later',
+        'lazy','lc','leading','level','like','line','lines','link','List',
+        'listen','live','lives-ok','local','lock','log','log10','lookup','lsb',
+        'made','MAIN','make','Map','match','max','maxpairs','merge','message',
+        'method','method_table','methods','migrate','min','minmax','minpairs',
+        'minute','misplaced','Mix','MixHash','mkdir','mode','modified','month',
+        'move','mro','msb','multi','multiness','my','name','named','named_names',
+        'narrow','nativecast','native-descriptor','nativesizeof','new','new_type',
+        'new-from-daycount','new-from-pairs','next','nextcallee','next-handle',
+        'nextsame','nextwith','NFC','NFD','NFKC','NFKD','nl-in','nl-out',
+        'nodemap','nok','none','norm','not','note','now','nude','Num',
+        'numerator','Numeric','of','offset','offset-in-hours','offset-in-minutes',
+        'ok','old','on-close','one','on-switch','open','opened','operation',
+        'optional','ord','ords','orig','os-error','osname','out-buffer','pack',
+        'package','package-kind','package-name','packages','pair','pairs',
+        'pairup','parameter','params','parent','parent-name','parents','parse',
+        'parse-base','parsefile','parse-names','parts','pass','path','path-sep',
+        'payload','peer-host','peer-port','periods','perl','permutations','phaser',
+        'pick','pickpairs','pid','placeholder','plan','plus','polar','poll',
+        'polymod','pop','pos','positional','posix','postfix','postmatch',
+        'precomp-ext','precomp-target','pred','prefix','prematch','prepend',
+        'print','printf','print-nl','print-to','private','private_method_table',
+        'proc','produce','Promise','prompt','protect','pull-one','push',
+        'push-all','push-at-least','push-exactly','push-until-lazy','put',
+        'qualifier-type','quit','r','race','radix','rand','range','Rat','raw',
+        're','read','readchars','readonly','ready','Real','reallocate','reals',
+        'reason','rebless','receive','recv','redispatcher','redo','reduce',
+        'rel2abs','relative','release','rename','repeated','replacement',
+        'report','reserved','resolve','restore','result','resume','rethrow',
+        'reverse','right','rindex','rmdir','role','roles_to_compose','rolish',
+        'roll','rootdir','roots','rotate','rotor','round','roundrobin',
+        'routine-type','run','rwx','s','samecase','samemark','samewith','say',
+        'schedule-on','scheduler','scope','sec','sech','second','seek','self',
+        'send','Set','set_hidden','set_name','set_package','set_rw','set_value',
+        'SetHash','set-instruments','setup_finalization','shape','share','shell',
+        'shift','sibling','sigil','sign','signal','signals','signature','sin',
+        'sinh','sink','sink-all','skip','skip-at-least','skip-at-least-pull-one',
+        'skip-one','skip-rest','sleep','sleep-timer','sleep-until','Slip','slurp',
+        'slurp-rest','slurpy','snap','snapper','so','socket-host','socket-port',
+        'sort','source','source-package','spawn','SPEC','splice','split',
+        'splitdir','splitpath','sprintf','spurt','sqrt','squish','srand','stable',
+        'start','started','starts-with','status','stderr','stdout','Str',
+        'sub_signature','subbuf','subbuf-rw','subname','subparse','subst',
+        'subst-mutate','substr','substr-eq','substr-rw','subtest','succ','sum',
+        'Supply','symlink','t','tail','take','take-rw','tan','tanh','tap',
+        'target','target-name','tc','tclc','tell','then','throttle','throw',
+        'throws-like','timezone','tmpdir','to','today','todo','toggle','to-posix',
+        'total','trailing','trans','tree','trim','trim-leading','trim-trailing',
+        'truncate','truncated-to','trusts','try_acquire','trying','twigil','type',
+        'type_captures','typename','uc','udp','uncaught_handler','unimatch',
+        'uniname','uninames','uniparse','uniprop','uniprops','unique','unival',
+        'univals','unlike','unlink','unlock','unpack','unpolar','unshift',
+        'unwrap','updir','USAGE','use-ok','utc','val','value','values','VAR',
+        'variable','verbose-config','version','VMnames','volume','vow','w','wait',
+        'warn','watch','watch-path','week','weekday-of-month','week-number',
+        'week-year','WHAT','when','WHERE','WHEREFORE','WHICH','WHO',
+        'whole-second','WHY','wordcase','words','workaround','wrap','write',
+        'write-to','x','yada','year','yield','yyyy-mm-dd','z','zip','zip-latest',
+
+    )
+
+    PERL6_BUILTIN_CLASSES = (
+        #Booleans
+        'False','True',
+        #Classes
+        'Any','Array','Associative','AST','atomicint','Attribute','Backtrace',
+        'Backtrace::Frame','Bag','Baggy','BagHash','Blob','Block','Bool','Buf',
+        'Callable','CallFrame','Cancellation','Capture','CArray','Channel','Code',
+        'compiler','Complex','ComplexStr','Cool','CurrentThreadScheduler',
+        'Cursor','Date','Dateish','DateTime','Distro','Duration','Encoding',
+        'Exception','Failure','FatRat','Grammar','Hash','HyperWhatever','Instant',
+        'Int','int16','int32','int64','int8','IntStr','IO','IO::ArgFiles',
+        'IO::CatHandle','IO::Handle','IO::Notification','IO::Path',
+        'IO::Path::Cygwin','IO::Path::QNX','IO::Path::Unix','IO::Path::Win32',
+        'IO::Pipe','IO::Socket','IO::Socket::Async','IO::Socket::INET','IO::Spec',
+        'IO::Spec::Cygwin','IO::Spec::QNX','IO::Spec::Unix','IO::Spec::Win32',
+        'IO::Special','Iterable','Iterator','Junction','Kernel','Label','List',
+        'Lock','Lock::Async','long','longlong','Macro','Map','Match',
+        'Metamodel::AttributeContainer','Metamodel::C3MRO','Metamodel::ClassHOW',
+        'Metamodel::EnumHOW','Metamodel::Finalization','Metamodel::MethodContainer',
+        'Metamodel::MROBasedMethodDispatch','Metamodel::MultipleInheritance',
+        'Metamodel::Naming','Metamodel::Primitives','Metamodel::PrivateMethodContainer',
+        'Metamodel::RoleContainer','Metamodel::Trusting','Method','Mix','MixHash',
+        'Mixy','Mu','NFC','NFD','NFKC','NFKD','Nil','Num','num32','num64',
+        'Numeric','NumStr','ObjAt','Order','Pair','Parameter','Perl','Pod::Block',
+        'Pod::Block::Code','Pod::Block::Comment','Pod::Block::Declarator',
+        'Pod::Block::Named','Pod::Block::Para','Pod::Block::Table','Pod::Heading',
+        'Pod::Item','Pointer','Positional','PositionalBindFailover','Proc',
+        'Proc::Async','Promise','Proxy','PseudoStash','QuantHash','Range','Rat',
+        'Rational','RatStr','Real','Regex','Routine','Scalar','Scheduler',
+        'Semaphore','Seq','Set','SetHash','Setty','Signature','size_t','Slip',
+        'Stash','Str','StrDistance','Stringy','Sub','Submethod','Supplier',
+        'Supplier::Preserving','Supply','Systemic','Tap','Telemetry',
+        'Telemetry::Instrument::Thread','Telemetry::Instrument::Usage',
+        'Telemetry::Period','Telemetry::Sampler','Thread','ThreadPoolScheduler',
+        'UInt','uint16','uint32','uint64','uint8','Uni','utf8','Variable',
+        'Version','VM','Whatever','WhateverCode','WrapHandle'
+    )
+
+    PERL6_OPERATORS = (
+        'X', 'Z', 'after', 'also', 'and', 'andthen', 'before', 'cmp', 'div',
+        'eq', 'eqv', 'extra', 'ff', 'fff', 'ge', 'gt', 'le', 'leg', 'lt', 'm',
+        'mm', 'mod', 'ne', 'or', 'orelse', 'rx', 's', 'tr', 'x', 'xor', 'xx',
+        '++', '--', '**', '!', '+', '-', '~', '?', '|', '||', '+^', '~^', '?^',
+        '^', '*', '/', '%', '%%', '+&', '+<', '+>', '~&', '~<', '~>', '?&',
+        'gcd', 'lcm', '+', '-', '+|', '+^', '~|', '~^', '?|', '?^',
+        '~', '&', '^', 'but', 'does', '<=>', '..', '..^', '^..', '^..^',
+        '!=', '==', '<', '<=', '>', '>=', '~~', '===', '!eqv',
+        '&&', '||', '^^', '//', 'min', 'max', '??', '!!', 'ff', 'fff', 'so',
+        'not', '<==', '==>', '<<==', '==>>','unicmp',
+    )
+
+    # Perl 6 has a *lot* of possible bracketing characters
+    # this list was lifted from STD.pm6 (https://github.com/perl6/std)
+    PERL6_BRACKETS = {
+        '\u0028': '\u0029', '\u003c': '\u003e', '\u005b': '\u005d',
+        '\u007b': '\u007d', '\u00ab': '\u00bb', '\u0f3a': '\u0f3b',
+        '\u0f3c': '\u0f3d', '\u169b': '\u169c', '\u2018': '\u2019',
+        '\u201a': '\u2019', '\u201b': '\u2019', '\u201c': '\u201d',
+        '\u201e': '\u201d', '\u201f': '\u201d', '\u2039': '\u203a',
+        '\u2045': '\u2046', '\u207d': '\u207e', '\u208d': '\u208e',
+        '\u2208': '\u220b', '\u2209': '\u220c', '\u220a': '\u220d',
+        '\u2215': '\u29f5', '\u223c': '\u223d', '\u2243': '\u22cd',
+        '\u2252': '\u2253', '\u2254': '\u2255', '\u2264': '\u2265',
+        '\u2266': '\u2267', '\u2268': '\u2269', '\u226a': '\u226b',
+        '\u226e': '\u226f', '\u2270': '\u2271', '\u2272': '\u2273',
+        '\u2274': '\u2275', '\u2276': '\u2277', '\u2278': '\u2279',
+        '\u227a': '\u227b', '\u227c': '\u227d', '\u227e': '\u227f',
+        '\u2280': '\u2281', '\u2282': '\u2283', '\u2284': '\u2285',
+        '\u2286': '\u2287', '\u2288': '\u2289', '\u228a': '\u228b',
+        '\u228f': '\u2290', '\u2291': '\u2292', '\u2298': '\u29b8',
+        '\u22a2': '\u22a3', '\u22a6': '\u2ade', '\u22a8': '\u2ae4',
+        '\u22a9': '\u2ae3', '\u22ab': '\u2ae5', '\u22b0': '\u22b1',
+        '\u22b2': '\u22b3', '\u22b4': '\u22b5', '\u22b6': '\u22b7',
+        '\u22c9': '\u22ca', '\u22cb': '\u22cc', '\u22d0': '\u22d1',
+        '\u22d6': '\u22d7', '\u22d8': '\u22d9', '\u22da': '\u22db',
+        '\u22dc': '\u22dd', '\u22de': '\u22df', '\u22e0': '\u22e1',
+        '\u22e2': '\u22e3', '\u22e4': '\u22e5', '\u22e6': '\u22e7',
+        '\u22e8': '\u22e9', '\u22ea': '\u22eb', '\u22ec': '\u22ed',
+        '\u22f0': '\u22f1', '\u22f2': '\u22fa', '\u22f3': '\u22fb',
+        '\u22f4': '\u22fc', '\u22f6': '\u22fd', '\u22f7': '\u22fe',
+        '\u2308': '\u2309', '\u230a': '\u230b', '\u2329': '\u232a',
+        '\u23b4': '\u23b5', '\u2768': '\u2769', '\u276a': '\u276b',
+        '\u276c': '\u276d', '\u276e': '\u276f', '\u2770': '\u2771',
+        '\u2772': '\u2773', '\u2774': '\u2775', '\u27c3': '\u27c4',
+        '\u27c5': '\u27c6', '\u27d5': '\u27d6', '\u27dd': '\u27de',
+        '\u27e2': '\u27e3', '\u27e4': '\u27e5', '\u27e6': '\u27e7',
+        '\u27e8': '\u27e9', '\u27ea': '\u27eb', '\u2983': '\u2984',
+        '\u2985': '\u2986', '\u2987': '\u2988', '\u2989': '\u298a',
+        '\u298b': '\u298c', '\u298d': '\u298e', '\u298f': '\u2990',
+        '\u2991': '\u2992', '\u2993': '\u2994', '\u2995': '\u2996',
+        '\u2997': '\u2998', '\u29c0': '\u29c1', '\u29c4': '\u29c5',
+        '\u29cf': '\u29d0', '\u29d1': '\u29d2', '\u29d4': '\u29d5',
+        '\u29d8': '\u29d9', '\u29da': '\u29db', '\u29f8': '\u29f9',
+        '\u29fc': '\u29fd', '\u2a2b': '\u2a2c', '\u2a2d': '\u2a2e',
+        '\u2a34': '\u2a35', '\u2a3c': '\u2a3d', '\u2a64': '\u2a65',
+        '\u2a79': '\u2a7a', '\u2a7d': '\u2a7e', '\u2a7f': '\u2a80',
+        '\u2a81': '\u2a82', '\u2a83': '\u2a84', '\u2a8b': '\u2a8c',
+        '\u2a91': '\u2a92', '\u2a93': '\u2a94', '\u2a95': '\u2a96',
+        '\u2a97': '\u2a98', '\u2a99': '\u2a9a', '\u2a9b': '\u2a9c',
+        '\u2aa1': '\u2aa2', '\u2aa6': '\u2aa7', '\u2aa8': '\u2aa9',
+        '\u2aaa': '\u2aab', '\u2aac': '\u2aad', '\u2aaf': '\u2ab0',
+        '\u2ab3': '\u2ab4', '\u2abb': '\u2abc', '\u2abd': '\u2abe',
+        '\u2abf': '\u2ac0', '\u2ac1': '\u2ac2', '\u2ac3': '\u2ac4',
+        '\u2ac5': '\u2ac6', '\u2acd': '\u2ace', '\u2acf': '\u2ad0',
+        '\u2ad1': '\u2ad2', '\u2ad3': '\u2ad4', '\u2ad5': '\u2ad6',
+        '\u2aec': '\u2aed', '\u2af7': '\u2af8', '\u2af9': '\u2afa',
+        '\u2e02': '\u2e03', '\u2e04': '\u2e05', '\u2e09': '\u2e0a',
+        '\u2e0c': '\u2e0d', '\u2e1c': '\u2e1d', '\u2e20': '\u2e21',
+        '\u3008': '\u3009', '\u300a': '\u300b', '\u300c': '\u300d',
+        '\u300e': '\u300f', '\u3010': '\u3011', '\u3014': '\u3015',
+        '\u3016': '\u3017', '\u3018': '\u3019', '\u301a': '\u301b',
+        '\u301d': '\u301e', '\ufd3e': '\ufd3f', '\ufe17': '\ufe18',
+        '\ufe35': '\ufe36', '\ufe37': '\ufe38', '\ufe39': '\ufe3a',
+        '\ufe3b': '\ufe3c', '\ufe3d': '\ufe3e', '\ufe3f': '\ufe40',
+        '\ufe41': '\ufe42', '\ufe43': '\ufe44', '\ufe47': '\ufe48',
+        '\ufe59': '\ufe5a', '\ufe5b': '\ufe5c', '\ufe5d': '\ufe5e',
+        '\uff08': '\uff09', '\uff1c': '\uff1e', '\uff3b': '\uff3d',
+        '\uff5b': '\uff5d', '\uff5f': '\uff60', '\uff62': '\uff63',
+    }
+
+    def _build_word_match(words, boundary_regex_fragment=None, prefix='', suffix=''):
+        if boundary_regex_fragment is None:
+            return r'\b(' + prefix + r'|'.join(re.escape(x) for x in words) + \
+                suffix + r')\b'
+        else:
+            return r'(? 0:
+                    next_open_pos = text.find(opening_chars, search_pos + n_chars)
+                    next_close_pos = text.find(closing_chars, search_pos + n_chars)
+
+                    if next_close_pos == -1:
+                        next_close_pos = len(text)
+                        nesting_level = 0
+                    elif next_open_pos != -1 and next_open_pos < next_close_pos:
+                        nesting_level += 1
+                        search_pos = next_open_pos
+                    else:  # next_close_pos < next_open_pos
+                        nesting_level -= 1
+                        search_pos = next_close_pos
+
+                end_pos = next_close_pos
+
+            if end_pos < 0:     # if we didn't find a closer, just highlight the
+                                # rest of the text in this class
+                end_pos = len(text)
+
+            if adverbs is not None and re.search(r':to\b', adverbs):
+                heredoc_terminator = text[match.start('delimiter') + n_chars:end_pos]
+                end_heredoc = re.search(r'^\s*' + re.escape(heredoc_terminator) +
+                                        r'\s*$', text[end_pos:], re.MULTILINE)
+
+                if end_heredoc:
+                    end_pos += end_heredoc.end()
+                else:
+                    end_pos = len(text)
+
+            yield match.start(), token_class, text[match.start():end_pos + n_chars]
+            context.pos = end_pos + n_chars
+
+        return callback
+
+    def opening_brace_callback(lexer, match, context):
+        stack = context.stack
+
+        yield match.start(), Text, context.text[match.start():match.end()]
+        context.pos = match.end()
+
+        # if we encounter an opening brace and we're one level
+        # below a token state, it means we need to increment
+        # the nesting level for braces so we know later when
+        # we should return to the token rules.
+        if len(stack) > 2 and stack[-2] == 'token':
+            context.perl6_token_nesting_level += 1
+
+    def closing_brace_callback(lexer, match, context):
+        stack = context.stack
+
+        yield match.start(), Text, context.text[match.start():match.end()]
+        context.pos = match.end()
+
+        # if we encounter a free closing brace and we're one level
+        # below a token state, it means we need to check the nesting
+        # level to see if we need to return to the token state.
+        if len(stack) > 2 and stack[-2] == 'token':
+            context.perl6_token_nesting_level -= 1
+            if context.perl6_token_nesting_level == 0:
+                stack.pop()
+
+    def embedded_perl6_callback(lexer, match, context):
+        context.perl6_token_nesting_level = 1
+        yield match.start(), Text, context.text[match.start():match.end()]
+        context.pos = match.end()
+        context.stack.append('root')
+
+    # If you're modifying these rules, be careful if you need to process '{' or '}'
+    # characters. We have special logic for processing these characters (due to the fact
+    # that you can nest Perl 6 code in regex blocks), so if you need to process one of
+    # them, make sure you also process the corresponding one!
+    tokens = {
+        'common': [
+            (r'#[`|=](?P(?P[' + ''.join(PERL6_BRACKETS) + r'])(?P=first_char)*)',
+             brackets_callback(Comment.Multiline)),
+            (r'#[^\n]*$', Comment.Single),
+            (r'^(\s*)=begin\s+(\w+)\b.*?^\1=end\s+\2', Comment.Multiline),
+            (r'^(\s*)=for.*?\n\s*?\n', Comment.Multiline),
+            (r'^=.*?\n\s*?\n', Comment.Multiline),
+            (r'(regex|token|rule)(\s*' + PERL6_IDENTIFIER_RANGE + '+:sym)',
+             bygroups(Keyword, Name), 'token-sym-brackets'),
+            (r'(regex|token|rule)(?!' + PERL6_IDENTIFIER_RANGE + r')(\s*' + PERL6_IDENTIFIER_RANGE + '+)?',
+             bygroups(Keyword, Name), 'pre-token'),
+            # deal with a special case in the Perl 6 grammar (role q { ... })
+            (r'(role)(\s+)(q)(\s*)', bygroups(Keyword, Whitespace, Name, Whitespace)),
+            (_build_word_match(PERL6_KEYWORDS, PERL6_IDENTIFIER_RANGE), Keyword),
+            (_build_word_match(PERL6_BUILTIN_CLASSES, PERL6_IDENTIFIER_RANGE, suffix='(?::[UD])?'),
+             Name.Builtin),
+            (_build_word_match(PERL6_BUILTINS, PERL6_IDENTIFIER_RANGE), Name.Builtin),
+            # copied from PerlLexer
+            (r'[$@%&][.^:?=!~]?' + PERL6_IDENTIFIER_RANGE + '+(?:<<.*?>>|<.*?>|«.*?»)*',
+             Name.Variable),
+            (r'\$[!/](?:<<.*?>>|<.*?>|«.*?»)*', Name.Variable.Global),
+            (r'::\?\w+', Name.Variable.Global),
+            (r'[$@%&]\*' + PERL6_IDENTIFIER_RANGE + '+(?:<<.*?>>|<.*?>|«.*?»)*',
+             Name.Variable.Global),
+            (r'\$(?:<.*?>)+', Name.Variable),
+            (r'(?:q|qq|Q)[a-zA-Z]?\s*(?P:[\w\s:]+)?\s*(?P(?P[^0-9a-zA-Z:\s])'
+             r'(?P=first_char)*)', brackets_callback(String)),
+            # copied from PerlLexer
+            (r'0_?[0-7]+(_[0-7]+)*', Number.Oct),
+            (r'0x[0-9A-Fa-f]+(_[0-9A-Fa-f]+)*', Number.Hex),
+            (r'0b[01]+(_[01]+)*', Number.Bin),
+            (r'(?i)(\d*(_\d*)*\.\d+(_\d*)*|\d+(_\d*)*\.\d+(_\d*)*)(e[+-]?\d+)?',
+             Number.Float),
+            (r'(?i)\d+(_\d*)*e[+-]?\d+(_\d*)*', Number.Float),
+            (r'\d+(_\d+)*', Number.Integer),
+            (r'(?<=~~)\s*/(?:\\\\|\\/|.)*?/', String.Regex),
+            (r'(?<=[=(,])\s*/(?:\\\\|\\/|.)*?/', String.Regex),
+            (r'm\w+(?=\()', Name),
+            (r'(?:m|ms|rx)\s*(?P:[\w\s:]+)?\s*(?P(?P[^\w:\s])'
+             r'(?P=first_char)*)', brackets_callback(String.Regex)),
+            (r'(?:s|ss|tr)\s*(?::[\w\s:]+)?\s*/(?:\\\\|\\/|.)*?/(?:\\\\|\\/|.)*?/',
+             String.Regex),
+            (r'<[^\s=].*?\S>', String),
+            (_build_word_match(PERL6_OPERATORS), Operator),
+            (r'\w' + PERL6_IDENTIFIER_RANGE + '*', Name),
+            (r"'(\\\\|\\[^\\]|[^'\\])*'", String),
+            (r'"(\\\\|\\[^\\]|[^"\\])*"', String),
+        ],
+        'root': [
+            include('common'),
+            (r'\{', opening_brace_callback),
+            (r'\}', closing_brace_callback),
+            (r'.+?', Text),
+        ],
+        'pre-token': [
+            include('common'),
+            (r'\{', Text, ('#pop', 'token')),
+            (r'.+?', Text),
+        ],
+        'token-sym-brackets': [
+            (r'(?P(?P[' + ''.join(PERL6_BRACKETS) + '])(?P=first_char)*)',
+             brackets_callback(Name), ('#pop', 'pre-token')),
+            default(('#pop', 'pre-token')),
+        ],
+        'token': [
+            (r'\}', Text, '#pop'),
+            (r'(?<=:)(?:my|our|state|constant|temp|let).*?;', using(this)),
+            # make sure that quotes in character classes aren't treated as strings
+            (r'<(?:[-!?+.]\s*)?\[.*?\]>', String.Regex),
+            # make sure that '#' characters in quotes aren't treated as comments
+            (r"(?my|our)\s+)?(?:module|class|role|enum|grammar)', line)
+            if class_decl:
+                if saw_perl_decl or class_decl.group('scope') is not None:
+                    return True
+                rating = 0.05
+                continue
+            break
+
+        if ':=' in text:
+            # Same logic as above for PerlLexer
+            rating /= 2
+
+        return rating
+
+    def __init__(self, **options):
+        super().__init__(**options)
+        self.encoding = options.get('encoding', 'utf-8')
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/phix.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/phix.py
new file mode 100644
index 00000000..f0b03775
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/phix.py
@@ -0,0 +1,363 @@
+"""
+    pygments.lexers.phix
+    ~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for Phix.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, words
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Whitespace
+
+__all__ = ['PhixLexer']
+
+
+class PhixLexer(RegexLexer):
+    """
+    Pygments Lexer for Phix files (.exw).
+    See http://phix.x10.mx
+    """
+
+    name = 'Phix'
+    url = 'http://phix.x10.mx'
+    aliases = ['phix']
+    filenames = ['*.exw']
+    mimetypes = ['text/x-phix']
+    version_added = '2.14'
+
+    flags = re.MULTILINE    # nb: **NOT** re.DOTALL! (totally spanners comment handling)
+
+    preproc = (
+        'ifdef', 'elsifdef', 'elsedef'
+    )
+    # Note these lists are auto-generated by pwa/p2js.exw, when pwa\src\p2js_keywords.e (etc)
+    #     change, though of course subsequent copy/commit/pull requests are all manual steps.
+    types = (
+        'string', 'nullable_string', 'atom_string', 'atom', 'bool', 'boolean',
+        'cdCanvan', 'cdCanvas', 'complex', 'CURLcode', 'dictionary', 'int',
+        'integer', 'Ihandle', 'Ihandles', 'Ihandln', 'mpfr', 'mpq', 'mpz',
+        'mpz_or_string', 'number', 'rid_string', 'seq', 'sequence', 'timedate',
+        'object'
+    )
+    keywords = (
+        'abstract', 'class', 'continue', 'export', 'extends', 'nullable',
+        'private', 'public', 'static', 'struct', 'trace',
+        'and', 'break', 'by', 'case', 'catch', 'const', 'constant', 'debug',
+        'default', 'do', 'else', 'elsif', 'end', 'enum', 'exit', 'fallthru',
+        'fallthrough', 'for', 'forward', 'function', 'global', 'if', 'in',
+        'include', 'js', 'javascript', 'javascript_semantics', 'let', 'not',
+        'or', 'procedure', 'profile', 'profile_time', 'return', 'safe_mode',
+        'switch', 'then', 'to', 'try', 'type', 'type_check', 'until', 'warning',
+        'while', 'with', 'without', 'xor'
+    )
+    routines = (
+        'abort', 'abs', 'adjust_timedate', 'and_bits', 'and_bitsu', 'apply',
+        'append', 'arccos', 'arcsin', 'arctan', 'assert', 'atan2',
+        'atom_to_float32', 'atom_to_float64', 'bankers_rounding', 'beep',
+        'begins', 'binary_search', 'bits_to_int', 'bk_color', 'bytes_to_int',
+        'call_func', 'call_proc', 'cdCanvasActivate', 'cdCanvasArc',
+        'cdCanvasBegin', 'cdCanvasBox', 'cdCanvasChord', 'cdCanvasCircle',
+        'cdCanvasClear', 'cdCanvasEnd', 'cdCanvasFlush', 'cdCanvasFont',
+        'cdCanvasGetImageRGB', 'cdCanvasGetSize', 'cdCanvasGetTextAlignment',
+        'cdCanvasGetTextSize', 'cdCanvasLine', 'cdCanvasMark',
+        'cdCanvasMarkSize', 'cdCanvasMultiLineVectorText', 'cdCanvasPixel',
+        'cdCanvasRect', 'cdCanvasRoundedBox', 'cdCanvasRoundedRect',
+        'cdCanvasSector', 'cdCanvasSetAttribute', 'cdCanvasSetBackground',
+        'cdCanvasSetFillMode', 'cdCanvasSetForeground',
+        'cdCanvasSetInteriorStyle', 'cdCanvasSetLineStyle',
+        'cdCanvasSetLineWidth', 'cdCanvasSetTextAlignment', 'cdCanvasText',
+        'cdCanvasSetTextOrientation', 'cdCanvasGetTextOrientation',
+        'cdCanvasVectorText', 'cdCanvasVectorTextDirection',
+        'cdCanvasVectorTextSize', 'cdCanvasVertex', 'cdCreateCanvas',
+        'cdDecodeAlpha', 'cdDecodeColor', 'cdDecodeColorAlpha', 'cdEncodeAlpha',
+        'cdEncodeColor', 'cdEncodeColorAlpha', 'cdKillCanvas', 'cdVersion',
+        'cdVersionDate', 'ceil', 'change_timezone', 'choose', 'clear_screen',
+        'columnize', 'command_line', 'compare', 'complex_abs', 'complex_add',
+        'complex_arg', 'complex_conjugate', 'complex_cos', 'complex_cosh',
+        'complex_div', 'complex_exp', 'complex_imag', 'complex_inv',
+        'complex_log', 'complex_mul', 'complex_neg', 'complex_new',
+        'complex_norm', 'complex_power', 'complex_rho', 'complex_real',
+        'complex_round', 'complex_sin', 'complex_sinh', 'complex_sprint',
+        'complex_sqrt', 'complex_sub', 'complex_theta', 'concat', 'cos',
+        'crash', 'custom_sort', 'date', 'day_of_week', 'day_of_year',
+        'days_in_month', 'decode_base64', 'decode_flags', 'deep_copy', 'deld',
+        'deserialize', 'destroy_dict', 'destroy_queue', 'destroy_stack',
+        'dict_name', 'dict_size', 'elapsed', 'elapsed_short', 'encode_base64',
+        'equal', 'even', 'exp', 'extract', 'factorial', 'factors',
+        'file_size_k', 'find', 'find_all', 'find_any', 'find_replace', 'filter',
+        'flatten', 'float32_to_atom', 'float64_to_atom', 'floor',
+        'format_timedate', 'free_console', 'from_polar', 'gcd', 'get_file_base',
+        'get_file_extension', 'get_file_name', 'get_file_name_and_path',
+        'get_file_path', 'get_file_path_and_name', 'get_maxprime', 'get_prime',
+        'get_primes', 'get_primes_le', 'get_proper_dir', 'get_proper_path',
+        'get_rand', 'get_routine_info', 'get_test_abort', 'get_test_logfile',
+        'get_test_pause', 'get_test_verbosity', 'get_tzid', 'getd', 'getdd',
+        'getd_all_keys', 'getd_by_index', 'getd_index', 'getd_partial_key',
+        'glAttachShader', 'glBindBuffer', 'glBindTexture', 'glBufferData',
+        'glCanvasSpecialText', 'glClear', 'glClearColor', 'glColor',
+        'glCompileShader', 'glCreateBuffer', 'glCreateProgram',
+        'glCreateShader', 'glCreateTexture', 'glDeleteProgram',
+        'glDeleteShader', 'glDrawArrays', 'glEnable',
+        'glEnableVertexAttribArray', 'glFloat32Array', 'glInt32Array',
+        'glFlush', 'glGetAttribLocation', 'glGetError', 'glGetProgramInfoLog',
+        'glGetProgramParameter', 'glGetShaderInfoLog', 'glGetShaderParameter',
+        'glGetUniformLocation', 'glLinkProgram', 'glLoadIdentity',
+        'glMatrixMode', 'glOrtho', 'glRotatef', 'glShadeModel',
+        'glShaderSource', 'glSimpleA7texcoords', 'glTexImage2Dc',
+        'glTexParameteri', 'glTranslate', 'glUniform1f', 'glUniform1i',
+        'glUniformMatrix4fv', 'glUseProgram', 'glVertex',
+        'glVertexAttribPointer', 'glViewport', 'head', 'hsv_to_rgb', 'iff',
+        'iif', 'include_file', 'incl0de_file', 'insert', 'instance',
+        'int_to_bits', 'int_to_bytes', 'is_dict', 'is_integer', 's_leap_year',
+        'is_prime', 'is_prime2', 'islower', 'isupper', 'Icallback',
+        'iup_isdouble', 'iup_isprint', 'iup_XkeyBase', 'IupAppend', 'IupAlarm',
+        'IupBackgroundBox', 'IupButton', 'IupCalendar', 'IupCanvas',
+        'IupClipboard', 'IupClose', 'IupCloseOnEscape', 'IupControlsOpen',
+        'IupDatePick', 'IupDestroy', 'IupDialog', 'IupDrawArc', 'IupDrawBegin',
+        'IupDrawEnd', 'IupDrawGetSize', 'IupDrawGetTextSize', 'IupDrawLine',
+        'IupDrawRectangle', 'IupDrawText', 'IupExpander', 'IupFill',
+        'IupFlatLabel', 'IupFlatList', 'IupFlatTree', 'IupFlush', 'IupFrame',
+        'IupGetAttribute', 'IupGetAttributeId', 'IupGetAttributePtr',
+        'IupGetBrother', 'IupGetChild', 'IupGetChildCount', 'IupGetClassName',
+        'IupGetDialog', 'IupGetDialogChild', 'IupGetDouble', 'IupGetFocus',
+        'IupGetGlobal', 'IupGetGlobalInt', 'IupGetGlobalIntInt', 'IupGetInt',
+        'IupGetInt2', 'IupGetIntId', 'IupGetIntInt', 'IupGetParent',
+        'IupGLCanvas', 'IupGLCanvasOpen', 'IupGLMakeCurrent', 'IupGraph',
+        'IupHbox', 'IupHide', 'IupImage', 'IupImageRGBA', 'IupItem',
+        'iupKeyCodeToName', 'IupLabel', 'IupLink', 'IupList', 'IupMap',
+        'IupMenu', 'IupMenuItem', 'IupMessage', 'IupMessageDlg', 'IupMultiBox',
+        'IupMultiLine', 'IupNextField', 'IupNormaliser', 'IupOpen',
+        'IupPlayInput', 'IupPopup', 'IupPreviousField', 'IupProgressBar',
+        'IupRadio', 'IupRecordInput', 'IupRedraw', 'IupRefresh',
+        'IupRefreshChildren', 'IupSeparator', 'IupSetAttribute',
+        'IupSetAttributes', 'IupSetAttributeHandle', 'IupSetAttributeId',
+        'IupSetAttributePtr', 'IupSetCallback', 'IupSetCallbacks',
+        'IupSetDouble', 'IupSetFocus', 'IupSetGlobal', 'IupSetGlobalInt',
+        'IupSetGlobalFunction', 'IupSetHandle', 'IupSetInt',
+        'IupSetStrAttribute', 'IupSetStrGlobal', 'IupShow', 'IupShowXY',
+        'IupSplit', 'IupStoreAttribute', 'IupSubmenu', 'IupTable',
+        'IupTableClearSelected', 'IupTableClick_cb', 'IupTableGetSelected',
+        'IupTableResize_cb', 'IupTableSetData', 'IupTabs', 'IupText',
+        'IupTimer', 'IupToggle', 'IupTreeAddNodes', 'IupTreeView', 'IupUpdate',
+        'IupValuator', 'IupVbox', 'join', 'join_by', 'join_path', 'k_perm',
+        'largest', 'lcm', 'length', 'log', 'log10', 'log2', 'lower',
+        'm4_crossProduct', 'm4_inverse', 'm4_lookAt', 'm4_multiply',
+        'm4_normalize', 'm4_perspective', 'm4_subtractVectors', 'm4_xRotate',
+        'm4_yRotate', 'machine_bits', 'machine_word', 'match', 'match_all',
+        'match_replace', 'max', 'maxsq', 'min', 'minsq', 'mod', 'mpfr_add',
+        'mpfr_ceil', 'mpfr_cmp', 'mpfr_cmp_si', 'mpfr_const_pi', 'mpfr_div',
+        'mpfr_div_si', 'mpfr_div_z', 'mpfr_floor', 'mpfr_free', 'mpfr_get_d',
+        'mpfr_get_default_precision', 'mpfr_get_default_rounding_mode',
+        'mpfr_get_fixed', 'mpfr_get_precision', 'mpfr_get_si', 'mpfr_init',
+        'mpfr_inits', 'mpfr_init_set', 'mpfr_init_set_q', 'mpfr_init_set_z',
+        'mpfr_mul', 'mpfr_mul_si', 'mpfr_pow_si', 'mpfr_set', 'mpfr_set_d',
+        'mpfr_set_default_precision', 'mpfr_set_default_rounding_mode',
+        'mpfr_set_precision', 'mpfr_set_q', 'mpfr_set_si', 'mpfr_set_str',
+        'mpfr_set_z', 'mpfr_si_div', 'mpfr_si_sub', 'mpfr_sqrt', 'mpfr_sub',
+        'mpfr_sub_si', 'mpq_abs', 'mpq_add', 'mpq_add_si', 'mpq_canonicalize',
+        'mpq_cmp', 'mpq_cmp_si', 'mpq_div', 'mpq_div_2exp', 'mpq_free',
+        'mpq_get_den', 'mpq_get_num', 'mpq_get_str', 'mpq_init', 'mpq_init_set',
+        'mpq_init_set_si', 'mpq_init_set_str', 'mpq_init_set_z', 'mpq_inits',
+        'mpq_inv', 'mpq_mul', 'mpq_neg', 'mpq_set', 'mpq_set_si', 'mpq_set_str',
+        'mpq_set_z', 'mpq_sub', 'mpz_abs', 'mpz_add', 'mpz_addmul',
+        'mpz_addmul_ui', 'mpz_addmul_si', 'mpz_add_si', 'mpz_add_ui', 'mpz_and',
+        'mpz_bin_uiui', 'mpz_cdiv_q', 'mpz_cmp', 'mpz_cmp_si', 'mpz_divexact',
+        'mpz_divexact_ui', 'mpz_divisible_p', 'mpz_divisible_ui_p', 'mpz_even',
+        'mpz_fac_ui', 'mpz_factorstring', 'mpz_fdiv_q', 'mpz_fdiv_q_2exp',
+        'mpz_fdiv_q_ui', 'mpz_fdiv_qr', 'mpz_fdiv_r', 'mpz_fdiv_ui',
+        'mpz_fib_ui', 'mpz_fib2_ui', 'mpz_fits_atom', 'mpz_fits_integer',
+        'mpz_free', 'mpz_gcd', 'mpz_gcd_ui', 'mpz_get_atom', 'mpz_get_integer',
+        'mpz_get_short_str', 'mpz_get_str', 'mpz_init', 'mpz_init_set',
+        'mpz_inits', 'mpz_invert', 'mpz_lcm', 'mpz_lcm_ui', 'mpz_max',
+        'mpz_min', 'mpz_mod', 'mpz_mod_ui', 'mpz_mul', 'mpz_mul_2exp',
+        'mpz_mul_d', 'mpz_mul_si', 'mpz_neg', 'mpz_nthroot', 'mpz_odd',
+        'mpz_pollard_rho', 'mpz_pow_ui', 'mpz_powm', 'mpz_powm_ui', 'mpz_prime',
+        'mpz_prime_factors', 'mpz_prime_mr', 'mpz_rand', 'mpz_rand_ui',
+        'mpz_re_compose', 'mpz_remove', 'mpz_scan0', 'mpz_scan1', 'mpz_set',
+        'mpz_set_d', 'mpz_set_si', 'mpz_set_str', 'mpz_set_v', 'mpz_sign',
+        'mpz_sizeinbase', 'mpz_sqrt', 'mpz_sub', 'mpz_sub_si', 'mpz_sub_ui',
+        'mpz_si_sub', 'mpz_tdiv_q_2exp', 'mpz_tdiv_r_2exp', 'mpz_tstbit',
+        'mpz_ui_pow_ui', 'mpz_xor', 'named_dict', 'new_dict', 'new_queue',
+        'new_stack', 'not_bits', 'not_bitsu', 'odd', 'or_all', 'or_allu',
+        'or_bits', 'or_bitsu', 'ord', 'ordinal', 'ordinant',
+        'override_timezone', 'pad', 'pad_head', 'pad_tail', 'parse_date_string',
+        'papply', 'peep', 'peepn', 'peep_dict', 'permute', 'permutes',
+        'platform', 'pop', 'popn', 'pop_dict', 'power', 'pp', 'ppEx', 'ppExf',
+        'ppf', 'ppOpt', 'pq_add', 'pq_destroy', 'pq_empty', 'pq_new', 'pq_peek',
+        'pq_pop', 'pq_pop_data', 'pq_size', 'prepend', 'prime_factors',
+        'printf', 'product', 'proper', 'push', 'pushn', 'putd', 'puts',
+        'queue_empty', 'queue_size', 'rand', 'rand_range', 'reinstate',
+        'remainder', 'remove', 'remove_all', 'repeat', 'repeatch', 'replace',
+        'requires', 'reverse', 'rfind', 'rgb', 'rmatch', 'rmdr', 'rnd', 'round',
+        'routine_id', 'scanf', 'serialize', 'series', 'set_rand',
+        'set_test_abort', 'set_test_logfile', 'set_test_module',
+        'set_test_pause', 'set_test_verbosity', 'set_timedate_formats',
+        'set_timezone', 'setd', 'setd_default', 'shorten', 'sha256',
+        'shift_bits', 'shuffle', 'sign', 'sin', 'smallest', 'sort',
+        'sort_columns', 'speak', 'splice', 'split', 'split_any', 'split_by',
+        'sprint', 'sprintf', 'sq_abs', 'sq_add', 'sq_and', 'sq_and_bits',
+        'sq_arccos', 'sq_arcsin', 'sq_arctan', 'sq_atom', 'sq_ceil', 'sq_cmp',
+        'sq_cos', 'sq_div', 'sq_even', 'sq_eq', 'sq_floor', 'sq_floor_div',
+        'sq_ge', 'sq_gt', 'sq_int', 'sq_le', 'sq_log', 'sq_log10', 'sq_log2',
+        'sq_lt', 'sq_max', 'sq_min', 'sq_mod', 'sq_mul', 'sq_ne', 'sq_not',
+        'sq_not_bits', 'sq_odd', 'sq_or', 'sq_or_bits', 'sq_power', 'sq_rand',
+        'sq_remainder', 'sq_rmdr', 'sq_rnd', 'sq_round', 'sq_seq', 'sq_sign',
+        'sq_sin', 'sq_sqrt', 'sq_str', 'sq_sub', 'sq_tan', 'sq_trunc',
+        'sq_uminus', 'sq_xor', 'sq_xor_bits', 'sqrt', 'square_free',
+        'stack_empty', 'stack_size', 'substitute', 'substitute_all', 'sum',
+        'tail', 'tan', 'test_equal', 'test_fail', 'test_false',
+        'test_not_equal', 'test_pass', 'test_summary', 'test_true',
+        'text_color', 'throw', 'time', 'timedate_diff', 'timedelta',
+        'to_integer', 'to_number', 'to_rgb', 'to_string', 'traverse_dict',
+        'traverse_dict_partial_key', 'trim', 'trim_head', 'trim_tail', 'trunc',
+        'tagset', 'tagstart', 'typeof', 'unique', 'unix_dict', 'upper',
+        'utf8_to_utf32', 'utf32_to_utf8', 'version', 'vlookup', 'vslice',
+        'wglGetProcAddress', 'wildcard_file', 'wildcard_match', 'with_rho',
+        'with_theta', 'xml_new_doc', 'xml_new_element', 'xml_set_attribute',
+        'xml_sprint', 'xor_bits', 'xor_bitsu',
+        'accept', 'allocate', 'allocate_string', 'allow_break', 'ARM',
+        'atom_to_float80', 'c_func', 'c_proc', 'call_back', 'chdir',
+        'check_break', 'clearDib', 'close', 'closesocket', 'console',
+        'copy_file', 'create', 'create_directory', 'create_thread',
+        'curl_easy_cleanup', 'curl_easy_get_file', 'curl_easy_init',
+        'curl_easy_perform', 'curl_easy_perform_ex', 'curl_easy_setopt',
+        'curl_easy_strerror', 'curl_global_cleanup', 'curl_global_init',
+        'curl_slist_append', 'curl_slist_free_all', 'current_dir', 'cursor',
+        'define_c_func', 'define_c_proc', 'delete', 'delete_cs', 'delete_file',
+        'dir', 'DLL', 'drawDib', 'drawShadedPolygonToDib', 'ELF32', 'ELF64',
+        'enter_cs', 'eval', 'exit_thread', 'free', 'file_exists', 'final',
+        'float80_to_atom', 'format', 'get_bytes', 'get_file_date',
+        'get_file_size', 'get_file_type', 'get_interpreter', 'get_key',
+        'get_socket_error', 'get_text', 'get_thread_exitcode', 'get_thread_id',
+        'getc', 'getenv', 'gets', 'getsockaddr', 'glBegin', 'glCallList',
+        'glFrustum', 'glGenLists', 'glGetString', 'glLight', 'glMaterial',
+        'glNewList', 'glNormal', 'glPopMatrix', 'glPushMatrix', 'glRotate',
+        'glEnd', 'glEndList', 'glTexImage2D', 'goto', 'GUI', 'icons', 'ilASM',
+        'include_files', 'include_paths', 'init_cs', 'ip_to_string',
+        'IupConfig', 'IupConfigDialogClosed', 'IupConfigDialogShow',
+        'IupConfigGetVariableInt', 'IupConfigLoad', 'IupConfigSave',
+        'IupConfigSetVariableInt', 'IupExitLoop', 'IupFileDlg', 'IupFileList',
+        'IupGLSwapBuffers', 'IupHelp', 'IupLoopStep', 'IupMainLoop',
+        'IupNormalizer', 'IupPlot', 'IupPlotAdd', 'IupPlotBegin', 'IupPlotEnd',
+        'IupPlotInsert', 'IupSaveImage', 'IupTreeGetUserId', 'IupUser',
+        'IupVersion', 'IupVersionDate', 'IupVersionNumber', 'IupVersionShow',
+        'killDib', 'leave_cs', 'listen', 'manifest', 'mem_copy', 'mem_set',
+        'mpfr_gamma', 'mpfr_printf', 'mpfr_sprintf', 'mpz_export', 'mpz_import',
+        'namespace', 'new', 'newDib', 'open', 'open_dll', 'PE32', 'PE64',
+        'peek', 'peek_string', 'peek1s', 'peek1u', 'peek2s', 'peek2u', 'peek4s',
+        'peek4u', 'peek8s', 'peek8u', 'peekNS', 'peekns', 'peeknu', 'poke',
+        'poke2', 'poke4', 'poke8', 'pokeN', 'poke_string', 'poke_wstring',
+        'position', 'progress', 'prompt_number', 'prompt_string', 'read_file',
+        'read_lines', 'recv', 'resume_thread', 'seek', 'select', 'send',
+        'setHandler', 'shutdown', 'sleep', 'SO', 'sockaddr_in', 'socket',
+        'split_path', 'suspend_thread', 'system', 'system_exec', 'system_open',
+        'system_wait', 'task_clock_start', 'task_clock_stop', 'task_create',
+        'task_delay', 'task_list', 'task_schedule', 'task_self', 'task_status',
+        'task_suspend', 'task_yield', 'thread_safe_string', 'try_cs',
+        'utf8_to_utf16', 'utf16_to_utf8', 'utf16_to_utf32', 'utf32_to_utf16',
+        'video_config', 'WSACleanup', 'wait_thread', 'walk_dir', 'where',
+        'write_lines', 'wait_key'
+    )
+    constants = (
+        'ANY_QUEUE', 'ASCENDING', 'BLACK', 'BLOCK_CURSOR', 'BLUE',
+        'BRIGHT_CYAN', 'BRIGHT_BLUE', 'BRIGHT_GREEN', 'BRIGHT_MAGENTA',
+        'BRIGHT_RED', 'BRIGHT_WHITE', 'BROWN', 'C_DWORD', 'C_INT', 'C_POINTER',
+        'C_USHORT', 'C_WORD', 'CD_AMBER', 'CD_BLACK', 'CD_BLUE', 'CD_BOLD',
+        'CD_BOLD_ITALIC', 'CD_BOX', 'CD_CENTER', 'CD_CIRCLE', 'CD_CLOSED_LINES',
+        'CD_CONTINUOUS', 'CD_CUSTOM', 'CD_CYAN', 'CD_DARK_BLUE', 'CD_DARK_CYAN',
+        'CD_DARK_GRAY', 'CD_DARK_GREY', 'CD_DARK_GREEN', 'CD_DARK_MAGENTA',
+        'CD_DARK_RED', 'CD_DARK_YELLOW', 'CD_DASH_DOT', 'CD_DASH_DOT_DOT',
+        'CD_DASHED', 'CD_DBUFFER', 'CD_DEG2RAD', 'CD_DIAMOND', 'CD_DOTTED',
+        'CD_EAST', 'CD_EVENODD', 'CD_FILL', 'CD_GL', 'CD_GRAY', 'CD_GREY',
+        'CD_GREEN', 'CD_HATCH', 'CD_HOLLOW', 'CD_HOLLOW_BOX',
+        'CD_HOLLOW_CIRCLE', 'CD_HOLLOW_DIAMOND', 'CD_INDIGO', 'CD_ITALIC',
+        'CD_IUP', 'CD_IUPDBUFFER', 'CD_LIGHT_BLUE', 'CD_LIGHT_GRAY',
+        'CD_LIGHT_GREY', 'CD_LIGHT_GREEN', 'CD_LIGHT_PARCHMENT', 'CD_MAGENTA',
+        'CD_NAVY', 'CD_NORTH', 'CD_NORTH_EAST', 'CD_NORTH_WEST', 'CD_OLIVE',
+        'CD_OPEN_LINES', 'CD_ORANGE', 'CD_PARCHMENT', 'CD_PATTERN',
+        'CD_PRINTER', 'CD_PURPLE', 'CD_PLAIN', 'CD_PLUS', 'CD_QUERY',
+        'CD_RAD2DEG', 'CD_RED', 'CD_SILVER', 'CD_SOLID', 'CD_SOUTH_EAST',
+        'CD_SOUTH_WEST', 'CD_STAR', 'CD_STIPPLE', 'CD_STRIKEOUT',
+        'CD_UNDERLINE', 'CD_WEST', 'CD_WHITE', 'CD_WINDING', 'CD_VIOLET',
+        'CD_X', 'CD_YELLOW', 'CURLE_OK', 'CURLOPT_MAIL_FROM',
+        'CURLOPT_MAIL_RCPT', 'CURLOPT_PASSWORD', 'CURLOPT_READDATA',
+        'CURLOPT_READFUNCTION', 'CURLOPT_SSL_VERIFYPEER',
+        'CURLOPT_SSL_VERIFYHOST', 'CURLOPT_UPLOAD', 'CURLOPT_URL',
+        'CURLOPT_USE_SSL', 'CURLOPT_USERNAME', 'CURLOPT_VERBOSE',
+        'CURLOPT_WRITEFUNCTION', 'CURLUSESSL_ALL', 'CYAN', 'D_NAME',
+        'D_ATTRIBUTES', 'D_SIZE', 'D_YEAR', 'D_MONTH', 'D_DAY', 'D_HOUR',
+        'D_MINUTE', 'D_SECOND', 'D_CREATION', 'D_LASTACCESS', 'D_MODIFICATION',
+        'DT_YEAR', 'DT_MONTH', 'DT_DAY', 'DT_HOUR', 'DT_MINUTE', 'DT_SECOND',
+        'DT_DOW', 'DT_MSEC', 'DT_DOY', 'DT_GMT', 'EULER', 'E_CODE', 'E_ADDR',
+        'E_LINE', 'E_RTN', 'E_NAME', 'E_FILE', 'E_PATH', 'E_USER', 'false',
+        'False', 'FALSE', 'FIFO_QUEUE', 'FILETYPE_DIRECTORY', 'FILETYPE_FILE',
+        'GET_EOF', 'GET_FAIL', 'GET_IGNORE', 'GET_SUCCESS',
+        'GL_AMBIENT_AND_DIFFUSE', 'GL_ARRAY_BUFFER', 'GL_CLAMP',
+        'GL_CLAMP_TO_BORDER', 'GL_CLAMP_TO_EDGE', 'GL_COLOR_BUFFER_BIT',
+        'GL_COMPILE', 'GL_COMPILE_STATUS', 'GL_CULL_FACE',
+        'GL_DEPTH_BUFFER_BIT', 'GL_DEPTH_TEST', 'GL_EXTENSIONS', 'GL_FLAT',
+        'GL_FLOAT', 'GL_FRAGMENT_SHADER', 'GL_FRONT', 'GL_LIGHT0',
+        'GL_LIGHTING', 'GL_LINEAR', 'GL_LINK_STATUS', 'GL_MODELVIEW',
+        'GL_NEAREST', 'GL_NO_ERROR', 'GL_NORMALIZE', 'GL_POSITION',
+        'GL_PROJECTION', 'GL_QUAD_STRIP', 'GL_QUADS', 'GL_RENDERER',
+        'GL_REPEAT', 'GL_RGB', 'GL_RGBA', 'GL_SMOOTH', 'GL_STATIC_DRAW',
+        'GL_TEXTURE_2D', 'GL_TEXTURE_MAG_FILTER', 'GL_TEXTURE_MIN_FILTER',
+        'GL_TEXTURE_WRAP_S', 'GL_TEXTURE_WRAP_T', 'GL_TRIANGLES',
+        'GL_UNSIGNED_BYTE', 'GL_VENDOR', 'GL_VERSION', 'GL_VERTEX_SHADER',
+        'GRAY', 'GREEN', 'GT_LF_STRIPPED', 'GT_WHOLE_FILE', 'INVLN10',
+        'IUP_CLOSE', 'IUP_CONTINUE', 'IUP_DEFAULT', 'IUP_BLACK', 'IUP_BLUE',
+        'IUP_BUTTON1', 'IUP_BUTTON3', 'IUP_CENTER', 'IUP_CYAN', 'IUP_DARK_BLUE',
+        'IUP_DARK_CYAN', 'IUP_DARK_GRAY', 'IUP_DARK_GREY', 'IUP_DARK_GREEN',
+        'IUP_DARK_MAGENTA', 'IUP_DARK_RED', 'IUP_GRAY', 'IUP_GREY', 'IUP_GREEN',
+        'IUP_IGNORE', 'IUP_INDIGO', 'IUP_MAGENTA', 'IUP_MASK_INT',
+        'IUP_MASK_UINT', 'IUP_MOUSEPOS', 'IUP_NAVY', 'IUP_OLIVE', 'IUP_RECTEXT',
+        'IUP_RED', 'IUP_LIGHT_BLUE', 'IUP_LIGHT_GRAY', 'IUP_LIGHT_GREY',
+        'IUP_LIGHT_GREEN', 'IUP_ORANGE', 'IUP_PARCHMENT', 'IUP_PURPLE',
+        'IUP_SILVER', 'IUP_TEAL', 'IUP_VIOLET', 'IUP_WHITE', 'IUP_YELLOW',
+        'K_BS', 'K_cA', 'K_cC', 'K_cD', 'K_cF5', 'K_cK', 'K_cM', 'K_cN', 'K_cO',
+        'K_cP', 'K_cR', 'K_cS', 'K_cT', 'K_cW', 'K_CR', 'K_DEL', 'K_DOWN',
+        'K_END', 'K_ESC', 'K_F1', 'K_F2', 'K_F3', 'K_F4', 'K_F5', 'K_F6',
+        'K_F7', 'K_F8', 'K_F9', 'K_F10', 'K_F11', 'K_F12', 'K_HOME', 'K_INS',
+        'K_LEFT', 'K_MIDDLE', 'K_PGDN', 'K_PGUP', 'K_RIGHT', 'K_SP', 'K_TAB',
+        'K_UP', 'K_h', 'K_i', 'K_j', 'K_p', 'K_r', 'K_s', 'JS', 'LIFO_QUEUE',
+        'LINUX', 'MAX_HEAP', 'MAGENTA', 'MIN_HEAP', 'Nan', 'NO_CURSOR', 'null',
+        'NULL', 'PI', 'pp_Ascii', 'pp_Brkt', 'pp_Date', 'pp_File', 'pp_FltFmt',
+        'pp_Indent', 'pp_IntCh', 'pp_IntFmt', 'pp_Maxlen', 'pp_Nest',
+        'pp_Pause', 'pp_Q22', 'pp_StrFmt', 'RED', 'SEEK_OK', 'SLASH',
+        'TEST_ABORT', 'TEST_CRASH', 'TEST_PAUSE', 'TEST_PAUSE_FAIL',
+        'TEST_QUIET', 'TEST_SHOW_ALL', 'TEST_SHOW_FAILED', 'TEST_SUMMARY',
+        'true', 'True', 'TRUE', 'VC_SCRNLINES', 'WHITE', 'WINDOWS', 'YELLOW'
+    )
+
+    tokens = {
+        'root': [
+            (r"\s+", Whitespace),
+            (r'/\*|--/\*|#\[', Comment.Multiline, 'comment'),
+            (r'(?://|--|#!).*$', Comment.Single),
+#Alt:
+#           (r'//.*$|--.*$|#!.*$', Comment.Single),
+            (r'"([^"\\]|\\.)*"', String.Other),
+            (r'\'[^\']*\'', String.Other),
+            (r'`[^`]*`', String.Other),
+
+            (words(types, prefix=r'\b', suffix=r'\b'), Name.Function),
+            (words(routines, prefix=r'\b', suffix=r'\b'), Name.Function),
+            (words(preproc, prefix=r'\b', suffix=r'\b'), Keyword.Declaration),
+            (words(keywords, prefix=r'\b', suffix=r'\b'), Keyword.Declaration),
+            (words(constants, prefix=r'\b', suffix=r'\b'), Name.Constant),
+            # Aside: Phix only supports/uses the ascii/non-unicode tilde
+            (r'!=|==|<<|>>|:=|[-~+/*%=<>&^|\.(){},?:\[\]$\\;#]', Operator),
+            (r'[\w-]+', Text)
+        ],
+        'comment': [
+            (r'[^*/#]+', Comment.Multiline),
+            (r'/\*|#\[', Comment.Multiline, '#push'),
+            (r'\*/|#\]', Comment.Multiline, '#pop'),
+            (r'[*/#]', Comment.Multiline)
+        ]
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/php.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/php.py
new file mode 100644
index 00000000..82d4aeb3
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/php.py
@@ -0,0 +1,334 @@
+"""
+    pygments.lexers.php
+    ~~~~~~~~~~~~~~~~~~~
+
+    Lexers for PHP and related languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import Lexer, RegexLexer, include, bygroups, default, \
+    using, this, words, do_insertions, line_re
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation, Other, Generic
+from pygments.util import get_bool_opt, get_list_opt, shebang_matches
+
+__all__ = ['ZephirLexer', 'PsyshConsoleLexer', 'PhpLexer']
+
+
+class ZephirLexer(RegexLexer):
+    """
+    For Zephir language source code.
+
+    Zephir is a compiled high level language aimed
+    to the creation of C-extensions for PHP.
+    """
+
+    name = 'Zephir'
+    url = 'http://zephir-lang.com/'
+    aliases = ['zephir']
+    filenames = ['*.zep']
+    version_added = '2.0'
+
+    zephir_keywords = ['fetch', 'echo', 'isset', 'empty']
+    zephir_type = ['bit', 'bits', 'string']
+
+    flags = re.DOTALL | re.MULTILINE
+
+    tokens = {
+        'commentsandwhitespace': [
+            (r'\s+', Text),
+            (r'//.*?\n', Comment.Single),
+            (r'/\*.*?\*/', Comment.Multiline)
+        ],
+        'slashstartsregex': [
+            include('commentsandwhitespace'),
+            (r'/(\\.|[^[/\\\n]|\[(\\.|[^\]\\\n])*])+/'
+             r'([gim]+\b|\B)', String.Regex, '#pop'),
+            (r'/', Operator, '#pop'),
+            default('#pop')
+        ],
+        'badregex': [
+            (r'\n', Text, '#pop')
+        ],
+        'root': [
+            (r'^(?=\s|/)', Text, 'slashstartsregex'),
+            include('commentsandwhitespace'),
+            (r'\+\+|--|~|&&|\?|:|\|\||\\(?=\n)|'
+             r'(<<|>>>?|==?|!=?|->|[-<>+*%&|^/])=?', Operator, 'slashstartsregex'),
+            (r'[{(\[;,]', Punctuation, 'slashstartsregex'),
+            (r'[})\].]', Punctuation),
+            (r'(for|in|while|do|break|return|continue|switch|case|default|if|else|loop|'
+             r'require|inline|throw|try|catch|finally|new|delete|typeof|instanceof|void|'
+             r'namespace|use|extends|this|fetch|isset|unset|echo|fetch|likely|unlikely|'
+             r'empty)\b', Keyword, 'slashstartsregex'),
+            (r'(var|let|with|function)\b', Keyword.Declaration, 'slashstartsregex'),
+            (r'(abstract|boolean|bool|char|class|const|double|enum|export|extends|final|'
+             r'native|goto|implements|import|int|string|interface|long|ulong|char|uchar|'
+             r'float|unsigned|private|protected|public|short|static|self|throws|reverse|'
+             r'transient|volatile|readonly)\b', Keyword.Reserved),
+            (r'(true|false|null|undefined)\b', Keyword.Constant),
+            (r'(Array|Boolean|Date|_REQUEST|_COOKIE|_SESSION|'
+             r'_GET|_POST|_SERVER|this|stdClass|range|count|iterator|'
+             r'window)\b', Name.Builtin),
+            (r'[$a-zA-Z_][\w\\]*', Name.Other),
+            (r'[0-9][0-9]*\.[0-9]+([eE][0-9]+)?[fd]?', Number.Float),
+            (r'0x[0-9a-fA-F]+', Number.Hex),
+            (r'[0-9]+', Number.Integer),
+            (r'"(\\\\|\\[^\\]|[^"\\])*"', String.Double),
+            (r"'(\\\\|\\[^\\]|[^'\\])*'", String.Single),
+        ]
+    }
+
+
+class PsyshConsoleLexer(Lexer):
+    """
+    For PsySH console output, such as:
+
+    .. sourcecode:: psysh
+
+        >>> $greeting = function($name): string {
+        ...     return "Hello, {$name}";
+        ... };
+        => Closure($name): string {#2371 …3}
+        >>> $greeting('World')
+        => "Hello, World"
+    """
+    name = 'PsySH console session for PHP'
+    url = 'https://psysh.org/'
+    aliases = ['psysh']
+    version_added = '2.7'
+
+    def __init__(self, **options):
+        options['startinline'] = True
+        Lexer.__init__(self, **options)
+
+    def get_tokens_unprocessed(self, text):
+        phplexer = PhpLexer(**self.options)
+        curcode = ''
+        insertions = []
+        for match in line_re.finditer(text):
+            line = match.group()
+            if line.startswith('>>> ') or line.startswith('... '):
+                insertions.append((len(curcode),
+                                   [(0, Generic.Prompt, line[:4])]))
+                curcode += line[4:]
+            elif line.rstrip() == '...':
+                insertions.append((len(curcode),
+                                   [(0, Generic.Prompt, '...')]))
+                curcode += line[3:]
+            else:
+                if curcode:
+                    yield from do_insertions(
+                        insertions, phplexer.get_tokens_unprocessed(curcode))
+                    curcode = ''
+                    insertions = []
+                yield match.start(), Generic.Output, line
+        if curcode:
+            yield from do_insertions(insertions,
+                                     phplexer.get_tokens_unprocessed(curcode))
+
+
+class PhpLexer(RegexLexer):
+    """
+    For PHP source code.
+    For PHP embedded in HTML, use the `HtmlPhpLexer`.
+
+    Additional options accepted:
+
+    `startinline`
+        If given and ``True`` the lexer starts highlighting with
+        php code (i.e.: no starting ``>> from pygments.lexers._php_builtins import MODULES
+            >>> MODULES.keys()
+            ['PHP Options/Info', 'Zip', 'dba', ...]
+
+        In fact the names of those modules match the module names from
+        the php documentation.
+    """
+
+    name = 'PHP'
+    url = 'https://www.php.net/'
+    aliases = ['php', 'php3', 'php4', 'php5']
+    filenames = ['*.php', '*.php[345]', '*.inc']
+    mimetypes = ['text/x-php']
+    version_added = ''
+
+    # Note that a backslash is included, PHP uses a backslash as a namespace
+    # separator.
+    _ident_inner = r'(?:[\\_a-z]|[^\x00-\x7f])(?:[\\\w]|[^\x00-\x7f])*'
+    # But not inside strings.
+    _ident_nons = r'(?:[_a-z]|[^\x00-\x7f])(?:\w|[^\x00-\x7f])*'
+
+    flags = re.IGNORECASE | re.DOTALL | re.MULTILINE
+    tokens = {
+        'root': [
+            (r'<\?(php)?', Comment.Preproc, 'php'),
+            (r'[^<]+', Other),
+            (r'<', Other)
+        ],
+        'php': [
+            (r'\?>', Comment.Preproc, '#pop'),
+            (r'(<<<)([\'"]?)(' + _ident_nons + r')(\2\n.*?\n\s*)(\3)(;?)(\n)',
+             bygroups(String, String, String.Delimiter, String, String.Delimiter,
+                      Punctuation, Text)),
+            (r'\s+', Text),
+            (r'#\[', Punctuation, 'attribute'),
+            (r'#.*?\n', Comment.Single),
+            (r'//.*?\n', Comment.Single),
+            # put the empty comment here, it is otherwise seen as
+            # the start of a docstring
+            (r'/\*\*/', Comment.Multiline),
+            (r'/\*\*.*?\*/', String.Doc),
+            (r'/\*.*?\*/', Comment.Multiline),
+            (r'(->|::)(\s*)(' + _ident_nons + ')',
+             bygroups(Operator, Text, Name.Attribute)),
+            (r'[~!%^&*+=|:.<>/@-]+', Operator),
+            (r'\?', Operator),  # don't add to the charclass above!
+            (r'[\[\]{}();,]+', Punctuation),
+            (r'(new)(\s+)(class)\b', bygroups(Keyword, Text, Keyword)),
+            (r'(class)(\s+)', bygroups(Keyword, Text), 'classname'),
+            (r'(function)(\s*)(?=\()', bygroups(Keyword, Text)),
+            (r'(function)(\s+)(&?)(\s*)',
+             bygroups(Keyword, Text, Operator, Text), 'functionname'),
+            (r'(const)(\s+)(' + _ident_inner + ')',
+             bygroups(Keyword, Text, Name.Constant)),
+            (r'(and|E_PARSE|old_function|E_ERROR|or|as|E_WARNING|parent|'
+             r'eval|PHP_OS|break|exit|case|extends|PHP_VERSION|cfunction|'
+             r'FALSE|print|for|require|continue|foreach|require_once|'
+             r'declare|return|default|static|do|switch|die|stdClass|'
+             r'echo|else|TRUE|elseif|var|empty|if|xor|enddeclare|include|'
+             r'virtual|endfor|include_once|while|endforeach|global|'
+             r'endif|list|endswitch|new|endwhile|not|'
+             r'array|E_ALL|NULL|final|php_user_filter|interface|'
+             r'implements|public|private|protected|abstract|clone|try|'
+             r'catch|throw|this|use|namespace|trait|yield|'
+             r'finally|match)\b', Keyword),
+            (r'(true|false|null)\b', Keyword.Constant),
+            include('magicconstants'),
+            (r'\$\{', Name.Variable, 'variablevariable'),
+            (r'\$+' + _ident_inner, Name.Variable),
+            (_ident_inner, Name.Other),
+            (r'(\d+\.\d*|\d*\.\d+)(e[+-]?[0-9]+)?', Number.Float),
+            (r'\d+e[+-]?[0-9]+', Number.Float),
+            (r'0[0-7]+', Number.Oct),
+            (r'0x[a-f0-9]+', Number.Hex),
+            (r'\d+', Number.Integer),
+            (r'0b[01]+', Number.Bin),
+            (r"'([^'\\]*(?:\\.[^'\\]*)*)'", String.Single),
+            (r'`([^`\\]*(?:\\.[^`\\]*)*)`', String.Backtick),
+            (r'"', String.Double, 'string'),
+        ],
+        'variablevariable': [
+            (r'\}', Name.Variable, '#pop'),
+            include('php')
+        ],
+        'magicfuncs': [
+            # source: http://php.net/manual/en/language.oop5.magic.php
+            (words((
+                '__construct', '__destruct', '__call', '__callStatic', '__get', '__set',
+                '__isset', '__unset', '__sleep', '__wakeup', '__toString', '__invoke',
+                '__set_state', '__clone', '__debugInfo',), suffix=r'\b'),
+             Name.Function.Magic),
+        ],
+        'magicconstants': [
+            # source: http://php.net/manual/en/language.constants.predefined.php
+            (words((
+                '__LINE__', '__FILE__', '__DIR__', '__FUNCTION__', '__CLASS__',
+                '__TRAIT__', '__METHOD__', '__NAMESPACE__',),
+                suffix=r'\b'),
+             Name.Constant),
+        ],
+        'classname': [
+            (_ident_inner, Name.Class, '#pop')
+        ],
+        'functionname': [
+            include('magicfuncs'),
+            (_ident_inner, Name.Function, '#pop'),
+            default('#pop')
+        ],
+        'string': [
+            (r'"', String.Double, '#pop'),
+            (r'[^{$"\\]+', String.Double),
+            (r'\\([nrt"$\\]|[0-7]{1,3}|x[0-9a-f]{1,2})', String.Escape),
+            (r'\$' + _ident_nons + r'(\[\S+?\]|->' + _ident_nons + ')?',
+             String.Interpol),
+            (r'(\{\$\{)(.*?)(\}\})',
+             bygroups(String.Interpol, using(this, _startinline=True),
+                      String.Interpol)),
+            (r'(\{)(\$.*?)(\})',
+             bygroups(String.Interpol, using(this, _startinline=True),
+                      String.Interpol)),
+            (r'(\$\{)(\S+)(\})',
+             bygroups(String.Interpol, Name.Variable, String.Interpol)),
+            (r'[${\\]', String.Double)
+        ],
+        'attribute': [
+            (r'\]', Punctuation, '#pop'),
+            (r'\(', Punctuation, 'attributeparams'),
+            (_ident_inner, Name.Decorator),
+            include('php')
+        ],
+        'attributeparams': [
+            (r'\)', Punctuation, '#pop'),
+            include('php')
+        ],
+    }
+
+    def __init__(self, **options):
+        self.funcnamehighlighting = get_bool_opt(
+            options, 'funcnamehighlighting', True)
+        self.disabledmodules = get_list_opt(
+            options, 'disabledmodules', ['unknown'])
+        self.startinline = get_bool_opt(options, 'startinline', False)
+
+        # private option argument for the lexer itself
+        if '_startinline' in options:
+            self.startinline = options.pop('_startinline')
+
+        # collect activated functions in a set
+        self._functions = set()
+        if self.funcnamehighlighting:
+            from pygments.lexers._php_builtins import MODULES
+            for key, value in MODULES.items():
+                if key not in self.disabledmodules:
+                    self._functions.update(value)
+        RegexLexer.__init__(self, **options)
+
+    def get_tokens_unprocessed(self, text):
+        stack = ['root']
+        if self.startinline:
+            stack.append('php')
+        for index, token, value in \
+                RegexLexer.get_tokens_unprocessed(self, text, stack):
+            if token is Name.Other:
+                if value in self._functions:
+                    yield index, Name.Builtin, value
+                    continue
+            yield index, token, value
+
+    def analyse_text(text):
+        if shebang_matches(text, r'php'):
+            return True
+        rv = 0.0
+        if re.search(r'<\?(?!xml)', text):
+            rv += 0.3
+        return rv
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/pointless.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/pointless.py
new file mode 100644
index 00000000..adedb757
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/pointless.py
@@ -0,0 +1,70 @@
+"""
+    pygments.lexers.pointless
+    ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for Pointless.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, words
+from pygments.token import Comment, Error, Keyword, Name, Number, Operator, \
+    Punctuation, String, Text
+
+__all__ = ['PointlessLexer']
+
+
+class PointlessLexer(RegexLexer):
+    """
+    For Pointless source code.
+    """
+
+    name = 'Pointless'
+    url = 'https://ptls.dev'
+    aliases = ['pointless']
+    filenames = ['*.ptls']
+    version_added = '2.7'
+
+    ops = words([
+        "+", "-", "*", "/", "**", "%", "+=", "-=", "*=",
+        "/=", "**=", "%=", "|>", "=", "==", "!=", "<", ">",
+        "<=", ">=", "=>", "$", "++",
+    ])
+
+    keywords = words([
+        "if", "then", "else", "where", "with", "cond",
+        "case", "and", "or", "not", "in", "as", "for",
+        "requires", "throw", "try", "catch", "when",
+        "yield", "upval",
+    ], suffix=r'\b')
+
+    tokens = {
+        'root': [
+            (r'[ \n\r]+', Text),
+            (r'--.*$', Comment.Single),
+            (r'"""', String, 'multiString'),
+            (r'"', String, 'string'),
+            (r'[\[\](){}:;,.]', Punctuation),
+            (ops, Operator),
+            (keywords, Keyword),
+            (r'\d+|\d*\.\d+', Number),
+            (r'(true|false)\b', Name.Builtin),
+            (r'[A-Z][a-zA-Z0-9]*\b', String.Symbol),
+            (r'output\b', Name.Variable.Magic),
+            (r'(export|import)\b', Keyword.Namespace),
+            (r'[a-z][a-zA-Z0-9]*\b', Name.Variable)
+        ],
+        'multiString': [
+            (r'\\.', String.Escape),
+            (r'"""', String, '#pop'),
+            (r'"', String),
+            (r'[^\\"]+', String),
+        ],
+        'string': [
+            (r'\\.', String.Escape),
+            (r'"', String, '#pop'),
+            (r'\n', Error),
+            (r'[^\\"]+', String),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/pony.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/pony.py
new file mode 100644
index 00000000..055423a4
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/pony.py
@@ -0,0 +1,93 @@
+"""
+    pygments.lexers.pony
+    ~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for Pony and related languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, bygroups, words
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation
+
+__all__ = ['PonyLexer']
+
+
+class PonyLexer(RegexLexer):
+    """
+    For Pony source code.
+    """
+
+    name = 'Pony'
+    aliases = ['pony']
+    filenames = ['*.pony']
+    url = 'https://www.ponylang.io'
+    version_added = '2.4'
+
+    _caps = r'(iso|trn|ref|val|box|tag)'
+
+    tokens = {
+        'root': [
+            (r'\n', Text),
+            (r'[^\S\n]+', Text),
+            (r'//.*\n', Comment.Single),
+            (r'/\*', Comment.Multiline, 'nested_comment'),
+            (r'"""(?:.|\n)*?"""', String.Doc),
+            (r'"', String, 'string'),
+            (r'\'.*\'', String.Char),
+            (r'=>|[]{}:().~;,|&!^?[]', Punctuation),
+            (words((
+                'addressof', 'and', 'as', 'consume', 'digestof', 'is', 'isnt',
+                'not', 'or'),
+                suffix=r'\b'),
+             Operator.Word),
+            (r'!=|==|<<|>>|[-+/*%=<>]', Operator),
+            (words((
+                'box', 'break', 'compile_error', 'compile_intrinsic',
+                'continue', 'do', 'else', 'elseif', 'embed', 'end', 'error',
+                'for', 'if', 'ifdef', 'in', 'iso', 'lambda', 'let', 'match',
+                'object', 'recover', 'ref', 'repeat', 'return', 'tag', 'then',
+                'this', 'trn', 'try', 'until', 'use', 'var', 'val', 'where',
+                'while', 'with', '#any', '#read', '#send', '#share'),
+                suffix=r'\b'),
+             Keyword),
+            (r'(actor|class|struct|primitive|interface|trait|type)((?:\s)+)',
+             bygroups(Keyword, Text), 'typename'),
+            (r'(new|fun|be)((?:\s)+)', bygroups(Keyword, Text), 'methodname'),
+            (words((
+                'I8', 'U8', 'I16', 'U16', 'I32', 'U32', 'I64', 'U64', 'I128',
+                'U128', 'ILong', 'ULong', 'ISize', 'USize', 'F32', 'F64',
+                'Bool', 'Pointer', 'None', 'Any', 'Array', 'String',
+                'Iterator'),
+                suffix=r'\b'),
+             Name.Builtin.Type),
+            (r'_?[A-Z]\w*', Name.Type),
+            (r'(\d+\.\d*|\.\d+|\d+)[eE][+-]?\d+', Number.Float),
+            (r'0x[0-9a-fA-F]+', Number.Hex),
+            (r'\d+', Number.Integer),
+            (r'(true|false)\b', Name.Builtin),
+            (r'_\d*', Name),
+            (r'_?[a-z][\w\']*', Name)
+        ],
+        'typename': [
+            (_caps + r'?((?:\s)*)(_?[A-Z]\w*)',
+             bygroups(Keyword, Text, Name.Class), '#pop')
+        ],
+        'methodname': [
+            (_caps + r'?((?:\s)*)(_?[a-z]\w*)',
+             bygroups(Keyword, Text, Name.Function), '#pop')
+        ],
+        'nested_comment': [
+            (r'[^*/]+', Comment.Multiline),
+            (r'/\*', Comment.Multiline, '#push'),
+            (r'\*/', Comment.Multiline, '#pop'),
+            (r'[*/]', Comment.Multiline)
+        ],
+        'string': [
+            (r'"', String, '#pop'),
+            (r'\\"', String),
+            (r'[^\\"]+', String)
+        ]
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/praat.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/praat.py
new file mode 100644
index 00000000..054f5b61
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/praat.py
@@ -0,0 +1,303 @@
+"""
+    pygments.lexers.praat
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    Lexer for Praat
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, words, bygroups, include
+from pygments.token import Name, Text, Comment, Keyword, String, Punctuation, \
+    Number, Operator, Whitespace
+
+__all__ = ['PraatLexer']
+
+
+class PraatLexer(RegexLexer):
+    """
+    For Praat scripts.
+    """
+
+    name = 'Praat'
+    url = 'http://www.praat.org'
+    aliases = ['praat']
+    filenames = ['*.praat', '*.proc', '*.psc']
+    version_added = '2.1'
+
+    keywords = (
+        'if', 'then', 'else', 'elsif', 'elif', 'endif', 'fi', 'for', 'from', 'to',
+        'endfor', 'endproc', 'while', 'endwhile', 'repeat', 'until', 'select', 'plus',
+        'minus', 'demo', 'assert', 'stopwatch', 'nocheck', 'nowarn', 'noprogress',
+        'editor', 'endeditor', 'clearinfo',
+    )
+
+    functions_string = (
+        'backslashTrigraphsToUnicode', 'chooseDirectory', 'chooseReadFile',
+        'chooseWriteFile', 'date', 'demoKey', 'do', 'environment', 'extractLine',
+        'extractWord', 'fixed', 'info', 'left', 'mid', 'percent', 'readFile', 'replace',
+        'replace_regex', 'right', 'selected', 'string', 'unicodeToBackslashTrigraphs',
+    )
+
+    functions_numeric = (
+        'abs', 'appendFile', 'appendFileLine', 'appendInfo', 'appendInfoLine', 'arccos',
+        'arccosh', 'arcsin', 'arcsinh', 'arctan', 'arctan2', 'arctanh', 'barkToHertz',
+        'beginPause', 'beginSendPraat', 'besselI', 'besselK', 'beta', 'beta2',
+        'binomialP', 'binomialQ', 'boolean', 'ceiling', 'chiSquareP', 'chiSquareQ',
+        'choice', 'comment', 'cos', 'cosh', 'createDirectory', 'deleteFile',
+        'demoClicked', 'demoClickedIn', 'demoCommandKeyPressed',
+        'demoExtraControlKeyPressed', 'demoInput', 'demoKeyPressed',
+        'demoOptionKeyPressed', 'demoShiftKeyPressed', 'demoShow', 'demoWaitForInput',
+        'demoWindowTitle', 'demoX', 'demoY', 'differenceLimensToPhon', 'do', 'editor',
+        'endPause', 'endSendPraat', 'endsWith', 'erb', 'erbToHertz', 'erf', 'erfc',
+        'exitScript', 'exp', 'extractNumber', 'fileReadable', 'fisherP', 'fisherQ',
+        'floor', 'gaussP', 'gaussQ', 'hertzToBark', 'hertzToErb', 'hertzToMel',
+        'hertzToSemitones', 'imax', 'imin', 'incompleteBeta', 'incompleteGammaP', 'index',
+        'index_regex', 'integer', 'invBinomialP', 'invBinomialQ', 'invChiSquareQ', 'invFisherQ',
+        'invGaussQ', 'invSigmoid', 'invStudentQ', 'length', 'ln', 'lnBeta', 'lnGamma',
+        'log10', 'log2', 'max', 'melToHertz', 'min', 'minusObject', 'natural', 'number',
+        'numberOfColumns', 'numberOfRows', 'numberOfSelected', 'objectsAreIdentical',
+        'option', 'optionMenu', 'pauseScript', 'phonToDifferenceLimens', 'plusObject',
+        'positive', 'randomBinomial', 'randomGauss', 'randomInteger', 'randomPoisson',
+        'randomUniform', 'real', 'readFile', 'removeObject', 'rindex', 'rindex_regex',
+        'round', 'runScript', 'runSystem', 'runSystem_nocheck', 'selectObject',
+        'selected', 'semitonesToHertz', 'sentence', 'sentencetext', 'sigmoid', 'sin', 'sinc',
+        'sincpi', 'sinh', 'soundPressureToPhon', 'sqrt', 'startsWith', 'studentP',
+        'studentQ', 'tan', 'tanh', 'text', 'variableExists', 'word', 'writeFile', 'writeFileLine',
+        'writeInfo', 'writeInfoLine',
+    )
+
+    functions_array = (
+        'linear', 'randomGauss', 'randomInteger', 'randomUniform', 'zero',
+    )
+
+    objects = (
+        'Activation', 'AffineTransform', 'AmplitudeTier', 'Art', 'Artword',
+        'Autosegment', 'BarkFilter', 'BarkSpectrogram', 'CCA', 'Categories',
+        'Cepstrogram', 'Cepstrum', 'Cepstrumc', 'ChebyshevSeries', 'ClassificationTable',
+        'Cochleagram', 'Collection', 'ComplexSpectrogram', 'Configuration', 'Confusion',
+        'ContingencyTable', 'Corpus', 'Correlation', 'Covariance',
+        'CrossCorrelationTable', 'CrossCorrelationTables', 'DTW', 'DataModeler',
+        'Diagonalizer', 'Discriminant', 'Dissimilarity', 'Distance', 'Distributions',
+        'DurationTier', 'EEG', 'ERP', 'ERPTier', 'EditCostsTable', 'EditDistanceTable',
+        'Eigen', 'Excitation', 'Excitations', 'ExperimentMFC', 'FFNet', 'FeatureWeights',
+        'FileInMemory', 'FilesInMemory', 'Formant', 'FormantFilter', 'FormantGrid',
+        'FormantModeler', 'FormantPoint', 'FormantTier', 'GaussianMixture', 'HMM',
+        'HMM_Observation', 'HMM_ObservationSequence', 'HMM_State', 'HMM_StateSequence',
+        'Harmonicity', 'ISpline', 'Index', 'Intensity', 'IntensityTier', 'IntervalTier',
+        'KNN', 'KlattGrid', 'KlattTable', 'LFCC', 'LPC', 'Label', 'LegendreSeries',
+        'LinearRegression', 'LogisticRegression', 'LongSound', 'Ltas', 'MFCC', 'MSpline',
+        'ManPages', 'Manipulation', 'Matrix', 'MelFilter', 'MelSpectrogram',
+        'MixingMatrix', 'Movie', 'Network', 'Object', 'OTGrammar', 'OTHistory', 'OTMulti',
+        'PCA', 'PairDistribution', 'ParamCurve', 'Pattern', 'Permutation', 'Photo',
+        'Pitch', 'PitchModeler', 'PitchTier', 'PointProcess', 'Polygon', 'Polynomial',
+        'PowerCepstrogram', 'PowerCepstrum', 'Procrustes', 'RealPoint', 'RealTier',
+        'ResultsMFC', 'Roots', 'SPINET', 'SSCP', 'SVD', 'Salience', 'ScalarProduct',
+        'Similarity', 'SimpleString', 'SortedSetOfString', 'Sound', 'Speaker',
+        'Spectrogram', 'Spectrum', 'SpectrumTier', 'SpeechSynthesizer', 'SpellingChecker',
+        'Strings', 'StringsIndex', 'Table', 'TableOfReal', 'TextGrid', 'TextInterval',
+        'TextPoint', 'TextTier', 'Tier', 'Transition', 'VocalTract', 'VocalTractTier',
+        'Weight', 'WordList',
+    )
+
+    variables_numeric = (
+        'macintosh', 'windows', 'unix', 'praatVersion', 'pi', 'e', 'undefined',
+    )
+
+    variables_string = (
+        'praatVersion', 'tab', 'shellDirectory', 'homeDirectory',
+        'preferencesDirectory', 'newline', 'temporaryDirectory',
+        'defaultDirectory',
+    )
+
+    object_attributes = (
+        'ncol', 'nrow', 'xmin', 'ymin', 'xmax', 'ymax', 'nx', 'ny', 'dx', 'dy',
+    )
+
+    tokens = {
+        'root': [
+            (r'(\s+)(#.*?$)',  bygroups(Whitespace, Comment.Single)),
+            (r'^#.*?$',        Comment.Single),
+            (r';[^\n]*',       Comment.Single),
+            (r'\s+',           Whitespace),
+
+            (r'\bprocedure\b', Keyword,       'procedure_definition'),
+            (r'\bcall\b',      Keyword,       'procedure_call'),
+            (r'@',             Name.Function, 'procedure_call'),
+
+            include('function_call'),
+
+            (words(keywords, suffix=r'\b'), Keyword),
+
+            (r'(\bform\b)(\s+)([^\n]+)',
+             bygroups(Keyword, Whitespace, String), 'old_form'),
+
+            (r'(print(?:line|tab)?|echo|exit|asserterror|pause|send(?:praat|socket)|'
+             r'include|execute|system(?:_nocheck)?)(\s+)',
+             bygroups(Keyword, Whitespace), 'string_unquoted'),
+
+            (r'(goto|label)(\s+)(\w+)', bygroups(Keyword, Whitespace, Name.Label)),
+
+            include('variable_name'),
+            include('number'),
+
+            (r'"', String, 'string'),
+
+            (words((objects), suffix=r'(?=\s+\S+\n)'), Name.Class, 'string_unquoted'),
+
+            (r'\b[A-Z]', Keyword, 'command'),
+            (r'(\.{3}|[)(,])', Punctuation),
+        ],
+        'command': [
+            (r'( ?[\w()-]+ ?)', Keyword),
+
+            include('string_interpolated'),
+
+            (r'\.{3}', Keyword, ('#pop', 'old_arguments')),
+            (r':', Keyword, ('#pop', 'comma_list')),
+            (r'\s', Whitespace, '#pop'),
+        ],
+        'procedure_call': [
+            (r'\s+', Whitespace),
+            (r'([\w.]+)(?:(:)|(?:(\s*)(\()))',
+             bygroups(Name.Function, Punctuation,
+                      Text.Whitespace, Punctuation), '#pop'),
+            (r'([\w.]+)', Name.Function, ('#pop', 'old_arguments')),
+        ],
+        'procedure_definition': [
+            (r'\s', Whitespace),
+            (r'([\w.]+)(\s*?[(:])',
+             bygroups(Name.Function, Whitespace), '#pop'),
+            (r'([\w.]+)([^\n]*)',
+             bygroups(Name.Function, Text), '#pop'),
+        ],
+        'function_call': [
+            (words(functions_string, suffix=r'\$(?=\s*[:(])'), Name.Function, 'function'),
+            (words(functions_array, suffix=r'#(?=\s*[:(])'),   Name.Function, 'function'),
+            (words(functions_numeric, suffix=r'(?=\s*[:(])'),  Name.Function, 'function'),
+        ],
+        'function': [
+            (r'\s+',   Whitespace),
+            (r':',     Punctuation, ('#pop', 'comma_list')),
+            (r'\s*\(', Punctuation, ('#pop', 'comma_list')),
+        ],
+        'comma_list': [
+            (r'(\s*\n\s*)(\.{3})', bygroups(Whitespace, Punctuation)),
+
+            (r'(\s*)(?:([)\]])|(\n))', bygroups(
+                Whitespace, Punctuation, Whitespace), '#pop'),
+
+            (r'\s+', Whitespace),
+            (r'"',   String, 'string'),
+            (r'\b(if|then|else|fi|endif)\b', Keyword),
+
+            include('function_call'),
+            include('variable_name'),
+            include('operator'),
+            include('number'),
+
+            (r'[()]', Text),
+            (r',', Punctuation),
+        ],
+        'old_arguments': [
+            (r'\n', Whitespace, '#pop'),
+
+            include('variable_name'),
+            include('operator'),
+            include('number'),
+
+            (r'"', String, 'string'),
+            (r'[^\n]', Text),
+        ],
+        'number': [
+            (r'\n', Whitespace, '#pop'),
+            (r'\b\d+(\.\d*)?([eE][-+]?\d+)?%?', Number),
+        ],
+        'object_reference': [
+            include('string_interpolated'),
+            (r'([a-z][a-zA-Z0-9_]*|\d+)', Name.Builtin),
+
+            (words(object_attributes, prefix=r'\.'), Name.Builtin, '#pop'),
+
+            (r'\$', Name.Builtin),
+            (r'\[', Text, '#pop'),
+        ],
+        'variable_name': [
+            include('operator'),
+            include('number'),
+
+            (words(variables_string,  suffix=r'\$'), Name.Variable.Global),
+            (words(variables_numeric,
+             suffix=r'(?=[^a-zA-Z0-9_."\'$#\[:(]|\s|^|$)'),
+             Name.Variable.Global),
+
+            (words(objects, prefix=r'\b', suffix=r"(_)"),
+             bygroups(Name.Builtin, Name.Builtin),
+             'object_reference'),
+
+            (r'\.?_?[a-z][\w.]*(\$|#)?', Text),
+            (r'[\[\]]', Punctuation, 'comma_list'),
+
+            include('string_interpolated'),
+        ],
+        'operator': [
+            (r'([+\/*<>=!-]=?|[&*|][&*|]?|\^|<>)',       Operator),
+            (r'(?', Punctuation),
+            (r'"(?:\\x[0-9a-fA-F]+\\|\\u[0-9a-fA-F]{4}|\\U[0-9a-fA-F]{8}|'
+             r'\\[0-7]+\\|\\["\\abcefnrstv]|[^\\"])*"', String.Double),
+            (r"'(?:''|[^'])*'", String.Atom),  # quoted atom
+            # Needs to not be followed by an atom.
+            # (r'=(?=\s|[a-zA-Z\[])', Operator),
+            (r'is\b', Operator),
+            (r'(<|>|=<|>=|==|=:=|=|/|//|\*|\+|-)(?=\s|[a-zA-Z0-9\[])',
+             Operator),
+            (r'(mod|div|not)\b', Operator),
+            (r'_', Keyword),  # The don't-care variable
+            (r'([a-z]+)(:)', bygroups(Name.Namespace, Punctuation)),
+            (r'([a-z\u00c0-\u1fff\u3040-\ud7ff\ue000-\uffef]'
+             r'[\w$\u00c0-\u1fff\u3040-\ud7ff\ue000-\uffef]*)'
+             r'(\s*)(:-|-->)',
+             bygroups(Name.Function, Text, Operator)),  # function defn
+            (r'([a-z\u00c0-\u1fff\u3040-\ud7ff\ue000-\uffef]'
+             r'[\w$\u00c0-\u1fff\u3040-\ud7ff\ue000-\uffef]*)'
+             r'(\s*)(\()',
+             bygroups(Name.Function, Text, Punctuation)),
+            (r'[a-z\u00c0-\u1fff\u3040-\ud7ff\ue000-\uffef]'
+             r'[\w$\u00c0-\u1fff\u3040-\ud7ff\ue000-\uffef]*',
+             String.Atom),  # atom, characters
+            # This one includes !
+            (r'[#&*+\-./:<=>?@\\^~\u00a1-\u00bf\u2010-\u303f]+',
+             String.Atom),  # atom, graphics
+            (r'[A-Z_]\w*', Name.Variable),
+            (r'\s+|[\u2000-\u200f\ufff0-\ufffe\uffef]', Text),
+        ],
+        'nested-comment': [
+            (r'\*/', Comment.Multiline, '#pop'),
+            (r'/\*', Comment.Multiline, '#push'),
+            (r'[^*/]+', Comment.Multiline),
+            (r'[*/]', Comment.Multiline),
+        ],
+    }
+
+    def analyse_text(text):
+        """Competes with IDL and Visual Prolog on *.pro"""
+        if ':-' in text:
+            # Visual Prolog also uses :-
+            return 0.5
+        else:
+            return 0
+
+
+class LogtalkLexer(RegexLexer):
+    """
+    For Logtalk source code.
+    """
+
+    name = 'Logtalk'
+    url = 'http://logtalk.org/'
+    aliases = ['logtalk']
+    filenames = ['*.lgt', '*.logtalk']
+    mimetypes = ['text/x-logtalk']
+    version_added = '0.10'
+
+    tokens = {
+        'root': [
+            # Directives
+            (r'^\s*:-\s', Punctuation, 'directive'),
+            # Comments
+            (r'%.*?\n', Comment),
+            (r'/\*(.|\n)*?\*/', Comment),
+            # Whitespace
+            (r'\n', Text),
+            (r'\s+', Text),
+            # Numbers
+            (r"0'[\\]?.", Number),
+            (r'0b[01]+', Number.Bin),
+            (r'0o[0-7]+', Number.Oct),
+            (r'0x[0-9a-fA-F]+', Number.Hex),
+            (r'\d+\.?\d*((e|E)(\+|-)?\d+)?', Number),
+            # Variables
+            (r'([A-Z_][a-zA-Z0-9_]*)', Name.Variable),
+            # Event handlers
+            (r'(after|before)(?=[(])', Keyword),
+            # Message forwarding handler
+            (r'forward(?=[(])', Keyword),
+            # Execution-context methods
+            (r'(context|parameter|this|se(lf|nder))(?=[(])', Keyword),
+            # Reflection
+            (r'(current_predicate|predicate_property)(?=[(])', Keyword),
+            # DCGs and term expansion
+            (r'(expand_(goal|term)|(goal|term)_expansion|phrase)(?=[(])', Keyword),
+            # Entity
+            (r'(abolish|c(reate|urrent))_(object|protocol|category)(?=[(])', Keyword),
+            (r'(object|protocol|category)_property(?=[(])', Keyword),
+            # Entity relations
+            (r'co(mplements_object|nforms_to_protocol)(?=[(])', Keyword),
+            (r'extends_(object|protocol|category)(?=[(])', Keyword),
+            (r'imp(lements_protocol|orts_category)(?=[(])', Keyword),
+            (r'(instantiat|specializ)es_class(?=[(])', Keyword),
+            # Events
+            (r'(current_event|(abolish|define)_events)(?=[(])', Keyword),
+            # Flags
+            (r'(create|current|set)_logtalk_flag(?=[(])', Keyword),
+            # Compiling, loading, and library paths
+            (r'logtalk_(compile|l(ibrary_path|oad|oad_context)|make(_target_action)?)(?=[(])', Keyword),
+            (r'\blogtalk_make\b', Keyword),
+            # Database
+            (r'(clause|retract(all)?)(?=[(])', Keyword),
+            (r'a(bolish|ssert(a|z))(?=[(])', Keyword),
+            # Control constructs
+            (r'(ca(ll|tch)|throw)(?=[(])', Keyword),
+            (r'(fa(il|lse)|true|(instantiation|system)_error)\b', Keyword),
+            (r'(uninstantiation|type|domain|existence|permission|representation|evaluation|resource|syntax)_error(?=[(])', Keyword),
+            # All solutions
+            (r'((bag|set)of|f(ind|or)all)(?=[(])', Keyword),
+            # Multi-threading predicates
+            (r'threaded(_(ca(ll|ncel)|once|ignore|exit|peek|wait|notify))?(?=[(])', Keyword),
+            # Engine predicates
+            (r'threaded_engine(_(create|destroy|self|next|next_reified|yield|post|fetch))?(?=[(])', Keyword),
+            # Term unification
+            (r'(subsumes_term|unify_with_occurs_check)(?=[(])', Keyword),
+            # Term creation and decomposition
+            (r'(functor|arg|copy_term|numbervars|term_variables)(?=[(])', Keyword),
+            # Evaluable functors
+            (r'(div|rem|m(ax|in|od)|abs|sign)(?=[(])', Keyword),
+            (r'float(_(integer|fractional)_part)?(?=[(])', Keyword),
+            (r'(floor|t(an|runcate)|round|ceiling)(?=[(])', Keyword),
+            # Other arithmetic functors
+            (r'(cos|a(cos|sin|tan|tan2)|exp|log|s(in|qrt)|xor)(?=[(])', Keyword),
+            # Term testing
+            (r'(var|atom(ic)?|integer|float|c(allable|ompound)|n(onvar|umber)|ground|acyclic_term)(?=[(])', Keyword),
+            # Term comparison
+            (r'compare(?=[(])', Keyword),
+            # Stream selection and control
+            (r'(curren|se)t_(in|out)put(?=[(])', Keyword),
+            (r'(open|close)(?=[(])', Keyword),
+            (r'flush_output(?=[(])', Keyword),
+            (r'(at_end_of_stream|flush_output)\b', Keyword),
+            (r'(stream_property|at_end_of_stream|set_stream_position)(?=[(])', Keyword),
+            # Character and byte input/output
+            (r'(nl|(get|peek|put)_(byte|c(har|ode)))(?=[(])', Keyword),
+            (r'\bnl\b', Keyword),
+            # Term input/output
+            (r'read(_term)?(?=[(])', Keyword),
+            (r'write(q|_(canonical|term))?(?=[(])', Keyword),
+            (r'(current_)?op(?=[(])', Keyword),
+            (r'(current_)?char_conversion(?=[(])', Keyword),
+            # Atomic term processing
+            (r'atom_(length|c(hars|o(ncat|des)))(?=[(])', Keyword),
+            (r'(char_code|sub_atom)(?=[(])', Keyword),
+            (r'number_c(har|ode)s(?=[(])', Keyword),
+            # Implementation defined hooks functions
+            (r'(se|curren)t_prolog_flag(?=[(])', Keyword),
+            (r'\bhalt\b', Keyword),
+            (r'halt(?=[(])', Keyword),
+            # Message sending operators
+            (r'(::|:|\^\^)', Operator),
+            # External call
+            (r'[{}]', Keyword),
+            # Logic and control
+            (r'(ignore|once)(?=[(])', Keyword),
+            (r'\brepeat\b', Keyword),
+            # Sorting
+            (r'(key)?sort(?=[(])', Keyword),
+            # Bitwise functors
+            (r'(>>|<<|/\\|\\\\|\\)', Operator),
+            # Predicate aliases
+            (r'\bas\b', Operator),
+            # Arithmetic evaluation
+            (r'\bis\b', Keyword),
+            # Arithmetic comparison
+            (r'(=:=|=\\=|<|=<|>=|>)', Operator),
+            # Term creation and decomposition
+            (r'=\.\.', Operator),
+            # Term unification
+            (r'(=|\\=)', Operator),
+            # Term comparison
+            (r'(==|\\==|@=<|@<|@>=|@>)', Operator),
+            # Evaluable functors
+            (r'(//|[-+*/])', Operator),
+            (r'\b(e|pi|div|mod|rem)\b', Operator),
+            # Other arithmetic functors
+            (r'\b\*\*\b', Operator),
+            # DCG rules
+            (r'-->', Operator),
+            # Control constructs
+            (r'([!;]|->)', Operator),
+            # Logic and control
+            (r'\\+', Operator),
+            # Mode operators
+            (r'[?@]', Operator),
+            # Existential quantifier
+            (r'\^', Operator),
+            # Punctuation
+            (r'[()\[\],.|]', Text),
+            # Atoms
+            (r"[a-z][a-zA-Z0-9_]*", Text),
+            (r"'", String, 'quoted_atom'),
+            # Double-quoted terms
+            (r'"', String, 'double_quoted_term'),
+        ],
+
+        'quoted_atom': [
+            (r"''", String),
+            (r"'", String, '#pop'),
+            (r'\\([\\abfnrtv"\']|(x[a-fA-F0-9]+|[0-7]+)\\)', String.Escape),
+            (r"[^\\'\n]+", String),
+            (r'\\', String),
+        ],
+
+        'double_quoted_term': [
+            (r'""', String),
+            (r'"', String, '#pop'),
+            (r'\\([\\abfnrtv"\']|(x[a-fA-F0-9]+|[0-7]+)\\)', String.Escape),
+            (r'[^\\"\n]+', String),
+            (r'\\', String),
+        ],
+
+        'directive': [
+            # Conditional compilation directives
+            (r'(el)?if(?=[(])', Keyword, 'root'),
+            (r'(e(lse|ndif))(?=[.])', Keyword, 'root'),
+            # Entity directives
+            (r'(category|object|protocol)(?=[(])', Keyword, 'entityrelations'),
+            (r'(end_(category|object|protocol))(?=[.])', Keyword, 'root'),
+            # Predicate scope directives
+            (r'(public|protected|private)(?=[(])', Keyword, 'root'),
+            # Other directives
+            (r'e(n(coding|sure_loaded)|xport)(?=[(])', Keyword, 'root'),
+            (r'in(clude|itialization|fo)(?=[(])', Keyword, 'root'),
+            (r'(built_in|dynamic|synchronized|threaded)(?=[.])', Keyword, 'root'),
+            (r'(alias|d(ynamic|iscontiguous)|m(eta_(non_terminal|predicate)|ode|ultifile)|s(et_(logtalk|prolog)_flag|ynchronized))(?=[(])', Keyword, 'root'),
+            (r'op(?=[(])', Keyword, 'root'),
+            (r'(c(alls|oinductive)|module|reexport|use(s|_module))(?=[(])', Keyword, 'root'),
+            (r'[a-z][a-zA-Z0-9_]*(?=[(])', Text, 'root'),
+            (r'[a-z][a-zA-Z0-9_]*(?=[.])', Text, 'root'),
+        ],
+
+        'entityrelations': [
+            (r'(complements|extends|i(nstantiates|mp(lements|orts))|specializes)(?=[(])', Keyword),
+            # Numbers
+            (r"0'[\\]?.", Number),
+            (r'0b[01]+', Number.Bin),
+            (r'0o[0-7]+', Number.Oct),
+            (r'0x[0-9a-fA-F]+', Number.Hex),
+            (r'\d+\.?\d*((e|E)(\+|-)?\d+)?', Number),
+            # Variables
+            (r'([A-Z_][a-zA-Z0-9_]*)', Name.Variable),
+            # Atoms
+            (r"[a-z][a-zA-Z0-9_]*", Text),
+            (r"'", String, 'quoted_atom'),
+            # Double-quoted terms
+            (r'"', String, 'double_quoted_term'),
+            # End of entity-opening directive
+            (r'([)]\.)', Text, 'root'),
+            # Scope operator
+            (r'(::)', Operator),
+            # Punctuation
+            (r'[()\[\],.|]', Text),
+            # Comments
+            (r'%.*?\n', Comment),
+            (r'/\*(.|\n)*?\*/', Comment),
+            # Whitespace
+            (r'\n', Text),
+            (r'\s+', Text),
+        ]
+    }
+
+    def analyse_text(text):
+        if ':- object(' in text:
+            return 1.0
+        elif ':- protocol(' in text:
+            return 1.0
+        elif ':- category(' in text:
+            return 1.0
+        elif re.search(r'^:-\s[a-z]', text, re.M):
+            return 0.9
+        else:
+            return 0.0
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/promql.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/promql.py
new file mode 100644
index 00000000..cad3c254
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/promql.py
@@ -0,0 +1,176 @@
+"""
+    pygments.lexers.promql
+    ~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexer for Prometheus Query Language.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, bygroups, default, words
+from pygments.token import Comment, Keyword, Name, Number, Operator, \
+    Punctuation, String, Whitespace
+
+__all__ = ["PromQLLexer"]
+
+
+class PromQLLexer(RegexLexer):
+    """
+    For PromQL queries.
+
+    For details about the grammar see:
+    https://github.com/prometheus/prometheus/tree/master/promql/parser
+
+    .. versionadded: 2.7
+    """
+
+    name = "PromQL"
+    url = 'https://prometheus.io/docs/prometheus/latest/querying/basics/'
+    aliases = ["promql"]
+    filenames = ["*.promql"]
+    version_added = ''
+
+    base_keywords = (
+        words(
+            (
+                "bool",
+                "by",
+                "group_left",
+                "group_right",
+                "ignoring",
+                "offset",
+                "on",
+                "without",
+            ),
+            suffix=r"\b",
+        ),
+        Keyword,
+    )
+
+    aggregator_keywords = (
+        words(
+            (
+                "sum",
+                "min",
+                "max",
+                "avg",
+                "group",
+                "stddev",
+                "stdvar",
+                "count",
+                "count_values",
+                "bottomk",
+                "topk",
+                "quantile",
+            ),
+            suffix=r"\b",
+        ),
+        Keyword,
+    )
+
+    function_keywords = (
+        words(
+            (
+                "abs",
+                "absent",
+                "absent_over_time",
+                "avg_over_time",
+                "ceil",
+                "changes",
+                "clamp_max",
+                "clamp_min",
+                "count_over_time",
+                "day_of_month",
+                "day_of_week",
+                "days_in_month",
+                "delta",
+                "deriv",
+                "exp",
+                "floor",
+                "histogram_quantile",
+                "holt_winters",
+                "hour",
+                "idelta",
+                "increase",
+                "irate",
+                "label_join",
+                "label_replace",
+                "ln",
+                "log10",
+                "log2",
+                "max_over_time",
+                "min_over_time",
+                "minute",
+                "month",
+                "predict_linear",
+                "quantile_over_time",
+                "rate",
+                "resets",
+                "round",
+                "scalar",
+                "sort",
+                "sort_desc",
+                "sqrt",
+                "stddev_over_time",
+                "stdvar_over_time",
+                "sum_over_time",
+                "time",
+                "timestamp",
+                "vector",
+                "year",
+            ),
+            suffix=r"\b",
+        ),
+        Keyword.Reserved,
+    )
+
+    tokens = {
+        "root": [
+            (r"\n", Whitespace),
+            (r"\s+", Whitespace),
+            (r",", Punctuation),
+            # Keywords
+            base_keywords,
+            aggregator_keywords,
+            function_keywords,
+            # Offsets
+            (r"[1-9][0-9]*[smhdwy]", String),
+            # Numbers
+            (r"-?[0-9]+\.[0-9]+", Number.Float),
+            (r"-?[0-9]+", Number.Integer),
+            # Comments
+            (r"#.*?$", Comment.Single),
+            # Operators
+            (r"(\+|\-|\*|\/|\%|\^)", Operator),
+            (r"==|!=|>=|<=|<|>", Operator),
+            (r"and|or|unless", Operator.Word),
+            # Metrics
+            (r"[_a-zA-Z][a-zA-Z0-9_]+", Name.Variable),
+            # Params
+            (r'(["\'])(.*?)(["\'])', bygroups(Punctuation, String, Punctuation)),
+            # Other states
+            (r"\(", Operator, "function"),
+            (r"\)", Operator),
+            (r"\{", Punctuation, "labels"),
+            (r"\[", Punctuation, "range"),
+        ],
+        "labels": [
+            (r"\}", Punctuation, "#pop"),
+            (r"\n", Whitespace),
+            (r"\s+", Whitespace),
+            (r",", Punctuation),
+            (r'([_a-zA-Z][a-zA-Z0-9_]*?)(\s*?)(=~|!=|=|!~)(\s*?)("|\')(.*?)("|\')',
+             bygroups(Name.Label, Whitespace, Operator, Whitespace,
+                      Punctuation, String, Punctuation)),
+        ],
+        "range": [
+            (r"\]", Punctuation, "#pop"),
+            (r"[1-9][0-9]*[smhdwy]", String),
+        ],
+        "function": [
+            (r"\)", Operator, "#pop"),
+            (r"\(", Operator, "#push"),
+            default("#pop"),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/prql.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/prql.py
new file mode 100644
index 00000000..ee95d2d4
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/prql.py
@@ -0,0 +1,251 @@
+"""
+    pygments.lexers.prql
+    ~~~~~~~~~~~~~~~~~~~~
+
+    Lexer for the PRQL query language.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, combined, words, include, bygroups
+from pygments.token import Comment, Literal, Keyword, Name, Number, Operator, \
+    Punctuation, String, Text, Whitespace
+
+__all__ = ['PrqlLexer']
+
+
+class PrqlLexer(RegexLexer):
+    """
+    For PRQL source code.
+
+    grammar: https://github.com/PRQL/prql/tree/main/grammars
+    """
+
+    name = 'PRQL'
+    url = 'https://prql-lang.org/'
+    aliases = ['prql']
+    filenames = ['*.prql']
+    mimetypes = ['application/prql', 'application/x-prql']
+    version_added = '2.17'
+
+    builtinTypes = words((
+        "bool",
+        "int",
+        "int8", "int16", "int32", "int64", "int128",
+        "float",
+        "text",
+        "set"), suffix=r'\b')
+
+    def innerstring_rules(ttype):
+        return [
+            # the new style '{}'.format(...) string formatting
+            (r'\{'
+             r'((\w+)((\.\w+)|(\[[^\]]+\]))*)?'  # field name
+             r'(\:(.?[<>=\^])?[-+ ]?#?0?(\d+)?,?(\.\d+)?[E-GXb-gnosx%]?)?'
+             r'\}', String.Interpol),
+
+            (r'[^\\\'"%{\n]+', ttype),
+            (r'[\'"\\]', ttype),
+            (r'%|(\{{1,2})', ttype)
+        ]
+
+    def fstring_rules(ttype):
+        return [
+            (r'\}', String.Interpol),
+            (r'\{', String.Interpol, 'expr-inside-fstring'),
+            (r'[^\\\'"{}\n]+', ttype),
+            (r'[\'"\\]', ttype),
+        ]
+
+    tokens = {
+        'root': [
+
+            # Comments
+            (r'#!.*', String.Doc),
+            (r'#.*', Comment.Single),
+
+            # Whitespace
+            (r'\s+', Whitespace),
+
+            # Modules
+            (r'^(\s*)(module)(\s*)',
+             bygroups(Whitespace, Keyword.Namespace, Whitespace),
+             'imports'),
+
+            (builtinTypes, Keyword.Type),
+
+            # Main
+            (r'^prql ', Keyword.Reserved),
+
+            ('let', Keyword.Declaration),
+
+            include('keywords'),
+            include('expr'),
+
+            # Transforms
+            (r'^[A-Za-z_][a-zA-Z0-9_]*', Keyword),
+        ],
+        'expr': [
+            # non-raw f-strings
+            ('(f)(""")', bygroups(String.Affix, String.Double),
+             combined('fstringescape', 'tdqf')),
+            ("(f)(''')", bygroups(String.Affix, String.Single),
+             combined('fstringescape', 'tsqf')),
+            ('(f)(")', bygroups(String.Affix, String.Double),
+             combined('fstringescape', 'dqf')),
+            ("(f)(')", bygroups(String.Affix, String.Single),
+             combined('fstringescape', 'sqf')),
+
+            # non-raw s-strings
+            ('(s)(""")', bygroups(String.Affix, String.Double),
+             combined('stringescape', 'tdqf')),
+            ("(s)(''')", bygroups(String.Affix, String.Single),
+             combined('stringescape', 'tsqf')),
+            ('(s)(")', bygroups(String.Affix, String.Double),
+             combined('stringescape', 'dqf')),
+            ("(s)(')", bygroups(String.Affix, String.Single),
+             combined('stringescape', 'sqf')),
+
+            # raw strings
+            ('(?i)(r)(""")',
+             bygroups(String.Affix, String.Double), 'tdqs'),
+            ("(?i)(r)(''')",
+             bygroups(String.Affix, String.Single), 'tsqs'),
+            ('(?i)(r)(")',
+             bygroups(String.Affix, String.Double), 'dqs'),
+            ("(?i)(r)(')",
+             bygroups(String.Affix, String.Single), 'sqs'),
+
+            # non-raw strings
+            ('"""', String.Double, combined('stringescape', 'tdqs')),
+            ("'''", String.Single, combined('stringescape', 'tsqs')),
+            ('"', String.Double, combined('stringescape', 'dqs')),
+            ("'", String.Single, combined('stringescape', 'sqs')),
+
+            # Time and dates
+            (r'@\d{4}-\d{2}-\d{2}T\d{2}(:\d{2})?(:\d{2})?(\.\d{1,6})?(Z|[+-]\d{1,2}(:\d{1,2})?)?', Literal.Date),
+            (r'@\d{4}-\d{2}-\d{2}', Literal.Date),
+            (r'@\d{2}(:\d{2})?(:\d{2})?(\.\d{1,6})?(Z|[+-]\d{1,2}(:\d{1,2})?)?', Literal.Date),
+
+            (r'[^\S\n]+', Text),
+            include('numbers'),
+            (r'->|=>|==|!=|>=|<=|~=|&&|\|\||\?\?|\/\/', Operator),
+            (r'[-~+/*%=<>&^|.@]', Operator),
+            (r'[]{}:(),;[]', Punctuation),
+            include('functions'),
+
+            # Variable Names
+            (r'[A-Za-z_][a-zA-Z0-9_]*', Name.Variable),
+        ],
+        'numbers': [
+            (r'(\d(?:_?\d)*\.(?:\d(?:_?\d)*)?|(?:\d(?:_?\d)*)?\.\d(?:_?\d)*)'
+             r'([eE][+-]?\d(?:_?\d)*)?', Number.Float),
+            (r'\d(?:_?\d)*[eE][+-]?\d(?:_?\d)*j?', Number.Float),
+            (r'0[oO](?:_?[0-7])+', Number.Oct),
+            (r'0[bB](?:_?[01])+', Number.Bin),
+            (r'0[xX](?:_?[a-fA-F0-9])+', Number.Hex),
+            (r'\d(?:_?\d)*', Number.Integer),
+        ],
+        'fstringescape': [
+            include('stringescape'),
+        ],
+        'bytesescape': [
+            (r'\\([\\bfnrt"\']|\n|x[a-fA-F0-9]{2}|[0-7]{1,3})', String.Escape)
+        ],
+        'stringescape': [
+            (r'\\(N\{.*?\}|u\{[a-fA-F0-9]{1,6}\})', String.Escape),
+            include('bytesescape')
+        ],
+        'fstrings-single': fstring_rules(String.Single),
+        'fstrings-double': fstring_rules(String.Double),
+        'strings-single': innerstring_rules(String.Single),
+        'strings-double': innerstring_rules(String.Double),
+        'dqf': [
+            (r'"', String.Double, '#pop'),
+            (r'\\\\|\\"|\\\n', String.Escape),  # included here for raw strings
+            include('fstrings-double')
+        ],
+        'sqf': [
+            (r"'", String.Single, '#pop'),
+            (r"\\\\|\\'|\\\n", String.Escape),  # included here for raw strings
+            include('fstrings-single')
+        ],
+        'dqs': [
+            (r'"', String.Double, '#pop'),
+            (r'\\\\|\\"|\\\n', String.Escape),  # included here for raw strings
+            include('strings-double')
+        ],
+        'sqs': [
+            (r"'", String.Single, '#pop'),
+            (r"\\\\|\\'|\\\n", String.Escape),  # included here for raw strings
+            include('strings-single')
+        ],
+        'tdqf': [
+            (r'"""', String.Double, '#pop'),
+            include('fstrings-double'),
+            (r'\n', String.Double)
+        ],
+        'tsqf': [
+            (r"'''", String.Single, '#pop'),
+            include('fstrings-single'),
+            (r'\n', String.Single)
+        ],
+        'tdqs': [
+            (r'"""', String.Double, '#pop'),
+            include('strings-double'),
+            (r'\n', String.Double)
+        ],
+        'tsqs': [
+            (r"'''", String.Single, '#pop'),
+            include('strings-single'),
+            (r'\n', String.Single)
+        ],
+
+        'expr-inside-fstring': [
+            (r'[{([]', Punctuation, 'expr-inside-fstring-inner'),
+            # without format specifier
+            (r'(=\s*)?'         # debug (https://bugs.python.org/issue36817)
+             r'\}', String.Interpol, '#pop'),
+            # with format specifier
+            # we'll catch the remaining '}' in the outer scope
+            (r'(=\s*)?'         # debug (https://bugs.python.org/issue36817)
+             r':', String.Interpol, '#pop'),
+            (r'\s+', Whitespace),  # allow new lines
+            include('expr'),
+        ],
+        'expr-inside-fstring-inner': [
+            (r'[{([]', Punctuation, 'expr-inside-fstring-inner'),
+            (r'[])}]', Punctuation, '#pop'),
+            (r'\s+', Whitespace),  # allow new lines
+            include('expr'),
+        ],
+        'keywords': [
+            (words((
+                'into', 'case', 'type', 'module', 'internal',
+            ), suffix=r'\b'),
+                Keyword),
+            (words(('true', 'false', 'null'), suffix=r'\b'), Keyword.Constant),
+        ],
+        'functions': [
+            (words((
+                "min", "max", "sum", "average", "stddev", "every", "any",
+                "concat_array", "count", "lag", "lead", "first", "last",
+                "rank", "rank_dense", "row_number", "round", "as", "in",
+                "tuple_every", "tuple_map", "tuple_zip", "_eq", "_is_null",
+                "from_text", "lower", "upper", "read_parquet", "read_csv"),
+                suffix=r'\b'),
+             Name.Function),
+        ],
+
+        'comment': [
+            (r'-(?!\})', Comment.Multiline),
+            (r'\{-', Comment.Multiline, 'comment'),
+            (r'[^-}]', Comment.Multiline),
+            (r'-\}', Comment.Multiline, '#pop'),
+        ],
+
+        'imports': [
+            (r'\w+(\.\w+)*', Name.Class, '#pop'),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/ptx.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/ptx.py
new file mode 100644
index 00000000..784ca13a
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/ptx.py
@@ -0,0 +1,119 @@
+"""
+    pygments.lexers.ptx
+    ~~~~~~~~~~~~~~~~~~~
+
+    Lexer for other PTX language.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, include, words
+from pygments.token import Comment, Keyword, Name, String, Number, \
+    Punctuation, Whitespace, Operator
+
+__all__ = ["PtxLexer"]
+
+
+class PtxLexer(RegexLexer):
+    """
+    For NVIDIA `PTX `_
+    source.
+    """
+    name = 'PTX'
+    url = "https://docs.nvidia.com/cuda/parallel-thread-execution/"
+    filenames = ['*.ptx']
+    aliases = ['ptx']
+    mimetypes = ['text/x-ptx']
+    version_added = '2.16'
+
+    #: optional Comment or Whitespace
+    string = r'"[^"]*?"'
+    followsym = r'[a-zA-Z0-9_$]'
+    identifier = r'([-a-zA-Z$._][\w\-$.]*|' + string + ')'
+    block_label = r'(' + identifier + r'|(\d+))'
+
+    tokens = {
+        'root': [
+            include('whitespace'),
+
+            (block_label + r'\s*:', Name.Label),
+
+            include('keyword'),
+
+            (r'%' + identifier, Name.Variable),
+            (r'%\d+', Name.Variable.Anonymous),
+            (r'c?' + string, String),
+            (identifier, Name.Variable),
+            (r';', Punctuation),
+            (r'[*+-/]', Operator),
+
+            (r'0[xX][a-fA-F0-9]+', Number),
+            (r'-?\d+(?:[.]\d+)?(?:[eE][-+]?\d+(?:[.]\d+)?)?', Number),
+
+            (r'[=<>{}\[\]()*.,!]|x\b', Punctuation)
+
+        ],
+        'whitespace': [
+            (r'(\n|\s+)+', Whitespace),
+            (r'//.*?\n', Comment)
+        ],
+
+        'keyword': [
+            # Instruction keywords
+            (words((
+                'abs', 'discard', 'min', 'shf', 'vadd',
+                'activemask', 'div', 'mma', 'shfl', 'vadd2',
+                'add', 'dp2a', 'mov', 'shl', 'vadd4',
+                'addc', 'dp4a', 'movmatrix', 'shr', 'vavrg2',
+                'alloca', 'elect', 'mul', 'sin', 'vavrg4',
+                'and', 'ex2', 'mul24', 'slct', 'vmad',
+                'applypriority', 'exit', 'multimem', 'sqrt', 'vmax',
+                'atom', 'fence', 'nanosleep', 'st', 'vmax2',
+                'bar', 'fma', 'neg', 'stackrestore', 'vmax4',
+                'barrier', 'fns', 'not', 'stacksave', 'vmin',
+                'bfe', 'getctarank', 'or', 'stmatrix', 'vmin2',
+                'bfi', 'griddepcontrol', 'pmevent', 'sub', 'vmin4',
+                'bfind', 'isspacep', 'popc', 'subc', 'vote',
+                'bmsk', 'istypep', 'prefetch', 'suld', 'vset',
+                'bra', 'ld', 'prefetchu', 'suq', 'vset2',
+                'brev', 'ldmatrix', 'prmt', 'sured', 'vset4',
+                'brkpt', 'ldu', 'rcp', 'sust', 'vshl',
+                'brx', 'lg2', 'red', 'szext', 'vshr',
+                'call', 'lop3', 'redux', 'tanh', 'vsub',
+                'clz', 'mad', 'rem', 'testp', 'vsub2',
+                'cnot', 'mad24', 'ret', 'tex', 'vsub4',
+                'copysign', 'madc', 'rsqrt', 'tld4', 'wgmma',
+                'cos', 'mapa', 'sad', 'trap', 'wmma',
+                'cp', 'match', 'selp', 'txq', 'xor',
+                'createpolicy', 'max', 'set', 'vabsdiff', 'cvt',
+                'mbarrier', 'setmaxnreg', 'vabsdiff2', 'cvta',
+                'membar', 'setp', 'vabsdiff4')), Keyword),
+            # State Spaces and Suffixes
+            (words((
+                'reg', '.sreg', '.const', '.global',
+                '.local', '.param', '.shared', '.tex',
+                '.wide', '.loc'
+            )), Keyword.Pseudo),
+            # PTX Directives
+            (words((
+                '.address_size', '.explicitcluster', '.maxnreg', '.section',
+                '.alias', '.extern', '.maxntid', '.shared',
+                '.align', '.file', '.minnctapersm', '.sreg',
+                '.branchtargets', '.func', '.noreturn', '.target',
+                '.callprototype', '.global', '.param', '.tex',
+                '.calltargets', '.loc', '.pragma', '.version',
+                '.common', '.local', '.reg', '.visible',
+                '.const', '.maxclusterrank', '.reqnctapercluster', '.weak',
+                '.entry', '.maxnctapersm', '.reqntid')), Keyword.Reserved),
+            # Fundamental Types
+            (words((
+                '.s8', '.s16', '.s32', '.s64',
+                '.u8', '.u16', '.u32', '.u64',
+                '.f16', '.f16x2', '.f32', '.f64',
+                '.b8', '.b16', '.b32', '.b64',
+                '.pred'
+            )), Keyword.Type)
+        ],
+
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/python.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/python.py
new file mode 100644
index 00000000..805f6ff2
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/python.py
@@ -0,0 +1,1201 @@
+"""
+    pygments.lexers.python
+    ~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for Python and related languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import keyword
+
+from pygments.lexer import DelegatingLexer, RegexLexer, include, \
+    bygroups, using, default, words, combined, this
+from pygments.util import get_bool_opt, shebang_matches
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation, Generic, Other, Error, Whitespace
+from pygments import unistring as uni
+
+__all__ = ['PythonLexer', 'PythonConsoleLexer', 'PythonTracebackLexer',
+           'Python2Lexer', 'Python2TracebackLexer',
+           'CythonLexer', 'DgLexer', 'NumPyLexer']
+
+
+class PythonLexer(RegexLexer):
+    """
+    For Python source code (version 3.x).
+
+    .. versionchanged:: 2.5
+       This is now the default ``PythonLexer``.  It is still available as the
+       alias ``Python3Lexer``.
+    """
+
+    name = 'Python'
+    url = 'https://www.python.org'
+    aliases = ['python', 'py', 'sage', 'python3', 'py3', 'bazel', 'starlark', 'pyi']
+    filenames = [
+        '*.py',
+        '*.pyw',
+        # Type stubs
+        '*.pyi',
+        # Jython
+        '*.jy',
+        # Sage
+        '*.sage',
+        # SCons
+        '*.sc',
+        'SConstruct',
+        'SConscript',
+        # Skylark/Starlark (used by Bazel, Buck, and Pants)
+        '*.bzl',
+        'BUCK',
+        'BUILD',
+        'BUILD.bazel',
+        'WORKSPACE',
+        # Twisted Application infrastructure
+        '*.tac',
+    ]
+    mimetypes = ['text/x-python', 'application/x-python',
+                 'text/x-python3', 'application/x-python3']
+    version_added = '0.10'
+
+    uni_name = f"[{uni.xid_start}][{uni.xid_continue}]*"
+
+    def innerstring_rules(ttype):
+        return [
+            # the old style '%s' % (...) string formatting (still valid in Py3)
+            (r'%(\(\w+\))?[-#0 +]*([0-9]+|[*])?(\.([0-9]+|[*]))?'
+             '[hlL]?[E-GXc-giorsaux%]', String.Interpol),
+            # the new style '{}'.format(...) string formatting
+            (r'\{'
+             r'((\w+)((\.\w+)|(\[[^\]]+\]))*)?'  # field name
+             r'(\![sra])?'                       # conversion
+             r'(\:(.?[<>=\^])?[-+ ]?#?0?(\d+)?,?(\.\d+)?[E-GXb-gnosx%]?)?'
+             r'\}', String.Interpol),
+
+            # backslashes, quotes and formatting signs must be parsed one at a time
+            (r'[^\\\'"%{\n]+', ttype),
+            (r'[\'"\\]', ttype),
+            # unhandled string formatting sign
+            (r'%|(\{{1,2})', ttype)
+            # newlines are an error (use "nl" state)
+        ]
+
+    def fstring_rules(ttype):
+        return [
+            # Assuming that a '}' is the closing brace after format specifier.
+            # Sadly, this means that we won't detect syntax error. But it's
+            # more important to parse correct syntax correctly, than to
+            # highlight invalid syntax.
+            (r'\}', String.Interpol),
+            (r'\{', String.Interpol, 'expr-inside-fstring'),
+            # backslashes, quotes and formatting signs must be parsed one at a time
+            (r'[^\\\'"{}\n]+', ttype),
+            (r'[\'"\\]', ttype),
+            # newlines are an error (use "nl" state)
+        ]
+
+    tokens = {
+        'root': [
+            (r'\n', Whitespace),
+            (r'^(\s*)([rRuUbB]{,2})("""(?:.|\n)*?""")',
+             bygroups(Whitespace, String.Affix, String.Doc)),
+            (r"^(\s*)([rRuUbB]{,2})('''(?:.|\n)*?''')",
+             bygroups(Whitespace, String.Affix, String.Doc)),
+            (r'\A#!.+$', Comment.Hashbang),
+            (r'#.*$', Comment.Single),
+            (r'\\\n', Text),
+            (r'\\', Text),
+            include('keywords'),
+            include('soft-keywords'),
+            (r'(def)((?:\s|\\\s)+)', bygroups(Keyword, Whitespace), 'funcname'),
+            (r'(class)((?:\s|\\\s)+)', bygroups(Keyword, Whitespace), 'classname'),
+            (r'(from)((?:\s|\\\s)+)', bygroups(Keyword.Namespace, Whitespace),
+             'fromimport'),
+            (r'(import)((?:\s|\\\s)+)', bygroups(Keyword.Namespace, Whitespace),
+             'import'),
+            include('expr'),
+        ],
+        'expr': [
+            # raw f-strings
+            ('(?i)(rf|fr)(""")',
+             bygroups(String.Affix, String.Double),
+             combined('rfstringescape', 'tdqf')),
+            ("(?i)(rf|fr)(''')",
+             bygroups(String.Affix, String.Single),
+             combined('rfstringescape', 'tsqf')),
+            ('(?i)(rf|fr)(")',
+             bygroups(String.Affix, String.Double),
+             combined('rfstringescape', 'dqf')),
+            ("(?i)(rf|fr)(')",
+             bygroups(String.Affix, String.Single),
+             combined('rfstringescape', 'sqf')),
+            # non-raw f-strings
+            ('([fF])(""")', bygroups(String.Affix, String.Double),
+             combined('fstringescape', 'tdqf')),
+            ("([fF])(''')", bygroups(String.Affix, String.Single),
+             combined('fstringescape', 'tsqf')),
+            ('([fF])(")', bygroups(String.Affix, String.Double),
+             combined('fstringescape', 'dqf')),
+            ("([fF])(')", bygroups(String.Affix, String.Single),
+             combined('fstringescape', 'sqf')),
+            # raw bytes and strings
+            ('(?i)(rb|br|r)(""")',
+             bygroups(String.Affix, String.Double), 'tdqs'),
+            ("(?i)(rb|br|r)(''')",
+             bygroups(String.Affix, String.Single), 'tsqs'),
+            ('(?i)(rb|br|r)(")',
+             bygroups(String.Affix, String.Double), 'dqs'),
+            ("(?i)(rb|br|r)(')",
+             bygroups(String.Affix, String.Single), 'sqs'),
+            # non-raw strings
+            ('([uU]?)(""")', bygroups(String.Affix, String.Double),
+             combined('stringescape', 'tdqs')),
+            ("([uU]?)(''')", bygroups(String.Affix, String.Single),
+             combined('stringescape', 'tsqs')),
+            ('([uU]?)(")', bygroups(String.Affix, String.Double),
+             combined('stringescape', 'dqs')),
+            ("([uU]?)(')", bygroups(String.Affix, String.Single),
+             combined('stringescape', 'sqs')),
+            # non-raw bytes
+            ('([bB])(""")', bygroups(String.Affix, String.Double),
+             combined('bytesescape', 'tdqs')),
+            ("([bB])(''')", bygroups(String.Affix, String.Single),
+             combined('bytesescape', 'tsqs')),
+            ('([bB])(")', bygroups(String.Affix, String.Double),
+             combined('bytesescape', 'dqs')),
+            ("([bB])(')", bygroups(String.Affix, String.Single),
+             combined('bytesescape', 'sqs')),
+
+            (r'[^\S\n]+', Text),
+            include('numbers'),
+            (r'!=|==|<<|>>|:=|[-~+/*%=<>&^|.]', Operator),
+            (r'[]{}:(),;[]', Punctuation),
+            (r'(in|is|and|or|not)\b', Operator.Word),
+            include('expr-keywords'),
+            include('builtins'),
+            include('magicfuncs'),
+            include('magicvars'),
+            include('name'),
+        ],
+        'expr-inside-fstring': [
+            (r'[{([]', Punctuation, 'expr-inside-fstring-inner'),
+            # without format specifier
+            (r'(=\s*)?'         # debug (https://bugs.python.org/issue36817)
+             r'(\![sraf])?'     # conversion
+             r'\}', String.Interpol, '#pop'),
+            # with format specifier
+            # we'll catch the remaining '}' in the outer scope
+            (r'(=\s*)?'         # debug (https://bugs.python.org/issue36817)
+             r'(\![sraf])?'     # conversion
+             r':', String.Interpol, '#pop'),
+            (r'\s+', Whitespace),  # allow new lines
+            include('expr'),
+        ],
+        'expr-inside-fstring-inner': [
+            (r'[{([]', Punctuation, 'expr-inside-fstring-inner'),
+            (r'[])}]', Punctuation, '#pop'),
+            (r'\s+', Whitespace),  # allow new lines
+            include('expr'),
+        ],
+        'expr-keywords': [
+            # Based on https://docs.python.org/3/reference/expressions.html
+            (words((
+                'async for', 'await', 'else', 'for', 'if', 'lambda',
+                'yield', 'yield from'), suffix=r'\b'),
+             Keyword),
+            (words(('True', 'False', 'None'), suffix=r'\b'), Keyword.Constant),
+        ],
+        'keywords': [
+            (words((
+                'assert', 'async', 'await', 'break', 'continue', 'del', 'elif',
+                'else', 'except', 'finally', 'for', 'global', 'if', 'lambda',
+                'pass', 'raise', 'nonlocal', 'return', 'try', 'while', 'yield',
+                'yield from', 'as', 'with'), suffix=r'\b'),
+             Keyword),
+            (words(('True', 'False', 'None'), suffix=r'\b'), Keyword.Constant),
+        ],
+        'soft-keywords': [
+            # `match`, `case` and `_` soft keywords
+            (r'(^[ \t]*)'              # at beginning of line + possible indentation
+             r'(match|case)\b'         # a possible keyword
+             r'(?![ \t]*(?:'           # not followed by...
+             r'[:,;=^&|@~)\]}]|(?:' +  # characters and keywords that mean this isn't
+                                       # pattern matching (but None/True/False is ok)
+             r'|'.join(k for k in keyword.kwlist if k[0].islower()) + r')\b))',
+             bygroups(Text, Keyword), 'soft-keywords-inner'),
+        ],
+        'soft-keywords-inner': [
+            # optional `_` keyword
+            (r'(\s+)([^\n_]*)(_\b)', bygroups(Whitespace, using(this), Keyword)),
+            default('#pop')
+        ],
+        'builtins': [
+            (words((
+                '__import__', 'abs', 'aiter', 'all', 'any', 'bin', 'bool', 'bytearray',
+                'breakpoint', 'bytes', 'callable', 'chr', 'classmethod', 'compile',
+                'complex', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval',
+                'filter', 'float', 'format', 'frozenset', 'getattr', 'globals',
+                'hasattr', 'hash', 'hex', 'id', 'input', 'int', 'isinstance',
+                'issubclass', 'iter', 'len', 'list', 'locals', 'map', 'max',
+                'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow',
+                'print', 'property', 'range', 'repr', 'reversed', 'round', 'set',
+                'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super',
+                'tuple', 'type', 'vars', 'zip'), prefix=r'(?>|[-~+/*%=<>&^|.]', Operator),
+            include('keywords'),
+            (r'(def)((?:\s|\\\s)+)', bygroups(Keyword, Whitespace), 'funcname'),
+            (r'(class)((?:\s|\\\s)+)', bygroups(Keyword, Whitespace), 'classname'),
+            (r'(from)((?:\s|\\\s)+)', bygroups(Keyword.Namespace, Whitespace),
+             'fromimport'),
+            (r'(import)((?:\s|\\\s)+)', bygroups(Keyword.Namespace, Whitespace),
+             'import'),
+            include('builtins'),
+            include('magicfuncs'),
+            include('magicvars'),
+            include('backtick'),
+            ('([rR]|[uUbB][rR]|[rR][uUbB])(""")',
+             bygroups(String.Affix, String.Double), 'tdqs'),
+            ("([rR]|[uUbB][rR]|[rR][uUbB])(''')",
+             bygroups(String.Affix, String.Single), 'tsqs'),
+            ('([rR]|[uUbB][rR]|[rR][uUbB])(")',
+             bygroups(String.Affix, String.Double), 'dqs'),
+            ("([rR]|[uUbB][rR]|[rR][uUbB])(')",
+             bygroups(String.Affix, String.Single), 'sqs'),
+            ('([uUbB]?)(""")', bygroups(String.Affix, String.Double),
+             combined('stringescape', 'tdqs')),
+            ("([uUbB]?)(''')", bygroups(String.Affix, String.Single),
+             combined('stringescape', 'tsqs')),
+            ('([uUbB]?)(")', bygroups(String.Affix, String.Double),
+             combined('stringescape', 'dqs')),
+            ("([uUbB]?)(')", bygroups(String.Affix, String.Single),
+             combined('stringescape', 'sqs')),
+            include('name'),
+            include('numbers'),
+        ],
+        'keywords': [
+            (words((
+                'assert', 'break', 'continue', 'del', 'elif', 'else', 'except',
+                'exec', 'finally', 'for', 'global', 'if', 'lambda', 'pass',
+                'print', 'raise', 'return', 'try', 'while', 'yield',
+                'yield from', 'as', 'with'), suffix=r'\b'),
+             Keyword),
+        ],
+        'builtins': [
+            (words((
+                '__import__', 'abs', 'all', 'any', 'apply', 'basestring', 'bin',
+                'bool', 'buffer', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod',
+                'cmp', 'coerce', 'compile', 'complex', 'delattr', 'dict', 'dir', 'divmod',
+                'enumerate', 'eval', 'execfile', 'exit', 'file', 'filter', 'float',
+                'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'hex', 'id',
+                'input', 'int', 'intern', 'isinstance', 'issubclass', 'iter', 'len',
+                'list', 'locals', 'long', 'map', 'max', 'min', 'next', 'object',
+                'oct', 'open', 'ord', 'pow', 'property', 'range', 'raw_input', 'reduce',
+                'reload', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice',
+                'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type',
+                'unichr', 'unicode', 'vars', 'xrange', 'zip'),
+                prefix=r'(?>> )(.*\n)', bygroups(Generic.Prompt, Other.Code), 'continuations'),
+            # This happens, e.g., when tracebacks are embedded in documentation;
+            # trailing whitespaces are often stripped in such contexts.
+            (r'(>>>)(\n)', bygroups(Generic.Prompt, Whitespace)),
+            (r'(\^C)?Traceback \(most recent call last\):\n', Other.Traceback, 'traceback'),
+            # SyntaxError starts with this
+            (r'  File "[^"]+", line \d+', Other.Traceback, 'traceback'),
+            (r'.*\n', Generic.Output),
+        ],
+        'continuations': [
+            (r'(\.\.\. )(.*\n)', bygroups(Generic.Prompt, Other.Code)),
+            # See above.
+            (r'(\.\.\.)(\n)', bygroups(Generic.Prompt, Whitespace)),
+            default('#pop'),
+        ],
+        'traceback': [
+            # As soon as we see a traceback, consume everything until the next
+            # >>> prompt.
+            (r'(?=>>>( |$))', Text, '#pop'),
+            (r'(KeyboardInterrupt)(\n)', bygroups(Name.Class, Whitespace)),
+            (r'.*\n', Other.Traceback),
+        ],
+    }
+
+
+class PythonConsoleLexer(DelegatingLexer):
+    """
+    For Python console output or doctests, such as:
+
+    .. sourcecode:: pycon
+
+        >>> a = 'foo'
+        >>> print(a)
+        foo
+        >>> 1 / 0
+        Traceback (most recent call last):
+          File "", line 1, in 
+        ZeroDivisionError: integer division or modulo by zero
+
+    Additional options:
+
+    `python3`
+        Use Python 3 lexer for code.  Default is ``True``.
+
+        .. versionadded:: 1.0
+        .. versionchanged:: 2.5
+           Now defaults to ``True``.
+    """
+
+    name = 'Python console session'
+    aliases = ['pycon', 'python-console']
+    mimetypes = ['text/x-python-doctest']
+    url = 'https://python.org'
+    version_added = ''
+
+    def __init__(self, **options):
+        python3 = get_bool_opt(options, 'python3', True)
+        if python3:
+            pylexer = PythonLexer
+            tblexer = PythonTracebackLexer
+        else:
+            pylexer = Python2Lexer
+            tblexer = Python2TracebackLexer
+        # We have two auxiliary lexers. Use DelegatingLexer twice with
+        # different tokens.  TODO: DelegatingLexer should support this
+        # directly, by accepting a tuplet of auxiliary lexers and a tuple of
+        # distinguishing tokens. Then we wouldn't need this intermediary
+        # class.
+        class _ReplaceInnerCode(DelegatingLexer):
+            def __init__(self, **options):
+                super().__init__(pylexer, _PythonConsoleLexerBase, Other.Code, **options)
+        super().__init__(tblexer, _ReplaceInnerCode, Other.Traceback, **options)
+
+
+class PythonTracebackLexer(RegexLexer):
+    """
+    For Python 3.x tracebacks, with support for chained exceptions.
+
+    .. versionchanged:: 2.5
+       This is now the default ``PythonTracebackLexer``.  It is still available
+       as the alias ``Python3TracebackLexer``.
+    """
+
+    name = 'Python Traceback'
+    aliases = ['pytb', 'py3tb']
+    filenames = ['*.pytb', '*.py3tb']
+    mimetypes = ['text/x-python-traceback', 'text/x-python3-traceback']
+    url = 'https://python.org'
+    version_added = '1.0'
+
+    tokens = {
+        'root': [
+            (r'\n', Whitespace),
+            (r'^(\^C)?Traceback \(most recent call last\):\n', Generic.Traceback, 'intb'),
+            (r'^During handling of the above exception, another '
+             r'exception occurred:\n\n', Generic.Traceback),
+            (r'^The above exception was the direct cause of the '
+             r'following exception:\n\n', Generic.Traceback),
+            (r'^(?=  File "[^"]+", line \d+)', Generic.Traceback, 'intb'),
+            (r'^.*\n', Other),
+        ],
+        'intb': [
+            (r'^(  File )("[^"]+")(, line )(\d+)(, in )(.+)(\n)',
+             bygroups(Text, Name.Builtin, Text, Number, Text, Name, Whitespace)),
+            (r'^(  File )("[^"]+")(, line )(\d+)(\n)',
+             bygroups(Text, Name.Builtin, Text, Number, Whitespace)),
+            (r'^(    )(.+)(\n)',
+             bygroups(Whitespace, using(PythonLexer), Whitespace), 'markers'),
+            (r'^([ \t]*)(\.\.\.)(\n)',
+             bygroups(Whitespace, Comment, Whitespace)),  # for doctests...
+            (r'^([^:]+)(: )(.+)(\n)',
+             bygroups(Generic.Error, Text, Name, Whitespace), '#pop'),
+            (r'^([a-zA-Z_][\w.]*)(:?\n)',
+             bygroups(Generic.Error, Whitespace), '#pop'),
+            default('#pop'),
+        ],
+        'markers': [
+            # Either `PEP 657 `
+            # error locations in Python 3.11+, or single-caret markers
+            # for syntax errors before that.
+            (r'^( {4,})([~^]+)(\n)',
+             bygroups(Whitespace, Punctuation.Marker, Whitespace),
+             '#pop'),
+            default('#pop'),
+        ],
+    }
+
+
+Python3TracebackLexer = PythonTracebackLexer
+
+
+class Python2TracebackLexer(RegexLexer):
+    """
+    For Python tracebacks.
+
+    .. versionchanged:: 2.5
+       This class has been renamed from ``PythonTracebackLexer``.
+       ``PythonTracebackLexer`` now refers to the Python 3 variant.
+    """
+
+    name = 'Python 2.x Traceback'
+    aliases = ['py2tb']
+    filenames = ['*.py2tb']
+    mimetypes = ['text/x-python2-traceback']
+    url = 'https://python.org'
+    version_added = '0.7'
+
+    tokens = {
+        'root': [
+            # Cover both (most recent call last) and (innermost last)
+            # The optional ^C allows us to catch keyboard interrupt signals.
+            (r'^(\^C)?(Traceback.*\n)',
+             bygroups(Text, Generic.Traceback), 'intb'),
+            # SyntaxError starts with this.
+            (r'^(?=  File "[^"]+", line \d+)', Generic.Traceback, 'intb'),
+            (r'^.*\n', Other),
+        ],
+        'intb': [
+            (r'^(  File )("[^"]+")(, line )(\d+)(, in )(.+)(\n)',
+             bygroups(Text, Name.Builtin, Text, Number, Text, Name, Whitespace)),
+            (r'^(  File )("[^"]+")(, line )(\d+)(\n)',
+             bygroups(Text, Name.Builtin, Text, Number, Whitespace)),
+            (r'^(    )(.+)(\n)',
+             bygroups(Text, using(Python2Lexer), Whitespace), 'marker'),
+            (r'^([ \t]*)(\.\.\.)(\n)',
+             bygroups(Text, Comment, Whitespace)),  # for doctests...
+            (r'^([^:]+)(: )(.+)(\n)',
+             bygroups(Generic.Error, Text, Name, Whitespace), '#pop'),
+            (r'^([a-zA-Z_]\w*)(:?\n)',
+             bygroups(Generic.Error, Whitespace), '#pop')
+        ],
+        'marker': [
+            # For syntax errors.
+            (r'( {4,})(\^)', bygroups(Text, Punctuation.Marker), '#pop'),
+            default('#pop'),
+        ],
+    }
+
+
+class CythonLexer(RegexLexer):
+    """
+    For Pyrex and Cython source code.
+    """
+
+    name = 'Cython'
+    url = 'https://cython.org'
+    aliases = ['cython', 'pyx', 'pyrex']
+    filenames = ['*.pyx', '*.pxd', '*.pxi']
+    mimetypes = ['text/x-cython', 'application/x-cython']
+    version_added = '1.1'
+
+    tokens = {
+        'root': [
+            (r'\n', Whitespace),
+            (r'^(\s*)("""(?:.|\n)*?""")', bygroups(Whitespace, String.Doc)),
+            (r"^(\s*)('''(?:.|\n)*?''')", bygroups(Whitespace, String.Doc)),
+            (r'[^\S\n]+', Text),
+            (r'#.*$', Comment),
+            (r'[]{}:(),;[]', Punctuation),
+            (r'\\\n', Whitespace),
+            (r'\\', Text),
+            (r'(in|is|and|or|not)\b', Operator.Word),
+            (r'(<)([a-zA-Z0-9.?]+)(>)',
+             bygroups(Punctuation, Keyword.Type, Punctuation)),
+            (r'!=|==|<<|>>|[-~+/*%=<>&^|.?]', Operator),
+            (r'(from)(\d+)(<=)(\s+)(<)(\d+)(:)',
+             bygroups(Keyword, Number.Integer, Operator, Whitespace, Operator,
+                      Name, Punctuation)),
+            include('keywords'),
+            (r'(def|property)(\s+)', bygroups(Keyword, Whitespace), 'funcname'),
+            (r'(cp?def)(\s+)', bygroups(Keyword, Whitespace), 'cdef'),
+            # (should actually start a block with only cdefs)
+            (r'(cdef)(:)', bygroups(Keyword, Punctuation)),
+            (r'(class|struct)(\s+)', bygroups(Keyword, Whitespace), 'classname'),
+            (r'(from)(\s+)', bygroups(Keyword, Whitespace), 'fromimport'),
+            (r'(c?import)(\s+)', bygroups(Keyword, Whitespace), 'import'),
+            include('builtins'),
+            include('backtick'),
+            ('(?:[rR]|[uU][rR]|[rR][uU])"""', String, 'tdqs'),
+            ("(?:[rR]|[uU][rR]|[rR][uU])'''", String, 'tsqs'),
+            ('(?:[rR]|[uU][rR]|[rR][uU])"', String, 'dqs'),
+            ("(?:[rR]|[uU][rR]|[rR][uU])'", String, 'sqs'),
+            ('[uU]?"""', String, combined('stringescape', 'tdqs')),
+            ("[uU]?'''", String, combined('stringescape', 'tsqs')),
+            ('[uU]?"', String, combined('stringescape', 'dqs')),
+            ("[uU]?'", String, combined('stringescape', 'sqs')),
+            include('name'),
+            include('numbers'),
+        ],
+        'keywords': [
+            (words((
+                'assert', 'async', 'await', 'break', 'by', 'continue', 'ctypedef', 'del', 'elif',
+                'else', 'except', 'except?', 'exec', 'finally', 'for', 'fused', 'gil',
+                'global', 'if', 'include', 'lambda', 'nogil', 'pass', 'print',
+                'raise', 'return', 'try', 'while', 'yield', 'as', 'with'), suffix=r'\b'),
+             Keyword),
+            (r'(DEF|IF|ELIF|ELSE)\b', Comment.Preproc),
+        ],
+        'builtins': [
+            (words((
+                '__import__', 'abs', 'all', 'any', 'apply', 'basestring', 'bin', 'bint',
+                'bool', 'buffer', 'bytearray', 'bytes', 'callable', 'chr',
+                'classmethod', 'cmp', 'coerce', 'compile', 'complex', 'delattr',
+                'dict', 'dir', 'divmod', 'enumerate', 'eval', 'execfile', 'exit',
+                'file', 'filter', 'float', 'frozenset', 'getattr', 'globals',
+                'hasattr', 'hash', 'hex', 'id', 'input', 'int', 'intern', 'isinstance',
+                'issubclass', 'iter', 'len', 'list', 'locals', 'long', 'map', 'max',
+                'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'property', 'Py_ssize_t',
+                'range', 'raw_input', 'reduce', 'reload', 'repr', 'reversed',
+                'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod',
+                'str', 'sum', 'super', 'tuple', 'type', 'unichr', 'unicode', 'unsigned',
+                'vars', 'xrange', 'zip'), prefix=r'(??/\\:']?:)(\s*)(\{)",
+             bygroups(Name.Function, Whitespace, Operator, Whitespace, Punctuation),
+             "functions"),
+            # Variable Names
+            (r"([.]?[a-zA-Z][\w.]*)(\s*)([-.~=!@#$%^&*_+|,<>?/\\:']?:)",
+             bygroups(Name.Variable, Whitespace, Operator)),
+            # Functions
+            (r"\{", Punctuation, "functions"),
+            # Parentheses
+            (r"\(", Punctuation, "parentheses"),
+            # Brackets
+            (r"\[", Punctuation, "brackets"),
+            # Errors
+            (r"'`([a-zA-Z][\w.]*)?", Name.Exception),
+            # File Symbols
+            (r"`:([a-zA-Z/][\w./]*)?", String.Symbol),
+            # Symbols
+            (r"`([a-zA-Z][\w.]*)?", String.Symbol),
+            # Numbers
+            include("numbers"),
+            # Variable Names
+            (r"[a-zA-Z][\w.]*", Name),
+            # Operators
+            (r"[-=+*#$%@!~^&:.,<>'\\|/?_]", Operator),
+            # Punctuation
+            (r";", Punctuation),
+        ],
+        "functions": [
+            include("root"),
+            (r"\}", Punctuation, "#pop"),
+        ],
+        "parentheses": [
+            include("root"),
+            (r"\)", Punctuation, "#pop"),
+        ],
+        "brackets": [
+            include("root"),
+            (r"\]", Punctuation, "#pop"),
+        ],
+        "numbers": [
+            # Binary Values
+            (r"[01]+b", Number.Bin),
+            # Nulls/Infinities
+            (r"0[nNwW][cefghijmndzuvtp]?", Number),
+            # Timestamps
+            ((r"(?:[0-9]{4}[.][0-9]{2}[.][0-9]{2}|[0-9]+)"
+              "D(?:[0-9](?:[0-9](?::[0-9]{2}"
+              "(?::[0-9]{2}(?:[.][0-9]*)?)?)?)?)?"), Literal.Date),
+            # Datetimes
+            ((r"[0-9]{4}[.][0-9]{2}"
+              "(?:m|[.][0-9]{2}(?:T(?:[0-9]{2}:[0-9]{2}"
+              "(?::[0-9]{2}(?:[.][0-9]*)?)?)?)?)"), Literal.Date),
+            # Times
+            (r"[0-9]{2}:[0-9]{2}(?::[0-9]{2}(?:[.][0-9]{1,3})?)?",
+             Literal.Date),
+            # GUIDs
+            (r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
+             Number.Hex),
+            # Byte Vectors
+            (r"0x[0-9a-fA-F]+", Number.Hex),
+            # Floats
+            (r"([0-9]*[.]?[0-9]+|[0-9]+[.]?[0-9]*)[eE][+-]?[0-9]+[ef]?",
+             Number.Float),
+            (r"([0-9]*[.][0-9]+|[0-9]+[.][0-9]*)[ef]?", Number.Float),
+            (r"[0-9]+[ef]", Number.Float),
+            # Characters
+            (r"[0-9]+c", Number),
+            # Integers
+            (r"[0-9]+[ihtuv]", Number.Integer),
+            # Long Integers
+            (r"[0-9]+[jnp]?", Number.Integer.Long),
+        ],
+        "comments": [
+            (r"[^\\]+", Comment.Multiline),
+            (r"^\\", Comment.Multiline, "#pop"),
+            (r"\\", Comment.Multiline),
+        ],
+        "strings": [
+            (r'[^"\\]+', String.Double),
+            (r"\\.", String.Escape),
+            (r'"', String.Double, "#pop"),
+        ],
+    }
+
+
+class QLexer(KLexer):
+    """
+    For `Q `_ source code.
+    """
+
+    name = "Q"
+    aliases = ["q"]
+    filenames = ["*.q"]
+    version_added = '2.12'
+
+    tokens = {
+        "root": [
+            (words(("aj", "aj0", "ajf", "ajf0", "all", "and", "any", "asc",
+                    "asof", "attr", "avgs", "ceiling", "cols", "count", "cross",
+                    "csv", "cut", "deltas", "desc", "differ", "distinct", "dsave",
+                    "each", "ej", "ema", "eval", "except", "fby", "fills", "first",
+                    "fkeys", "flip", "floor", "get", "group", "gtime", "hclose",
+                    "hcount", "hdel", "hsym", "iasc", "idesc", "ij", "ijf",
+                    "inter", "inv", "key", "keys", "lj", "ljf", "load", "lower",
+                    "lsq", "ltime", "ltrim", "mavg", "maxs", "mcount", "md5",
+                    "mdev", "med", "meta", "mins", "mmax", "mmin", "mmu", "mod",
+                    "msum", "neg", "next", "not", "null", "or", "over", "parse",
+                    "peach", "pj", "prds", "prior", "prev", "rand", "rank", "ratios",
+                    "raze", "read0", "read1", "reciprocal", "reval", "reverse",
+                    "rload", "rotate", "rsave", "rtrim", "save", "scan", "scov",
+                    "sdev", "set", "show", "signum", "ssr", "string", "sublist",
+                    "sums", "sv", "svar", "system", "tables", "til", "trim", "txf",
+                    "type", "uj", "ujf", "ungroup", "union", "upper", "upsert",
+                    "value", "view", "views", "vs", "where", "wj", "wj1", "ww",
+                    "xasc", "xbar", "xcol", "xcols", "xdesc", "xgroup", "xkey",
+                    "xlog", "xprev", "xrank"),
+                    suffix=r"\b"), Name.Builtin,
+            ),
+            inherit,
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/qlik.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/qlik.py
new file mode 100644
index 00000000..a29f89f3
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/qlik.py
@@ -0,0 +1,117 @@
+"""
+    pygments.lexers.qlik
+    ~~~~~~~~~~~~~~~~~~~~
+
+    Lexer for the qlik scripting language
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, include, bygroups, words
+from pygments.token import Comment, Keyword, Name, Number, Operator, \
+    Punctuation, String, Text
+from pygments.lexers._qlik_builtins import OPERATORS_LIST, STATEMENT_LIST, \
+    SCRIPT_FUNCTIONS, CONSTANT_LIST
+
+__all__ = ["QlikLexer"]
+
+
+class QlikLexer(RegexLexer):
+    """
+    Lexer for qlik code, including .qvs files
+    """
+
+    name = "Qlik"
+    aliases = ["qlik", "qlikview", "qliksense", "qlikscript"]
+    filenames = ["*.qvs", "*.qvw"]
+    url = "https://qlik.com"
+    version_added = '2.12'
+
+    flags = re.IGNORECASE
+
+    tokens = {
+        # Handle multi-line comments
+        "comment": [
+            (r"\*/", Comment.Multiline, "#pop"),
+            (r"[^*]+", Comment.Multiline),
+        ],
+        # Handle numbers
+        "numerics": [
+            (r"\b\d+\.\d+(e\d+)?[fd]?\b", Number.Float),
+            (r"\b\d+\b", Number.Integer),
+        ],
+        # Handle variable names in things
+        "interp": [
+            (
+                r"(\$\()(\w+)(\))",
+                bygroups(String.Interpol, Name.Variable, String.Interpol),
+            ),
+        ],
+        # Handle strings
+        "string": [
+            (r"'", String, "#pop"),
+            include("interp"),
+            (r"[^'$]+", String),
+            (r"\$", String),
+        ],
+        #
+        "assignment": [
+            (r";", Punctuation, "#pop"),
+            include("root"),
+        ],
+        "field_name_quote": [
+            (r'"', String.Symbol, "#pop"),
+            include("interp"),
+            (r"[^\"$]+", String.Symbol),
+            (r"\$", String.Symbol),
+        ],
+        "field_name_bracket": [
+            (r"\]", String.Symbol, "#pop"),
+            include("interp"),
+            (r"[^\]$]+", String.Symbol),
+            (r"\$", String.Symbol),
+        ],
+        "function": [(r"\)", Punctuation, "#pop"), include("root")],
+        "root": [
+            # Whitespace and comments
+            (r"\s+", Text.Whitespace),
+            (r"/\*", Comment.Multiline, "comment"),
+            (r"//.*\n", Comment.Single),
+            # variable assignment
+            (r"(let|set)(\s+)", bygroups(Keyword.Declaration, Text.Whitespace),
+             "assignment"),
+            # Word operators
+            (words(OPERATORS_LIST["words"], prefix=r"\b", suffix=r"\b"),
+             Operator.Word),
+            # Statements
+            (words(STATEMENT_LIST, suffix=r"\b"), Keyword),
+            # Table names
+            (r"[a-z]\w*:", Keyword.Declaration),
+            # Constants
+            (words(CONSTANT_LIST, suffix=r"\b"), Keyword.Constant),
+            # Functions
+            (words(SCRIPT_FUNCTIONS, suffix=r"(?=\s*\()"), Name.Builtin,
+             "function"),
+            # interpolation - e.g. $(variableName)
+            include("interp"),
+            # Quotes denote a field/file name
+            (r'"', String.Symbol, "field_name_quote"),
+            # Square brackets denote a field/file name
+            (r"\[", String.Symbol, "field_name_bracket"),
+            # Strings
+            (r"'", String, "string"),
+            # Numbers
+            include("numerics"),
+            # Operator symbols
+            (words(OPERATORS_LIST["symbols"]), Operator),
+            # Strings denoted by single quotes
+            (r"'.+?'", String),
+            # Words as text
+            (r"\b\w+\b", Text),
+            # Basic punctuation
+            (r"[,;.()\\/]", Punctuation),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/qvt.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/qvt.py
new file mode 100644
index 00000000..302d1b6e
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/qvt.py
@@ -0,0 +1,153 @@
+"""
+    pygments.lexers.qvt
+    ~~~~~~~~~~~~~~~~~~~
+
+    Lexer for QVT Operational language.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, bygroups, include, combined, default, \
+    words
+from pygments.token import Text, Comment, Operator, Keyword, Punctuation, \
+    Name, String, Number
+
+__all__ = ['QVToLexer']
+
+
+class QVToLexer(RegexLexer):
+    """
+    For the QVT Operational Mapping language.
+
+    Reference for implementing this: «Meta Object Facility (MOF) 2.0
+    Query/View/Transformation Specification», Version 1.1 - January 2011
+    (https://www.omg.org/spec/QVT/1.1/), see §8.4, «Concrete Syntax» in
+    particular.
+
+    Notable tokens assignments:
+
+    - Name.Class is assigned to the identifier following any of the following
+      keywords: metamodel, class, exception, primitive, enum, transformation
+      or library
+
+    - Name.Function is assigned to the names of mappings and queries
+
+    - Name.Builtin.Pseudo is assigned to the pre-defined variables 'this',
+      'self' and 'result'.
+    """
+    # With obvious borrowings & inspiration from the Java, Python and C lexers
+
+    name = 'QVTO'
+    aliases = ['qvto', 'qvt']
+    filenames = ['*.qvto']
+    url = 'https://www.omg.org/spec/QVT/1.1'
+    version_added = ''
+
+    tokens = {
+        'root': [
+            (r'\n', Text),
+            (r'[^\S\n]+', Text),
+            (r'(--|//)(\s*)(directive:)?(.*)$',
+             bygroups(Comment, Comment, Comment.Preproc, Comment)),
+            # Uncomment the following if you want to distinguish between
+            # '/*' and '/**', à la javadoc
+            # (r'/[*]{2}(.|\n)*?[*]/', Comment.Multiline),
+            (r'/[*](.|\n)*?[*]/', Comment.Multiline),
+            (r'\\\n', Text),
+            (r'(and|not|or|xor|##?)\b', Operator.Word),
+            (r'(:{1,2}=|[-+]=)\b', Operator.Word),
+            (r'(@|<<|>>)\b', Keyword),  # stereotypes
+            (r'!=|<>|==|=|!->|->|>=|<=|[.]{3}|[+/*%=<>&|.~]', Operator),
+            (r'[]{}:(),;[]', Punctuation),
+            (r'(true|false|unlimited|null)\b', Keyword.Constant),
+            (r'(this|self|result)\b', Name.Builtin.Pseudo),
+            (r'(var)\b', Keyword.Declaration),
+            (r'(from|import)\b', Keyword.Namespace, 'fromimport'),
+            (r'(metamodel|class|exception|primitive|enum|transformation|'
+             r'library)(\s+)(\w+)',
+             bygroups(Keyword.Word, Text, Name.Class)),
+            (r'(exception)(\s+)(\w+)',
+             bygroups(Keyword.Word, Text, Name.Exception)),
+            (r'(main)\b', Name.Function),
+            (r'(mapping|helper|query)(\s+)',
+             bygroups(Keyword.Declaration, Text), 'operation'),
+            (r'(assert)(\s+)\b', bygroups(Keyword, Text), 'assert'),
+            (r'(Bag|Collection|Dict|OrderedSet|Sequence|Set|Tuple|List)\b',
+             Keyword.Type),
+            include('keywords'),
+            ('"', String, combined('stringescape', 'dqs')),
+            ("'", String, combined('stringescape', 'sqs')),
+            include('name'),
+            include('numbers'),
+            # (r'([a-zA-Z_]\w*)(::)([a-zA-Z_]\w*)',
+            # bygroups(Text, Text, Text)),
+        ],
+
+        'fromimport': [
+            (r'(?:[ \t]|\\\n)+', Text),
+            (r'[a-zA-Z_][\w.]*', Name.Namespace),
+            default('#pop'),
+        ],
+
+        'operation': [
+            (r'::', Text),
+            (r'(.*::)([a-zA-Z_]\w*)([ \t]*)(\()',
+             bygroups(Text, Name.Function, Text, Punctuation), '#pop')
+        ],
+
+        'assert': [
+            (r'(warning|error|fatal)\b', Keyword, '#pop'),
+            default('#pop'),  # all else: go back
+        ],
+
+        'keywords': [
+            (words((
+                'abstract', 'access', 'any', 'assert', 'blackbox', 'break',
+                'case', 'collect', 'collectNested', 'collectOne', 'collectselect',
+                'collectselectOne', 'composes', 'compute', 'configuration',
+                'constructor', 'continue', 'datatype', 'default', 'derived',
+                'disjuncts', 'do', 'elif', 'else', 'end', 'endif', 'except',
+                'exists', 'extends', 'forAll', 'forEach', 'forOne', 'from', 'if',
+                'implies', 'in', 'inherits', 'init', 'inout', 'intermediate',
+                'invresolve', 'invresolveIn', 'invresolveone', 'invresolveoneIn',
+                'isUnique', 'iterate', 'late', 'let', 'literal', 'log', 'map',
+                'merges', 'modeltype', 'new', 'object', 'one', 'ordered', 'out',
+                'package', 'population', 'property', 'raise', 'readonly',
+                'references', 'refines', 'reject', 'resolve', 'resolveIn',
+                'resolveone', 'resolveoneIn', 'return', 'select', 'selectOne',
+                'sortedBy', 'static', 'switch', 'tag', 'then', 'try', 'typedef',
+                'unlimited', 'uses', 'when', 'where', 'while', 'with', 'xcollect',
+                'xmap', 'xselect'), suffix=r'\b'), Keyword),
+        ],
+
+        # There is no need to distinguish between String.Single and
+        # String.Double: 'strings' is factorised for 'dqs' and 'sqs'
+        'strings': [
+            (r'[^\\\'"\n]+', String),
+            # quotes, percents and backslashes must be parsed one at a time
+            (r'[\'"\\]', String),
+        ],
+        'stringescape': [
+            (r'\\([\\btnfr"\']|u[0-3][0-7]{2}|u[0-7]{1,2})', String.Escape)
+        ],
+        'dqs': [  # double-quoted string
+            (r'"', String, '#pop'),
+            (r'\\\\|\\"', String.Escape),
+            include('strings')
+        ],
+        'sqs': [  # single-quoted string
+            (r"'", String, '#pop'),
+            (r"\\\\|\\'", String.Escape),
+            include('strings')
+        ],
+        'name': [
+            (r'[a-zA-Z_]\w*', Name),
+        ],
+        # numbers: excerpt taken from the python lexer
+        'numbers': [
+            (r'(\d+\.\d*|\d*\.\d+)([eE][+-]?[0-9]+)?', Number.Float),
+            (r'\d+[eE][+-]?[0-9]+', Number.Float),
+            (r'\d+', Number.Integer)
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/r.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/r.py
new file mode 100644
index 00000000..d3f65ba2
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/r.py
@@ -0,0 +1,196 @@
+"""
+    pygments.lexers.r
+    ~~~~~~~~~~~~~~~~~
+
+    Lexers for the R/S languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import Lexer, RegexLexer, include, do_insertions
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation, Generic, Whitespace
+
+__all__ = ['RConsoleLexer', 'SLexer', 'RdLexer']
+
+
+line_re  = re.compile('.*?\n')
+
+
+class RConsoleLexer(Lexer):
+    """
+    For R console transcripts or R CMD BATCH output files.
+    """
+
+    name = 'RConsole'
+    aliases = ['rconsole', 'rout']
+    filenames = ['*.Rout']
+    url = 'https://www.r-project.org'
+    version_added = ''
+    _example = "rconsole/r-console-transcript.Rout"
+
+    def get_tokens_unprocessed(self, text):
+        slexer = SLexer(**self.options)
+
+        current_code_block = ''
+        insertions = []
+
+        for match in line_re.finditer(text):
+            line = match.group()
+            if line.startswith('>') or line.startswith('+'):
+                # Colorize the prompt as such,
+                # then put rest of line into current_code_block
+                insertions.append((len(current_code_block),
+                                   [(0, Generic.Prompt, line[:2])]))
+                current_code_block += line[2:]
+            else:
+                # We have reached a non-prompt line!
+                # If we have stored prompt lines, need to process them first.
+                if current_code_block:
+                    # Weave together the prompts and highlight code.
+                    yield from do_insertions(
+                        insertions, slexer.get_tokens_unprocessed(current_code_block))
+                    # Reset vars for next code block.
+                    current_code_block = ''
+                    insertions = []
+                # Now process the actual line itself, this is output from R.
+                yield match.start(), Generic.Output, line
+
+        # If we happen to end on a code block with nothing after it, need to
+        # process the last code block. This is neither elegant nor DRY so
+        # should be changed.
+        if current_code_block:
+            yield from do_insertions(
+                insertions, slexer.get_tokens_unprocessed(current_code_block))
+
+
+class SLexer(RegexLexer):
+    """
+    For S, S-plus, and R source code.
+    """
+
+    name = 'S'
+    aliases = ['splus', 's', 'r']
+    filenames = ['*.S', '*.R', '.Rhistory', '.Rprofile', '.Renviron']
+    mimetypes = ['text/S-plus', 'text/S', 'text/x-r-source', 'text/x-r',
+                 'text/x-R', 'text/x-r-history', 'text/x-r-profile']
+    url = 'https://www.r-project.org'
+    version_added = '0.10'
+
+    valid_name = r'`[^`\\]*(?:\\.[^`\\]*)*`|(?:[a-zA-Z]|\.[A-Za-z_.])[\w.]*|\.'
+    tokens = {
+        'comments': [
+            (r'#.*$', Comment.Single),
+        ],
+        'valid_name': [
+            (valid_name, Name),
+        ],
+        'function_name': [
+            (rf'({valid_name})\s*(?=\()', Name.Function),
+        ],
+        'punctuation': [
+            (r'\[{1,2}|\]{1,2}|\(|\)|;|,', Punctuation),
+        ],
+        'keywords': [
+            (r'(if|else|for|while|repeat|in|next|break|return|switch|function)'
+             r'(?![\w.])',
+             Keyword.Reserved),
+        ],
+        'operators': [
+            (r'<>?|-|==|<=|>=|\|>|<|>|&&?|!=|\|\|?|\?', Operator),
+            (r'\*|\+|\^|/|!|%[^%]*%|=|~|\$|@|:{1,3}', Operator),
+        ],
+        'builtin_symbols': [
+            (r'(NULL|NA(_(integer|real|complex|character)_)?|'
+             r'letters|LETTERS|Inf|TRUE|FALSE|NaN|pi|\.\.(\.|[0-9]+))'
+             r'(?![\w.])',
+             Keyword.Constant),
+            (r'(T|F)\b', Name.Builtin.Pseudo),
+        ],
+        'numbers': [
+            # hex number
+            (r'0[xX][a-fA-F0-9]+([pP][0-9]+)?[Li]?', Number.Hex),
+            # decimal number
+            (r'[+-]?([0-9]+(\.[0-9]+)?|\.[0-9]+|\.)([eE][+-]?[0-9]+)?[Li]?',
+             Number),
+        ],
+        'statements': [
+            include('comments'),
+            # whitespaces
+            (r'\s+', Whitespace),
+            (r'\'', String, 'string_squote'),
+            (r'\"', String, 'string_dquote'),
+            include('builtin_symbols'),
+            include('keywords'),
+            include('function_name'),
+            include('valid_name'),
+            include('numbers'),
+            include('punctuation'),
+            include('operators'),
+        ],
+        'root': [
+            # calls:
+            include('statements'),
+            # blocks:
+            (r'\{|\}', Punctuation),
+            # (r'\{', Punctuation, 'block'),
+            (r'.', Text),
+        ],
+        # 'block': [
+        #    include('statements'),
+        #    ('\{', Punctuation, '#push'),
+        #    ('\}', Punctuation, '#pop')
+        # ],
+        'string_squote': [
+            (r'([^\'\\]|\\.)*\'', String, '#pop'),
+        ],
+        'string_dquote': [
+            (r'([^"\\]|\\.)*"', String, '#pop'),
+        ],
+    }
+
+    def analyse_text(text):
+        if re.search(r'[a-z0-9_\])\s]<-(?!-)', text):
+            return 0.11
+
+
+class RdLexer(RegexLexer):
+    """
+    Pygments Lexer for R documentation (Rd) files
+
+    This is a very minimal implementation, highlighting little more
+    than the macros. A description of Rd syntax is found in `Writing R
+    Extensions `_
+    and `Parsing Rd files `_.
+    """
+    name = 'Rd'
+    aliases = ['rd']
+    filenames = ['*.Rd']
+    mimetypes = ['text/x-r-doc']
+    url = 'http://cran.r-project.org/doc/manuals/R-exts.html'
+    version_added = '1.6'
+
+    # To account for verbatim / LaTeX-like / and R-like areas
+    # would require parsing.
+    tokens = {
+        'root': [
+            # catch escaped brackets and percent sign
+            (r'\\[\\{}%]', String.Escape),
+            # comments
+            (r'%.*$', Comment),
+            # special macros with no arguments
+            (r'\\(?:cr|l?dots|R|tab)\b', Keyword.Constant),
+            # macros
+            (r'\\[a-zA-Z]+\b', Keyword),
+            # special preprocessor macros
+            (r'^\s*#(?:ifn?def|endif).*\b', Comment.Preproc),
+            # non-escaped brackets
+            (r'[{}]', Name.Builtin),
+            # everything else
+            (r'[^\\%\n{}]+', Text),
+            (r'.', Text),
+        ]
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rdf.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rdf.py
new file mode 100644
index 00000000..4930c1b3
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rdf.py
@@ -0,0 +1,468 @@
+"""
+    pygments.lexers.rdf
+    ~~~~~~~~~~~~~~~~~~~
+
+    Lexers for semantic web and RDF query languages and markup.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, bygroups, default
+from pygments.token import Keyword, Punctuation, String, Number, Operator, \
+    Generic, Whitespace, Name, Literal, Comment, Text
+
+__all__ = ['SparqlLexer', 'TurtleLexer', 'ShExCLexer']
+
+
+class SparqlLexer(RegexLexer):
+    """
+    Lexer for SPARQL query language.
+    """
+    name = 'SPARQL'
+    aliases = ['sparql']
+    filenames = ['*.rq', '*.sparql']
+    mimetypes = ['application/sparql-query']
+    url = 'https://www.w3.org/TR/sparql11-query'
+    version_added = '2.0'
+
+    # character group definitions ::
+
+    PN_CHARS_BASE_GRP = ('a-zA-Z'
+                         '\u00c0-\u00d6'
+                         '\u00d8-\u00f6'
+                         '\u00f8-\u02ff'
+                         '\u0370-\u037d'
+                         '\u037f-\u1fff'
+                         '\u200c-\u200d'
+                         '\u2070-\u218f'
+                         '\u2c00-\u2fef'
+                         '\u3001-\ud7ff'
+                         '\uf900-\ufdcf'
+                         '\ufdf0-\ufffd')
+
+    PN_CHARS_U_GRP = (PN_CHARS_BASE_GRP + '_')
+
+    PN_CHARS_GRP = (PN_CHARS_U_GRP +
+                    r'\-' +
+                    r'0-9' +
+                    '\u00b7' +
+                    '\u0300-\u036f' +
+                    '\u203f-\u2040')
+
+    HEX_GRP = '0-9A-Fa-f'
+
+    PN_LOCAL_ESC_CHARS_GRP = r' _~.\-!$&"()*+,;=/?#@%'
+
+    # terminal productions ::
+
+    PN_CHARS_BASE = '[' + PN_CHARS_BASE_GRP + ']'
+
+    PN_CHARS_U = '[' + PN_CHARS_U_GRP + ']'
+
+    PN_CHARS = '[' + PN_CHARS_GRP + ']'
+
+    HEX = '[' + HEX_GRP + ']'
+
+    PN_LOCAL_ESC_CHARS = '[' + PN_LOCAL_ESC_CHARS_GRP + ']'
+
+    IRIREF = r'<(?:[^<>"{}|^`\\\x00-\x20])*>'
+
+    BLANK_NODE_LABEL = '_:[0-9' + PN_CHARS_U_GRP + '](?:[' + PN_CHARS_GRP + \
+                       '.]*' + PN_CHARS + ')?'
+
+    PN_PREFIX = PN_CHARS_BASE + '(?:[' + PN_CHARS_GRP + '.]*' + PN_CHARS + ')?'
+
+    VARNAME = '[0-9' + PN_CHARS_U_GRP + '][' + PN_CHARS_U_GRP + \
+              '0-9\u00b7\u0300-\u036f\u203f-\u2040]*'
+
+    PERCENT = '%' + HEX + HEX
+
+    PN_LOCAL_ESC = r'\\' + PN_LOCAL_ESC_CHARS
+
+    PLX = '(?:' + PERCENT + ')|(?:' + PN_LOCAL_ESC + ')'
+
+    PN_LOCAL = ('(?:[' + PN_CHARS_U_GRP + ':0-9' + ']|' + PLX + ')' +
+                '(?:(?:[' + PN_CHARS_GRP + '.:]|' + PLX + ')*(?:[' +
+                PN_CHARS_GRP + ':]|' + PLX + '))?')
+
+    EXPONENT = r'[eE][+-]?\d+'
+
+    # Lexer token definitions ::
+
+    tokens = {
+        'root': [
+            (r'\s+', Text),
+            # keywords ::
+            (r'(?i)(select|construct|describe|ask|where|filter|group\s+by|minus|'
+             r'distinct|reduced|from\s+named|from|order\s+by|desc|asc|limit|'
+             r'offset|values|bindings|load|into|clear|drop|create|add|move|copy|'
+             r'insert\s+data|delete\s+data|delete\s+where|with|delete|insert|'
+             r'using\s+named|using|graph|default|named|all|optional|service|'
+             r'silent|bind|undef|union|not\s+in|in|as|having|to|prefix|base)\b', Keyword),
+            (r'(a)\b', Keyword),
+            # IRIs ::
+            ('(' + IRIREF + ')', Name.Label),
+            # blank nodes ::
+            ('(' + BLANK_NODE_LABEL + ')', Name.Label),
+            #  # variables ::
+            ('[?$]' + VARNAME, Name.Variable),
+            # prefixed names ::
+            (r'(' + PN_PREFIX + r')?(\:)(' + PN_LOCAL + r')?',
+             bygroups(Name.Namespace, Punctuation, Name.Tag)),
+            # function names ::
+            (r'(?i)(str|lang|langmatches|datatype|bound|iri|uri|bnode|rand|abs|'
+             r'ceil|floor|round|concat|strlen|ucase|lcase|encode_for_uri|'
+             r'contains|strstarts|strends|strbefore|strafter|year|month|day|'
+             r'hours|minutes|seconds|timezone|tz|now|uuid|struuid|md5|sha1|sha256|sha384|'
+             r'sha512|coalesce|if|strlang|strdt|sameterm|isiri|isuri|isblank|'
+             r'isliteral|isnumeric|regex|substr|replace|exists|not\s+exists|'
+             r'count|sum|min|max|avg|sample|group_concat|separator)\b',
+             Name.Function),
+            # boolean literals ::
+            (r'(true|false)', Keyword.Constant),
+            # double literals ::
+            (r'[+\-]?(\d+\.\d*' + EXPONENT + r'|\.?\d+' + EXPONENT + ')', Number.Float),
+            # decimal literals ::
+            (r'[+\-]?(\d+\.\d*|\.\d+)', Number.Float),
+            # integer literals ::
+            (r'[+\-]?\d+', Number.Integer),
+            # operators ::
+            (r'(\|\||&&|=|\*|\-|\+|/|!=|<=|>=|!|<|>)', Operator),
+            # punctuation characters ::
+            (r'[(){}.;,:^\[\]]', Punctuation),
+            # line comments ::
+            (r'#[^\n]*', Comment),
+            # strings ::
+            (r'"""', String, 'triple-double-quoted-string'),
+            (r'"', String, 'single-double-quoted-string'),
+            (r"'''", String, 'triple-single-quoted-string'),
+            (r"'", String, 'single-single-quoted-string'),
+        ],
+        'triple-double-quoted-string': [
+            (r'"""', String, 'end-of-string'),
+            (r'[^\\]+', String),
+            (r'\\', String, 'string-escape'),
+        ],
+        'single-double-quoted-string': [
+            (r'"', String, 'end-of-string'),
+            (r'[^"\\\n]+', String),
+            (r'\\', String, 'string-escape'),
+        ],
+        'triple-single-quoted-string': [
+            (r"'''", String, 'end-of-string'),
+            (r'[^\\]+', String),
+            (r'\\', String.Escape, 'string-escape'),
+        ],
+        'single-single-quoted-string': [
+            (r"'", String, 'end-of-string'),
+            (r"[^'\\\n]+", String),
+            (r'\\', String, 'string-escape'),
+        ],
+        'string-escape': [
+            (r'u' + HEX + '{4}', String.Escape, '#pop'),
+            (r'U' + HEX + '{8}', String.Escape, '#pop'),
+            (r'.', String.Escape, '#pop'),
+        ],
+        'end-of-string': [
+            (r'(@)([a-zA-Z]+(?:-[a-zA-Z0-9]+)*)',
+             bygroups(Operator, Name.Function), '#pop:2'),
+            (r'\^\^', Operator, '#pop:2'),
+            default('#pop:2'),
+        ],
+    }
+
+
+class TurtleLexer(RegexLexer):
+    """
+    Lexer for Turtle data language.
+    """
+    name = 'Turtle'
+    aliases = ['turtle']
+    filenames = ['*.ttl']
+    mimetypes = ['text/turtle', 'application/x-turtle']
+    url = 'https://www.w3.org/TR/turtle'
+    version_added = '2.1'
+
+    # character group definitions ::
+    PN_CHARS_BASE_GRP = ('a-zA-Z'
+                         '\u00c0-\u00d6'
+                         '\u00d8-\u00f6'
+                         '\u00f8-\u02ff'
+                         '\u0370-\u037d'
+                         '\u037f-\u1fff'
+                         '\u200c-\u200d'
+                         '\u2070-\u218f'
+                         '\u2c00-\u2fef'
+                         '\u3001-\ud7ff'
+                         '\uf900-\ufdcf'
+                         '\ufdf0-\ufffd')
+
+    PN_CHARS_U_GRP = (PN_CHARS_BASE_GRP + '_')
+
+    PN_CHARS_GRP = (PN_CHARS_U_GRP +
+                    r'\-' +
+                    r'0-9' +
+                    '\u00b7' +
+                    '\u0300-\u036f' +
+                    '\u203f-\u2040')
+
+    PN_CHARS = '[' + PN_CHARS_GRP + ']'
+
+    PN_CHARS_BASE = '[' + PN_CHARS_BASE_GRP + ']'
+
+    PN_PREFIX = PN_CHARS_BASE + '(?:[' + PN_CHARS_GRP + '.]*' + PN_CHARS + ')?'
+
+    HEX_GRP = '0-9A-Fa-f'
+
+    HEX = '[' + HEX_GRP + ']'
+
+    PERCENT = '%' + HEX + HEX
+
+    PN_LOCAL_ESC_CHARS_GRP = r' _~.\-!$&"()*+,;=/?#@%'
+
+    PN_LOCAL_ESC_CHARS = '[' + PN_LOCAL_ESC_CHARS_GRP + ']'
+
+    PN_LOCAL_ESC = r'\\' + PN_LOCAL_ESC_CHARS
+
+    PLX = '(?:' + PERCENT + ')|(?:' + PN_LOCAL_ESC + ')'
+
+    PN_LOCAL = ('(?:[' + PN_CHARS_U_GRP + ':0-9' + ']|' + PLX + ')' +
+                '(?:(?:[' + PN_CHARS_GRP + '.:]|' + PLX + ')*(?:[' +
+                PN_CHARS_GRP + ':]|' + PLX + '))?')
+
+    patterns = {
+        'PNAME_NS': r'((?:[a-zA-Z][\w-]*)?\:)',  # Simplified character range
+        'IRIREF': r'(<[^<>"{}|^`\\\x00-\x20]*>)'
+    }
+
+    tokens = {
+        'root': [
+            (r'\s+', Text),
+
+            # Base / prefix
+            (r'(@base|BASE)(\s+){IRIREF}(\s*)(\.?)'.format(**patterns),
+             bygroups(Keyword, Whitespace, Name.Variable, Whitespace,
+                      Punctuation)),
+            (r'(@prefix|PREFIX)(\s+){PNAME_NS}(\s+){IRIREF}(\s*)(\.?)'.format(**patterns),
+             bygroups(Keyword, Whitespace, Name.Namespace, Whitespace,
+                      Name.Variable, Whitespace, Punctuation)),
+
+            # The shorthand predicate 'a'
+            (r'(?<=\s)a(?=\s)', Keyword.Type),
+
+            # IRIREF
+            (r'{IRIREF}'.format(**patterns), Name.Variable),
+
+            # PrefixedName
+            (r'(' + PN_PREFIX + r')?(\:)(' + PN_LOCAL + r')?',
+             bygroups(Name.Namespace, Punctuation, Name.Tag)),
+
+            # BlankNodeLabel
+            (r'(_)(:)([' + PN_CHARS_U_GRP + r'0-9]([' + PN_CHARS_GRP + r'.]*' + PN_CHARS + ')?)',
+             bygroups(Name.Namespace, Punctuation, Name.Tag)),
+
+            # Comment
+            (r'#[^\n]+', Comment),
+
+            (r'\b(true|false)\b', Literal),
+            (r'[+\-]?\d*\.\d+', Number.Float),
+            (r'[+\-]?\d*(:?\.\d+)?E[+\-]?\d+', Number.Float),
+            (r'[+\-]?\d+', Number.Integer),
+            (r'[\[\](){}.;,:^]', Punctuation),
+
+            (r'"""', String, 'triple-double-quoted-string'),
+            (r'"', String, 'single-double-quoted-string'),
+            (r"'''", String, 'triple-single-quoted-string'),
+            (r"'", String, 'single-single-quoted-string'),
+        ],
+        'triple-double-quoted-string': [
+            (r'"""', String, 'end-of-string'),
+            (r'[^\\]+(?=""")', String),
+            (r'\\', String, 'string-escape'),
+        ],
+        'single-double-quoted-string': [
+            (r'"', String, 'end-of-string'),
+            (r'[^"\\\n]+', String),
+            (r'\\', String, 'string-escape'),
+        ],
+        'triple-single-quoted-string': [
+            (r"'''", String, 'end-of-string'),
+            (r"[^\\]+(?=''')", String),
+            (r'\\', String, 'string-escape'),
+        ],
+        'single-single-quoted-string': [
+            (r"'", String, 'end-of-string'),
+            (r"[^'\\\n]+", String),
+            (r'\\', String, 'string-escape'),
+        ],
+        'string-escape': [
+            (r'.', String, '#pop'),
+        ],
+        'end-of-string': [
+            (r'(@)([a-zA-Z]+(?:-[a-zA-Z0-9]+)*)',
+             bygroups(Operator, Generic.Emph), '#pop:2'),
+
+            (r'(\^\^){IRIREF}'.format(**patterns), bygroups(Operator, Generic.Emph), '#pop:2'),
+
+            default('#pop:2'),
+
+        ],
+    }
+
+    # Turtle and Tera Term macro files share the same file extension
+    # but each has a recognizable and distinct syntax.
+    def analyse_text(text):
+        for t in ('@base ', 'BASE ', '@prefix ', 'PREFIX '):
+            if re.search(rf'^\s*{t}', text):
+                return 0.80
+
+
+class ShExCLexer(RegexLexer):
+    """
+    Lexer for ShExC shape expressions language syntax.
+    """
+    name = 'ShExC'
+    aliases = ['shexc', 'shex']
+    filenames = ['*.shex']
+    mimetypes = ['text/shex']
+    url = 'https://shex.io/shex-semantics/#shexc'
+    version_added = ''
+
+    # character group definitions ::
+
+    PN_CHARS_BASE_GRP = ('a-zA-Z'
+                         '\u00c0-\u00d6'
+                         '\u00d8-\u00f6'
+                         '\u00f8-\u02ff'
+                         '\u0370-\u037d'
+                         '\u037f-\u1fff'
+                         '\u200c-\u200d'
+                         '\u2070-\u218f'
+                         '\u2c00-\u2fef'
+                         '\u3001-\ud7ff'
+                         '\uf900-\ufdcf'
+                         '\ufdf0-\ufffd')
+
+    PN_CHARS_U_GRP = (PN_CHARS_BASE_GRP + '_')
+
+    PN_CHARS_GRP = (PN_CHARS_U_GRP +
+                    r'\-' +
+                    r'0-9' +
+                    '\u00b7' +
+                    '\u0300-\u036f' +
+                    '\u203f-\u2040')
+
+    HEX_GRP = '0-9A-Fa-f'
+
+    PN_LOCAL_ESC_CHARS_GRP = r"_~.\-!$&'()*+,;=/?#@%"
+
+    # terminal productions ::
+
+    PN_CHARS_BASE = '[' + PN_CHARS_BASE_GRP + ']'
+
+    PN_CHARS_U = '[' + PN_CHARS_U_GRP + ']'
+
+    PN_CHARS = '[' + PN_CHARS_GRP + ']'
+
+    HEX = '[' + HEX_GRP + ']'
+
+    PN_LOCAL_ESC_CHARS = '[' + PN_LOCAL_ESC_CHARS_GRP + ']'
+
+    UCHAR_NO_BACKSLASH = '(?:u' + HEX + '{4}|U' + HEX + '{8})'
+
+    UCHAR = r'\\' + UCHAR_NO_BACKSLASH
+
+    IRIREF = r'<(?:[^\x00-\x20<>"{}|^`\\]|' + UCHAR + ')*>'
+
+    BLANK_NODE_LABEL = '_:[0-9' + PN_CHARS_U_GRP + '](?:[' + PN_CHARS_GRP + \
+                       '.]*' + PN_CHARS + ')?'
+
+    PN_PREFIX = PN_CHARS_BASE + '(?:[' + PN_CHARS_GRP + '.]*' + PN_CHARS + ')?'
+
+    PERCENT = '%' + HEX + HEX
+
+    PN_LOCAL_ESC = r'\\' + PN_LOCAL_ESC_CHARS
+
+    PLX = '(?:' + PERCENT + ')|(?:' + PN_LOCAL_ESC + ')'
+
+    PN_LOCAL = ('(?:[' + PN_CHARS_U_GRP + ':0-9' + ']|' + PLX + ')' +
+                '(?:(?:[' + PN_CHARS_GRP + '.:]|' + PLX + ')*(?:[' +
+                PN_CHARS_GRP + ':]|' + PLX + '))?')
+
+    EXPONENT = r'[eE][+-]?\d+'
+
+    # Lexer token definitions ::
+
+    tokens = {
+        'root': [
+            (r'\s+', Text),
+            # keywords ::
+            (r'(?i)(base|prefix|start|external|'
+             r'literal|iri|bnode|nonliteral|length|minlength|maxlength|'
+             r'mininclusive|minexclusive|maxinclusive|maxexclusive|'
+             r'totaldigits|fractiondigits|'
+             r'closed|extra)\b', Keyword),
+            (r'(a)\b', Keyword),
+            # IRIs ::
+            ('(' + IRIREF + ')', Name.Label),
+            # blank nodes ::
+            ('(' + BLANK_NODE_LABEL + ')', Name.Label),
+            # prefixed names ::
+            (r'(' + PN_PREFIX + r')?(\:)(' + PN_LOCAL + ')?',
+             bygroups(Name.Namespace, Punctuation, Name.Tag)),
+            # boolean literals ::
+            (r'(true|false)', Keyword.Constant),
+            # double literals ::
+            (r'[+\-]?(\d+\.\d*' + EXPONENT + r'|\.?\d+' + EXPONENT + ')', Number.Float),
+            # decimal literals ::
+            (r'[+\-]?(\d+\.\d*|\.\d+)', Number.Float),
+            # integer literals ::
+            (r'[+\-]?\d+', Number.Integer),
+            # operators ::
+            (r'[@|$&=*+?^\-~]', Operator),
+            # operator keywords ::
+            (r'(?i)(and|or|not)\b', Operator.Word),
+            # punctuation characters ::
+            (r'[(){}.;,:^\[\]]', Punctuation),
+            # line comments ::
+            (r'#[^\n]*', Comment),
+            # strings ::
+            (r'"""', String, 'triple-double-quoted-string'),
+            (r'"', String, 'single-double-quoted-string'),
+            (r"'''", String, 'triple-single-quoted-string'),
+            (r"'", String, 'single-single-quoted-string'),
+        ],
+        'triple-double-quoted-string': [
+            (r'"""', String, 'end-of-string'),
+            (r'[^\\]+', String),
+            (r'\\', String, 'string-escape'),
+        ],
+        'single-double-quoted-string': [
+            (r'"', String, 'end-of-string'),
+            (r'[^"\\\n]+', String),
+            (r'\\', String, 'string-escape'),
+        ],
+        'triple-single-quoted-string': [
+            (r"'''", String, 'end-of-string'),
+            (r'[^\\]+', String),
+            (r'\\', String.Escape, 'string-escape'),
+        ],
+        'single-single-quoted-string': [
+            (r"'", String, 'end-of-string'),
+            (r"[^'\\\n]+", String),
+            (r'\\', String, 'string-escape'),
+        ],
+        'string-escape': [
+            (UCHAR_NO_BACKSLASH, String.Escape, '#pop'),
+            (r'.', String.Escape, '#pop'),
+        ],
+        'end-of-string': [
+            (r'(@)([a-zA-Z]+(?:-[a-zA-Z0-9]+)*)',
+             bygroups(Operator, Name.Function), '#pop:2'),
+            (r'\^\^', Operator, '#pop:2'),
+            default('#pop:2'),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rebol.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rebol.py
new file mode 100644
index 00000000..4b37a749
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rebol.py
@@ -0,0 +1,419 @@
+"""
+    pygments.lexers.rebol
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for the REBOL and related languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, bygroups
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Generic, Whitespace
+
+__all__ = ['RebolLexer', 'RedLexer']
+
+
+class RebolLexer(RegexLexer):
+    """
+    A REBOL lexer.
+    """
+    name = 'REBOL'
+    aliases = ['rebol']
+    filenames = ['*.r', '*.r3', '*.reb']
+    mimetypes = ['text/x-rebol']
+    url = 'http://www.rebol.com'
+    version_added = '1.1'
+
+    flags = re.IGNORECASE | re.MULTILINE
+
+    escape_re = r'(?:\^\([0-9a-f]{1,4}\)*)'
+
+    def word_callback(lexer, match):
+        word = match.group()
+
+        if re.match(".*:$", word):
+            yield match.start(), Generic.Subheading, word
+        elif re.match(
+            r'(native|alias|all|any|as-string|as-binary|bind|bound\?|case|'
+            r'catch|checksum|comment|debase|dehex|exclude|difference|disarm|'
+            r'either|else|enbase|foreach|remove-each|form|free|get|get-env|if|'
+            r'in|intersect|loop|minimum-of|maximum-of|mold|new-line|'
+            r'new-line\?|not|now|prin|print|reduce|compose|construct|repeat|'
+            r'reverse|save|script\?|set|shift|switch|throw|to-hex|trace|try|'
+            r'type\?|union|unique|unless|unprotect|unset|until|use|value\?|'
+            r'while|compress|decompress|secure|open|close|read|read-io|'
+            r'write-io|write|update|query|wait|input\?|exp|log-10|log-2|'
+            r'log-e|square-root|cosine|sine|tangent|arccosine|arcsine|'
+            r'arctangent|protect|lowercase|uppercase|entab|detab|connected\?|'
+            r'browse|launch|stats|get-modes|set-modes|to-local-file|'
+            r'to-rebol-file|encloak|decloak|create-link|do-browser|bind\?|'
+            r'hide|draw|show|size-text|textinfo|offset-to-caret|'
+            r'caret-to-offset|local-request-file|rgb-to-hsv|hsv-to-rgb|'
+            r'crypt-strength\?|dh-make-key|dh-generate-key|dh-compute-key|'
+            r'dsa-make-key|dsa-generate-key|dsa-make-signature|'
+            r'dsa-verify-signature|rsa-make-key|rsa-generate-key|'
+            r'rsa-encrypt)$', word):
+            yield match.start(), Name.Builtin, word
+        elif re.match(
+            r'(add|subtract|multiply|divide|remainder|power|and~|or~|xor~|'
+            r'minimum|maximum|negate|complement|absolute|random|head|tail|'
+            r'next|back|skip|at|pick|first|second|third|fourth|fifth|sixth|'
+            r'seventh|eighth|ninth|tenth|last|path|find|select|make|to|copy\*|'
+            r'insert|remove|change|poke|clear|trim|sort|min|max|abs|cp|'
+            r'copy)$', word):
+            yield match.start(), Name.Function, word
+        elif re.match(
+            r'(error|source|input|license|help|install|echo|Usage|with|func|'
+            r'throw-on-error|function|does|has|context|probe|\?\?|as-pair|'
+            r'mod|modulo|round|repend|about|set-net|append|join|rejoin|reform|'
+            r'remold|charset|array|replace|move|extract|forskip|forall|alter|'
+            r'first+|also|take|for|forever|dispatch|attempt|what-dir|'
+            r'change-dir|clean-path|list-dir|dirize|rename|split-path|delete|'
+            r'make-dir|delete-dir|in-dir|confirm|dump-obj|upgrade|what|'
+            r'build-tag|process-source|build-markup|decode-cgi|read-cgi|'
+            r'write-user|save-user|set-user-name|protect-system|parse-xml|'
+            r'cvs-date|cvs-version|do-boot|get-net-info|desktop|layout|'
+            r'scroll-para|get-face|alert|set-face|uninstall|unfocus|'
+            r'request-dir|center-face|do-events|net-error|decode-url|'
+            r'parse-header|parse-header-date|parse-email-addrs|import-email|'
+            r'send|build-attach-body|resend|show-popup|hide-popup|open-events|'
+            r'find-key-face|do-face|viewtop|confine|find-window|'
+            r'insert-event-func|remove-event-func|inform|dump-pane|dump-face|'
+            r'flag-face|deflag-face|clear-fields|read-net|vbug|path-thru|'
+            r'read-thru|load-thru|do-thru|launch-thru|load-image|'
+            r'request-download|do-face-alt|set-font|set-para|get-style|'
+            r'set-style|make-face|stylize|choose|hilight-text|hilight-all|'
+            r'unlight-text|focus|scroll-drag|clear-face|reset-face|scroll-face|'
+            r'resize-face|load-stock|load-stock-block|notify|request|flash|'
+            r'request-color|request-pass|request-text|request-list|'
+            r'request-date|request-file|dbug|editor|link-relative-path|'
+            r'emailer|parse-error)$', word):
+            yield match.start(), Keyword.Namespace, word
+        elif re.match(
+            r'(halt|quit|do|load|q|recycle|call|run|ask|parse|view|unview|'
+            r'return|exit|break)$', word):
+            yield match.start(), Name.Exception, word
+        elif re.match('REBOL$', word):
+            yield match.start(), Generic.Heading, word
+        elif re.match("to-.*", word):
+            yield match.start(), Keyword, word
+        elif re.match(r'(\+|-|\*|/|//|\*\*|and|or|xor|=\?|=|==|<>|<|>|<=|>=)$',
+                      word):
+            yield match.start(), Operator, word
+        elif re.match(r".*\?$", word):
+            yield match.start(), Keyword, word
+        elif re.match(r".*\!$", word):
+            yield match.start(), Keyword.Type, word
+        elif re.match("'.*", word):
+            yield match.start(), Name.Variable.Instance, word  # lit-word
+        elif re.match("#.*", word):
+            yield match.start(), Name.Label, word  # issue
+        elif re.match("%.*", word):
+            yield match.start(), Name.Decorator, word  # file
+        else:
+            yield match.start(), Name.Variable, word
+
+    tokens = {
+        'root': [
+            (r'\s+', Text),
+            (r'#"', String.Char, 'char'),
+            (r'#\{[0-9a-f]*\}', Number.Hex),
+            (r'2#\{', Number.Hex, 'bin2'),
+            (r'64#\{[0-9a-z+/=\s]*\}', Number.Hex),
+            (r'"', String, 'string'),
+            (r'\{', String, 'string2'),
+            (r';#+.*\n', Comment.Special),
+            (r';\*+.*\n', Comment.Preproc),
+            (r';.*\n', Comment),
+            (r'%"', Name.Decorator, 'stringFile'),
+            (r'%[^(^{")\s\[\]]+', Name.Decorator),
+            (r'[+-]?([a-z]{1,3})?\$\d+(\.\d+)?', Number.Float),  # money
+            (r'[+-]?\d+\:\d+(\:\d+)?(\.\d+)?', String.Other),    # time
+            (r'\d+[\-/][0-9a-z]+[\-/]\d+(\/\d+\:\d+((\:\d+)?'
+             r'([.\d+]?([+-]?\d+:\d+)?)?)?)?', String.Other),   # date
+            (r'\d+(\.\d+)+\.\d+', Keyword.Constant),             # tuple
+            (r'\d+X\d+', Keyword.Constant),                   # pair
+            (r'[+-]?\d+(\'\d+)?([.,]\d*)?E[+-]?\d+', Number.Float),
+            (r'[+-]?\d+(\'\d+)?[.,]\d*', Number.Float),
+            (r'[+-]?\d+(\'\d+)?', Number),
+            (r'[\[\]()]', Generic.Strong),
+            (r'[a-z]+[^(^{"\s:)]*://[^(^{"\s)]*', Name.Decorator),  # url
+            (r'mailto:[^(^{"@\s)]+@[^(^{"@\s)]+', Name.Decorator),  # url
+            (r'[^(^{"@\s)]+@[^(^{"@\s)]+', Name.Decorator),         # email
+            (r'comment\s"', Comment, 'commentString1'),
+            (r'comment\s\{', Comment, 'commentString2'),
+            (r'comment\s\[', Comment, 'commentBlock'),
+            (r'comment\s[^(\s{"\[]+', Comment),
+            (r'/[^(^{")\s/[\]]*', Name.Attribute),
+            (r'([^(^{")\s/[\]]+)(?=[:({"\s/\[\]])', word_callback),
+            (r'<[\w:.-]*>', Name.Tag),
+            (r'<[^(<>\s")]+', Name.Tag, 'tag'),
+            (r'([^(^{")\s]+)', Text),
+        ],
+        'string': [
+            (r'[^(^")]+', String),
+            (escape_re, String.Escape),
+            (r'[(|)]+', String),
+            (r'\^.', String.Escape),
+            (r'"', String, '#pop'),
+        ],
+        'string2': [
+            (r'[^(^{})]+', String),
+            (escape_re, String.Escape),
+            (r'[(|)]+', String),
+            (r'\^.', String.Escape),
+            (r'\{', String, '#push'),
+            (r'\}', String, '#pop'),
+        ],
+        'stringFile': [
+            (r'[^(^")]+', Name.Decorator),
+            (escape_re, Name.Decorator),
+            (r'\^.', Name.Decorator),
+            (r'"', Name.Decorator, '#pop'),
+        ],
+        'char': [
+            (escape_re + '"', String.Char, '#pop'),
+            (r'\^."', String.Char, '#pop'),
+            (r'."', String.Char, '#pop'),
+        ],
+        'tag': [
+            (escape_re, Name.Tag),
+            (r'"', Name.Tag, 'tagString'),
+            (r'[^(<>\r\n")]+', Name.Tag),
+            (r'>', Name.Tag, '#pop'),
+        ],
+        'tagString': [
+            (r'[^(^")]+', Name.Tag),
+            (escape_re, Name.Tag),
+            (r'[(|)]+', Name.Tag),
+            (r'\^.', Name.Tag),
+            (r'"', Name.Tag, '#pop'),
+        ],
+        'tuple': [
+            (r'(\d+\.)+', Keyword.Constant),
+            (r'\d+', Keyword.Constant, '#pop'),
+        ],
+        'bin2': [
+            (r'\s+', Number.Hex),
+            (r'([01]\s*){8}', Number.Hex),
+            (r'\}', Number.Hex, '#pop'),
+        ],
+        'commentString1': [
+            (r'[^(^")]+', Comment),
+            (escape_re, Comment),
+            (r'[(|)]+', Comment),
+            (r'\^.', Comment),
+            (r'"', Comment, '#pop'),
+        ],
+        'commentString2': [
+            (r'[^(^{})]+', Comment),
+            (escape_re, Comment),
+            (r'[(|)]+', Comment),
+            (r'\^.', Comment),
+            (r'\{', Comment, '#push'),
+            (r'\}', Comment, '#pop'),
+        ],
+        'commentBlock': [
+            (r'\[', Comment, '#push'),
+            (r'\]', Comment, '#pop'),
+            (r'"', Comment, "commentString1"),
+            (r'\{', Comment, "commentString2"),
+            (r'[^(\[\]"{)]+', Comment),
+        ],
+    }
+
+    def analyse_text(text):
+        """
+        Check if code contains REBOL header and so it probably not R code
+        """
+        if re.match(r'^\s*REBOL\s*\[', text, re.IGNORECASE):
+            # The code starts with REBOL header
+            return 1.0
+        elif re.search(r'\s*REBOL\s*\[', text, re.IGNORECASE):
+            # The code contains REBOL header but also some text before it
+            return 0.5
+
+
+class RedLexer(RegexLexer):
+    """
+    A Red-language lexer.
+    """
+    name = 'Red'
+    aliases = ['red', 'red/system']
+    filenames = ['*.red', '*.reds']
+    mimetypes = ['text/x-red', 'text/x-red-system']
+    url = 'https://www.red-lang.org'
+    version_added = '2.0'
+
+    flags = re.IGNORECASE | re.MULTILINE
+
+    escape_re = r'(?:\^\([0-9a-f]{1,4}\)*)'
+
+    def word_callback(lexer, match):
+        word = match.group()
+
+        if re.match(".*:$", word):
+            yield match.start(), Generic.Subheading, word
+        elif re.match(r'(if|unless|either|any|all|while|until|loop|repeat|'
+                      r'foreach|forall|func|function|does|has|switch|'
+                      r'case|reduce|compose|get|set|print|prin|equal\?|'
+                      r'not-equal\?|strict-equal\?|lesser\?|greater\?|lesser-or-equal\?|'
+                      r'greater-or-equal\?|same\?|not|type\?|stats|'
+                      r'bind|union|replace|charset|routine)$', word):
+            yield match.start(), Name.Builtin, word
+        elif re.match(r'(make|random|reflect|to|form|mold|absolute|add|divide|multiply|negate|'
+                      r'power|remainder|round|subtract|even\?|odd\?|and~|complement|or~|xor~|'
+                      r'append|at|back|change|clear|copy|find|head|head\?|index\?|insert|'
+                      r'length\?|next|pick|poke|remove|reverse|select|sort|skip|swap|tail|tail\?|'
+                      r'take|trim|create|close|delete|modify|open|open\?|query|read|rename|'
+                      r'update|write)$', word):
+            yield match.start(), Name.Function, word
+        elif re.match(r'(yes|on|no|off|true|false|tab|cr|lf|newline|escape|slash|sp|space|null|'
+                      r'none|crlf|dot|null-byte)$', word):
+            yield match.start(), Name.Builtin.Pseudo, word
+        elif re.match(r'(#system-global|#include|#enum|#define|#either|#if|#import|#export|'
+                      r'#switch|#default|#get-definition)$', word):
+            yield match.start(), Keyword.Namespace, word
+        elif re.match(r'(system|halt|quit|quit-return|do|load|q|recycle|call|run|ask|parse|'
+                      r'raise-error|return|exit|break|alias|push|pop|probe|\?\?|spec-of|body-of|'
+                      r'quote|forever)$', word):
+            yield match.start(), Name.Exception, word
+        elif re.match(r'(action\?|block\?|char\?|datatype\?|file\?|function\?|get-path\?|zero\?|'
+                      r'get-word\?|integer\?|issue\?|lit-path\?|lit-word\?|logic\?|native\?|'
+                      r'op\?|paren\?|path\?|refinement\?|set-path\?|set-word\?|string\?|unset\?|'
+                      r'any-struct\?|none\?|word\?|any-series\?)$', word):
+            yield match.start(), Keyword, word
+        elif re.match(r'(JNICALL|stdcall|cdecl|infix)$', word):
+            yield match.start(), Keyword.Namespace, word
+        elif re.match("to-.*", word):
+            yield match.start(), Keyword, word
+        elif re.match(r'(\+|-\*\*|-|\*\*|//|/|\*|and|or|xor|=\?|===|==|=|<>|<=|>=|'
+                      r'<<<|>>>|<<|>>|<|>%)$', word):
+            yield match.start(), Operator, word
+        elif re.match(r".*\!$", word):
+            yield match.start(), Keyword.Type, word
+        elif re.match("'.*", word):
+            yield match.start(), Name.Variable.Instance, word  # lit-word
+        elif re.match("#.*", word):
+            yield match.start(), Name.Label, word  # issue
+        elif re.match("%.*", word):
+            yield match.start(), Name.Decorator, word  # file
+        elif re.match(":.*", word):
+            yield match.start(), Generic.Subheading, word  # get-word
+        else:
+            yield match.start(), Name.Variable, word
+
+    tokens = {
+        'root': [
+            (r'\s+', Text),
+            (r'#"', String.Char, 'char'),
+            (r'#\{[0-9a-f\s]*\}', Number.Hex),
+            (r'2#\{', Number.Hex, 'bin2'),
+            (r'64#\{[0-9a-z+/=\s]*\}', Number.Hex),
+            (r'([0-9a-f]+)(h)((\s)|(?=[\[\]{}"()]))',
+             bygroups(Number.Hex, Name.Variable, Whitespace)),
+            (r'"', String, 'string'),
+            (r'\{', String, 'string2'),
+            (r';#+.*\n', Comment.Special),
+            (r';\*+.*\n', Comment.Preproc),
+            (r';.*\n', Comment),
+            (r'%"', Name.Decorator, 'stringFile'),
+            (r'%[^(^{")\s\[\]]+', Name.Decorator),
+            (r'[+-]?([a-z]{1,3})?\$\d+(\.\d+)?', Number.Float),  # money
+            (r'[+-]?\d+\:\d+(\:\d+)?(\.\d+)?', String.Other),    # time
+            (r'\d+[\-/][0-9a-z]+[\-/]\d+(/\d+:\d+((:\d+)?'
+             r'([\.\d+]?([+-]?\d+:\d+)?)?)?)?', String.Other),   # date
+            (r'\d+(\.\d+)+\.\d+', Keyword.Constant),             # tuple
+            (r'\d+X\d+', Keyword.Constant),                   # pair
+            (r'[+-]?\d+(\'\d+)?([.,]\d*)?E[+-]?\d+', Number.Float),
+            (r'[+-]?\d+(\'\d+)?[.,]\d*', Number.Float),
+            (r'[+-]?\d+(\'\d+)?', Number),
+            (r'[\[\]()]', Generic.Strong),
+            (r'[a-z]+[^(^{"\s:)]*://[^(^{"\s)]*', Name.Decorator),  # url
+            (r'mailto:[^(^{"@\s)]+@[^(^{"@\s)]+', Name.Decorator),  # url
+            (r'[^(^{"@\s)]+@[^(^{"@\s)]+', Name.Decorator),         # email
+            (r'comment\s"', Comment, 'commentString1'),
+            (r'comment\s\{', Comment, 'commentString2'),
+            (r'comment\s\[', Comment, 'commentBlock'),
+            (r'comment\s[^(\s{"\[]+', Comment),
+            (r'/[^(^{^")\s/[\]]*', Name.Attribute),
+            (r'([^(^{^")\s/[\]]+)(?=[:({"\s/\[\]])', word_callback),
+            (r'<[\w:.-]*>', Name.Tag),
+            (r'<[^(<>\s")]+', Name.Tag, 'tag'),
+            (r'([^(^{")\s]+)', Text),
+        ],
+        'string': [
+            (r'[^(^")]+', String),
+            (escape_re, String.Escape),
+            (r'[(|)]+', String),
+            (r'\^.', String.Escape),
+            (r'"', String, '#pop'),
+        ],
+        'string2': [
+            (r'[^(^{})]+', String),
+            (escape_re, String.Escape),
+            (r'[(|)]+', String),
+            (r'\^.', String.Escape),
+            (r'\{', String, '#push'),
+            (r'\}', String, '#pop'),
+        ],
+        'stringFile': [
+            (r'[^(^")]+', Name.Decorator),
+            (escape_re, Name.Decorator),
+            (r'\^.', Name.Decorator),
+            (r'"', Name.Decorator, '#pop'),
+        ],
+        'char': [
+            (escape_re + '"', String.Char, '#pop'),
+            (r'\^."', String.Char, '#pop'),
+            (r'."', String.Char, '#pop'),
+        ],
+        'tag': [
+            (escape_re, Name.Tag),
+            (r'"', Name.Tag, 'tagString'),
+            (r'[^(<>\r\n")]+', Name.Tag),
+            (r'>', Name.Tag, '#pop'),
+        ],
+        'tagString': [
+            (r'[^(^")]+', Name.Tag),
+            (escape_re, Name.Tag),
+            (r'[(|)]+', Name.Tag),
+            (r'\^.', Name.Tag),
+            (r'"', Name.Tag, '#pop'),
+        ],
+        'tuple': [
+            (r'(\d+\.)+', Keyword.Constant),
+            (r'\d+', Keyword.Constant, '#pop'),
+        ],
+        'bin2': [
+            (r'\s+', Number.Hex),
+            (r'([01]\s*){8}', Number.Hex),
+            (r'\}', Number.Hex, '#pop'),
+        ],
+        'commentString1': [
+            (r'[^(^")]+', Comment),
+            (escape_re, Comment),
+            (r'[(|)]+', Comment),
+            (r'\^.', Comment),
+            (r'"', Comment, '#pop'),
+        ],
+        'commentString2': [
+            (r'[^(^{})]+', Comment),
+            (escape_re, Comment),
+            (r'[(|)]+', Comment),
+            (r'\^.', Comment),
+            (r'\{', Comment, '#push'),
+            (r'\}', Comment, '#pop'),
+        ],
+        'commentBlock': [
+            (r'\[', Comment, '#push'),
+            (r'\]', Comment, '#pop'),
+            (r'"', Comment, "commentString1"),
+            (r'\{', Comment, "commentString2"),
+            (r'[^(\[\]"{)]+', Comment),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rego.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rego.py
new file mode 100644
index 00000000..6f2e3e9e
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rego.py
@@ -0,0 +1,57 @@
+"""
+    pygments.lexers.rego
+    ~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for the Rego policy languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, words
+from pygments.token import Comment, Operator, Keyword, Name, String, Number, Punctuation, Whitespace
+
+class RegoLexer(RegexLexer):
+    """
+    For Rego source.
+    """
+    name = 'Rego'
+    url = 'https://www.openpolicyagent.org/docs/latest/policy-language/'
+    filenames = ['*.rego']
+    aliases = ['rego']
+    mimetypes = ['text/x-rego']
+    version_added = '2.19'
+
+    reserved_words = (
+        'as', 'contains', 'data', 'default', 'else', 'every', 'false',
+        'if', 'in', 'import', 'package', 'not', 'null',
+        'some', 'true', 'with'
+    )
+
+    builtins = (
+        # https://www.openpolicyagent.org/docs/latest/philosophy/#the-opa-document-model
+        'data',  # Global variable for accessing base and virtual documents
+        'input', # Represents synchronously pushed base documents
+    )
+
+    tokens = {
+        'root': [
+            (r'\n', Whitespace),
+            (r'\s+', Whitespace),
+            (r'#.*?$', Comment.Single),
+            (words(reserved_words, suffix=r'\b'), Keyword),
+            (words(builtins, suffix=r'\b'), Name.Builtin),
+            (r'[a-zA-Z_][a-zA-Z0-9_]*', Name),
+            (r'"(\\\\|\\"|[^"])*"', String.Double),
+            (r'`[^`]*`', String.Backtick),
+            (r'-?\d+(\.\d+)?', Number),
+            (r'(==|!=|<=|>=|:=)', Operator),  # Compound operators
+            (r'[=<>+\-*/%&|]', Operator),     # Single-character operators
+            (r'[\[\]{}(),.:;]', Punctuation),
+        ]
+    }
+
+__all__ = ['RegoLexer']
+
+
+
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/resource.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/resource.py
new file mode 100644
index 00000000..9593c212
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/resource.py
@@ -0,0 +1,83 @@
+"""
+    pygments.lexers.resource
+    ~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexer for resource definition files.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, bygroups, words
+from pygments.token import Comment, String, Number, Operator, Text, \
+    Keyword, Name
+
+__all__ = ['ResourceLexer']
+
+
+class ResourceLexer(RegexLexer):
+    """Lexer for ICU Resource bundles.
+    """
+    name = 'ResourceBundle'
+    aliases = ['resourcebundle', 'resource']
+    filenames = []
+    url = 'https://unicode-org.github.io/icu/userguide/locale/resources.html'
+    version_added = '2.0'
+
+    _types = (':table', ':array', ':string', ':bin', ':import', ':intvector',
+              ':int', ':alias')
+
+    flags = re.MULTILINE | re.IGNORECASE
+    tokens = {
+        'root': [
+            (r'//.*?$', Comment),
+            (r'"', String, 'string'),
+            (r'-?\d+', Number.Integer),
+            (r'[,{}]', Operator),
+            (r'([^\s{{:]+)(\s*)({}?)'.format('|'.join(_types)),
+             bygroups(Name, Text, Keyword)),
+            (r'\s+', Text),
+            (words(_types), Keyword),
+        ],
+        'string': [
+            (r'(\\x[0-9a-f]{2}|\\u[0-9a-f]{4}|\\U00[0-9a-f]{6}|'
+             r'\\[0-7]{1,3}|\\c.|\\[abtnvfre\'"?\\]|\\\{|[^"{\\])+', String),
+            (r'\{', String.Escape, 'msgname'),
+            (r'"', String, '#pop')
+        ],
+        'msgname': [
+            (r'([^{},]+)(\s*)', bygroups(Name, String.Escape), ('#pop', 'message'))
+        ],
+        'message': [
+            (r'\{', String.Escape, 'msgname'),
+            (r'\}', String.Escape, '#pop'),
+            (r'(,)(\s*)([a-z]+)(\s*\})',
+             bygroups(Operator, String.Escape, Keyword, String.Escape), '#pop'),
+            (r'(,)(\s*)([a-z]+)(\s*)(,)(\s*)(offset)(\s*)(:)(\s*)(-?\d+)(\s*)',
+             bygroups(Operator, String.Escape, Keyword, String.Escape, Operator,
+                      String.Escape, Operator.Word, String.Escape, Operator,
+                      String.Escape, Number.Integer, String.Escape), 'choice'),
+            (r'(,)(\s*)([a-z]+)(\s*)(,)(\s*)',
+             bygroups(Operator, String.Escape, Keyword, String.Escape, Operator,
+                      String.Escape), 'choice'),
+            (r'\s+', String.Escape)
+        ],
+        'choice': [
+            (r'(=|<|>|<=|>=|!=)(-?\d+)(\s*\{)',
+             bygroups(Operator, Number.Integer, String.Escape), 'message'),
+            (r'([a-z]+)(\s*\{)', bygroups(Keyword.Type, String.Escape), 'str'),
+            (r'\}', String.Escape, ('#pop', '#pop')),
+            (r'\s+', String.Escape)
+        ],
+        'str': [
+            (r'\}', String.Escape, '#pop'),
+            (r'\{', String.Escape, 'msgname'),
+            (r'[^{}]+', String)
+        ]
+    }
+
+    def analyse_text(text):
+        if text.startswith('root:table'):
+            return 1.0
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/ride.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/ride.py
new file mode 100644
index 00000000..4d60c29c
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/ride.py
@@ -0,0 +1,138 @@
+"""
+    pygments.lexers.ride
+    ~~~~~~~~~~~~~~~~~~~~
+
+    Lexer for the Ride programming language.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, words, include
+from pygments.token import Comment, Keyword, Name, Number, Punctuation, \
+    String, Text
+
+__all__ = ['RideLexer']
+
+
+class RideLexer(RegexLexer):
+    """
+    For Ride source code.
+    """
+
+    name = 'Ride'
+    aliases = ['ride']
+    filenames = ['*.ride']
+    mimetypes = ['text/x-ride']
+    url = 'https://docs.waves.tech/en/ride'
+    version_added = '2.6'
+
+    validName = r'[a-zA-Z_][a-zA-Z0-9_\']*'
+
+    builtinOps = (
+        '||', '|', '>=', '>', '==', '!',
+        '=', '<=', '<', '::', ':+', ':', '!=', '/',
+        '.', '=>', '-', '+', '*', '&&', '%', '++',
+    )
+
+    globalVariablesName = (
+        'NOALG', 'MD5', 'SHA1', 'SHA224', 'SHA256', 'SHA384', 'SHA512',
+        'SHA3224', 'SHA3256', 'SHA3384', 'SHA3512', 'nil', 'this', 'unit',
+        'height', 'lastBlock', 'Buy', 'Sell', 'CEILING', 'FLOOR', 'DOWN',
+        'HALFDOWN', 'HALFEVEN', 'HALFUP', 'UP',
+    )
+
+    typesName = (
+        'Unit', 'Int', 'Boolean', 'ByteVector', 'String', 'Address', 'Alias',
+        'Transfer', 'AssetPair', 'DataEntry', 'Order', 'Transaction',
+        'GenesisTransaction', 'PaymentTransaction', 'ReissueTransaction',
+        'BurnTransaction', 'MassTransferTransaction', 'ExchangeTransaction',
+        'TransferTransaction', 'SetAssetScriptTransaction',
+        'InvokeScriptTransaction', 'IssueTransaction', 'LeaseTransaction',
+        'LeaseCancelTransaction', 'CreateAliasTransaction',
+        'SetScriptTransaction', 'SponsorFeeTransaction', 'DataTransaction',
+        'WriteSet', 'AttachedPayment', 'ScriptTransfer', 'TransferSet',
+        'ScriptResult', 'Invocation', 'Asset', 'BlockInfo', 'Issue', 'Reissue',
+        'Burn', 'NoAlg', 'Md5', 'Sha1', 'Sha224', 'Sha256', 'Sha384', 'Sha512',
+        'Sha3224', 'Sha3256', 'Sha3384', 'Sha3512', 'BinaryEntry',
+        'BooleanEntry', 'IntegerEntry', 'StringEntry', 'List', 'Ceiling',
+        'Down', 'Floor', 'HalfDown', 'HalfEven', 'HalfUp', 'Up',
+    )
+
+    functionsName = (
+        'fraction', 'size', 'toBytes', 'take', 'drop', 'takeRight', 'dropRight',
+        'toString', 'isDefined', 'extract', 'throw', 'getElement', 'value',
+        'cons', 'toUtf8String', 'toInt', 'indexOf', 'lastIndexOf', 'split',
+        'parseInt', 'parseIntValue', 'keccak256', 'blake2b256', 'sha256',
+        'sigVerify', 'toBase58String', 'fromBase58String', 'toBase64String',
+        'fromBase64String', 'transactionById', 'transactionHeightById',
+        'getInteger', 'getBoolean', 'getBinary', 'getString',
+        'addressFromPublicKey', 'addressFromString', 'addressFromRecipient',
+        'assetBalance', 'wavesBalance', 'getIntegerValue', 'getBooleanValue',
+        'getBinaryValue', 'getStringValue', 'addressFromStringValue',
+        'assetInfo', 'rsaVerify', 'checkMerkleProof', 'median',
+        'valueOrElse', 'valueOrErrorMessage', 'contains', 'log', 'pow',
+        'toBase16String', 'fromBase16String', 'blockInfoByHeight',
+        'transferTransactionById',
+    )
+
+    reservedWords = words((
+        'match', 'case', 'else', 'func', 'if',
+        'let', 'then', '@Callable', '@Verifier',
+    ), suffix=r'\b')
+
+    tokens = {
+        'root': [
+            # Comments
+            (r'#.*', Comment.Single),
+            # Whitespace
+            (r'\s+', Text),
+            # Strings
+            (r'"', String, 'doublequote'),
+            (r'utf8\'', String, 'utf8quote'),
+            (r'base(58|64|16)\'', String, 'singlequote'),
+            # Keywords
+            (reservedWords, Keyword.Reserved),
+            (r'\{-#.*?#-\}', Keyword.Reserved),
+            (r'FOLD<\d+>', Keyword.Reserved),
+            # Types
+            (words(typesName), Keyword.Type),
+            # Main
+            # (specialName, Keyword.Reserved),
+            # Prefix Operators
+            (words(builtinOps, prefix=r'\(', suffix=r'\)'), Name.Function),
+            # Infix Operators
+            (words(builtinOps), Name.Function),
+            (words(globalVariablesName), Name.Function),
+            (words(functionsName), Name.Function),
+            # Numbers
+            include('numbers'),
+            # Variable Names
+            (validName, Name.Variable),
+            # Parens
+            (r'[,()\[\]{}]', Punctuation),
+        ],
+
+        'doublequote': [
+            (r'\\u[0-9a-fA-F]{4}', String.Escape),
+            (r'\\[nrfvb\\"]', String.Escape),
+            (r'[^"]', String),
+            (r'"', String, '#pop'),
+        ],
+
+        'utf8quote': [
+            (r'\\u[0-9a-fA-F]{4}', String.Escape),
+            (r'\\[nrfvb\\\']', String.Escape),
+            (r'[^\']', String),
+            (r'\'', String, '#pop'),
+        ],
+
+        'singlequote': [
+            (r'[^\']', String),
+            (r'\'', String, '#pop'),
+        ],
+
+        'numbers': [
+            (r'_?\d+', Number.Integer),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rita.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rita.py
new file mode 100644
index 00000000..536aafff
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rita.py
@@ -0,0 +1,42 @@
+"""
+    pygments.lexers.rita
+    ~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for RITA language
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer
+from pygments.token import Comment, Operator, Keyword, Name, Literal, \
+    Punctuation, Whitespace
+
+__all__ = ['RitaLexer']
+
+
+class RitaLexer(RegexLexer):
+    """
+    Lexer for RITA.
+    """
+    name = 'Rita'
+    url = 'https://github.com/zaibacu/rita-dsl'
+    filenames = ['*.rita']
+    aliases = ['rita']
+    mimetypes = ['text/rita']
+    version_added = '2.11'
+
+    tokens = {
+        'root': [
+            (r'\n', Whitespace),
+            (r'\s+', Whitespace),
+            (r'#(.*?)\n', Comment.Single),
+            (r'@(.*?)\n', Operator),  # Yes, whole line as an operator
+            (r'"(\w|\d|\s|(\\")|[\'_\-./,\?\!])+?"', Literal),
+            (r'\'(\w|\d|\s|(\\\')|["_\-./,\?\!])+?\'', Literal),
+            (r'([A-Z_]+)', Keyword),
+            (r'([a-z0-9_]+)', Name),
+            (r'((->)|[!?+*|=])', Operator),
+            (r'[\(\),\{\}]', Punctuation)
+        ]
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rnc.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rnc.py
new file mode 100644
index 00000000..b7a06bb9
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rnc.py
@@ -0,0 +1,66 @@
+"""
+    pygments.lexers.rnc
+    ~~~~~~~~~~~~~~~~~~~
+
+    Lexer for Relax-NG Compact syntax
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Punctuation
+
+__all__ = ['RNCCompactLexer']
+
+
+class RNCCompactLexer(RegexLexer):
+    """
+    For RelaxNG-compact syntax.
+    """
+
+    name = 'Relax-NG Compact'
+    url = 'http://relaxng.org'
+    aliases = ['rng-compact', 'rnc']
+    filenames = ['*.rnc']
+    version_added = '2.2'
+
+    tokens = {
+        'root': [
+            (r'namespace\b', Keyword.Namespace),
+            (r'(?:default|datatypes)\b', Keyword.Declaration),
+            (r'##.*$', Comment.Preproc),
+            (r'#.*$', Comment.Single),
+            (r'"[^"]*"', String.Double),
+            # TODO single quoted strings and escape sequences outside of
+            # double-quoted strings
+            (r'(?:element|attribute|mixed)\b', Keyword.Declaration, 'variable'),
+            (r'(text\b|xsd:[^ ]+)', Keyword.Type, 'maybe_xsdattributes'),
+            (r'[,?&*=|~]|>>', Operator),
+            (r'[(){}]', Punctuation),
+            (r'.', Text),
+        ],
+
+        # a variable has been declared using `element` or `attribute`
+        'variable': [
+            (r'[^{]+', Name.Variable),
+            (r'\{', Punctuation, '#pop'),
+        ],
+
+        # after an xsd: declaration there may be attributes
+        'maybe_xsdattributes': [
+            (r'\{', Punctuation, 'xsdattributes'),
+            (r'\}', Punctuation, '#pop'),
+            (r'.', Text),
+        ],
+
+        # attributes take the form { key1 = value1 key2 = value2 ... }
+        'xsdattributes': [
+            (r'[^ =}]', Name.Attribute),
+            (r'=', Operator),
+            (r'"[^"]*"', String.Double),
+            (r'\}', Punctuation, '#pop'),
+            (r'.', Text),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/roboconf.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/roboconf.py
new file mode 100644
index 00000000..31adba9f
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/roboconf.py
@@ -0,0 +1,81 @@
+"""
+    pygments.lexers.roboconf
+    ~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for Roboconf DSL.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, words, re
+from pygments.token import Text, Operator, Keyword, Name, Comment
+
+__all__ = ['RoboconfGraphLexer', 'RoboconfInstancesLexer']
+
+
+class RoboconfGraphLexer(RegexLexer):
+    """
+    Lexer for Roboconf graph files.
+    """
+    name = 'Roboconf Graph'
+    aliases = ['roboconf-graph']
+    filenames = ['*.graph']
+    url = 'https://roboconf.github.io/en/user-guide/graph-definition.html'
+    version_added = '2.1'
+
+    flags = re.IGNORECASE | re.MULTILINE
+    tokens = {
+        'root': [
+            # Skip white spaces
+            (r'\s+', Text),
+
+            # There is one operator
+            (r'=', Operator),
+
+            # Keywords
+            (words(('facet', 'import'), suffix=r'\s*\b', prefix=r'\b'), Keyword),
+            (words((
+                'installer', 'extends', 'exports', 'imports', 'facets',
+                'children'), suffix=r'\s*:?', prefix=r'\b'), Name),
+
+            # Comments
+            (r'#.*\n', Comment),
+
+            # Default
+            (r'[^#]', Text),
+            (r'.*\n', Text)
+        ]
+    }
+
+
+class RoboconfInstancesLexer(RegexLexer):
+    """
+    Lexer for Roboconf instances files.
+    """
+    name = 'Roboconf Instances'
+    aliases = ['roboconf-instances']
+    filenames = ['*.instances']
+    url = 'https://roboconf.github.io'
+    version_added = '2.1'
+
+    flags = re.IGNORECASE | re.MULTILINE
+    tokens = {
+        'root': [
+
+            # Skip white spaces
+            (r'\s+', Text),
+
+            # Keywords
+            (words(('instance of', 'import'), suffix=r'\s*\b', prefix=r'\b'), Keyword),
+            (words(('name', 'count'), suffix=r's*:?', prefix=r'\b'), Name),
+            (r'\s*[\w.-]+\s*:', Name),
+
+            # Comments
+            (r'#.*\n', Comment),
+
+            # Default
+            (r'[^#]', Text),
+            (r'.*\n', Text)
+        ]
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/robotframework.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/robotframework.py
new file mode 100644
index 00000000..f92d5675
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/robotframework.py
@@ -0,0 +1,551 @@
+"""
+    pygments.lexers.robotframework
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexer for Robot Framework.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+#  Copyright 2012 Nokia Siemens Networks Oyj
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import re
+
+from pygments.lexer import Lexer
+from pygments.token import Token
+
+__all__ = ['RobotFrameworkLexer']
+
+
+HEADING = Token.Generic.Heading
+SETTING = Token.Keyword.Namespace
+IMPORT = Token.Name.Namespace
+TC_KW_NAME = Token.Generic.Subheading
+KEYWORD = Token.Name.Function
+ARGUMENT = Token.String
+VARIABLE = Token.Name.Variable
+COMMENT = Token.Comment
+SEPARATOR = Token.Punctuation
+SYNTAX = Token.Punctuation
+GHERKIN = Token.Generic.Emph
+ERROR = Token.Error
+
+
+def normalize(string, remove=''):
+    string = string.lower()
+    for char in remove + ' ':
+        if char in string:
+            string = string.replace(char, '')
+    return string
+
+
+class RobotFrameworkLexer(Lexer):
+    """
+    For Robot Framework test data.
+
+    Supports both space and pipe separated plain text formats.
+    """
+    name = 'RobotFramework'
+    url = 'http://robotframework.org'
+    aliases = ['robotframework']
+    filenames = ['*.robot', '*.resource']
+    mimetypes = ['text/x-robotframework']
+    version_added = '1.6'
+
+    def __init__(self, **options):
+        options['tabsize'] = 2
+        options['encoding'] = 'UTF-8'
+        Lexer.__init__(self, **options)
+
+    def get_tokens_unprocessed(self, text):
+        row_tokenizer = RowTokenizer()
+        var_tokenizer = VariableTokenizer()
+        index = 0
+        for row in text.splitlines():
+            for value, token in row_tokenizer.tokenize(row):
+                for value, token in var_tokenizer.tokenize(value, token):
+                    if value:
+                        yield index, token, str(value)
+                        index += len(value)
+
+
+class VariableTokenizer:
+
+    def tokenize(self, string, token):
+        var = VariableSplitter(string, identifiers='$@%&')
+        if var.start < 0 or token in (COMMENT, ERROR):
+            yield string, token
+            return
+        for value, token in self._tokenize(var, string, token):
+            if value:
+                yield value, token
+
+    def _tokenize(self, var, string, orig_token):
+        before = string[:var.start]
+        yield before, orig_token
+        yield var.identifier + '{', SYNTAX
+        yield from self.tokenize(var.base, VARIABLE)
+        yield '}', SYNTAX
+        if var.index is not None:
+            yield '[', SYNTAX
+            yield from self.tokenize(var.index, VARIABLE)
+            yield ']', SYNTAX
+        yield from self.tokenize(string[var.end:], orig_token)
+
+
+class RowTokenizer:
+
+    def __init__(self):
+        self._table = UnknownTable()
+        self._splitter = RowSplitter()
+        testcases = TestCaseTable()
+        settings = SettingTable(testcases.set_default_template)
+        variables = VariableTable()
+        keywords = KeywordTable()
+        self._tables = {'settings': settings, 'setting': settings,
+                        'metadata': settings,
+                        'variables': variables, 'variable': variables,
+                        'testcases': testcases, 'testcase': testcases,
+                        'tasks': testcases, 'task': testcases,
+                        'keywords': keywords, 'keyword': keywords,
+                        'userkeywords': keywords, 'userkeyword': keywords}
+
+    def tokenize(self, row):
+        commented = False
+        heading = False
+        for index, value in enumerate(self._splitter.split(row)):
+            # First value, and every second after that, is a separator.
+            index, separator = divmod(index-1, 2)
+            if value.startswith('#'):
+                commented = True
+            elif index == 0 and value.startswith('*'):
+                self._table = self._start_table(value)
+                heading = True
+            yield from self._tokenize(value, index, commented,
+                                      separator, heading)
+        self._table.end_row()
+
+    def _start_table(self, header):
+        name = normalize(header, remove='*')
+        return self._tables.get(name, UnknownTable())
+
+    def _tokenize(self, value, index, commented, separator, heading):
+        if commented:
+            yield value, COMMENT
+        elif separator:
+            yield value, SEPARATOR
+        elif heading:
+            yield value, HEADING
+        else:
+            yield from self._table.tokenize(value, index)
+
+
+class RowSplitter:
+    _space_splitter = re.compile('( {2,})')
+    _pipe_splitter = re.compile(r'((?:^| +)\|(?: +|$))')
+
+    def split(self, row):
+        splitter = (row.startswith('| ') and self._split_from_pipes
+                    or self._split_from_spaces)
+        yield from splitter(row)
+        yield '\n'
+
+    def _split_from_spaces(self, row):
+        yield ''  # Start with (pseudo)separator similarly as with pipes
+        yield from self._space_splitter.split(row)
+
+    def _split_from_pipes(self, row):
+        _, separator, rest = self._pipe_splitter.split(row, 1)
+        yield separator
+        while self._pipe_splitter.search(rest):
+            cell, separator, rest = self._pipe_splitter.split(rest, 1)
+            yield cell
+            yield separator
+        yield rest
+
+
+class Tokenizer:
+    _tokens = None
+
+    def __init__(self):
+        self._index = 0
+
+    def tokenize(self, value):
+        values_and_tokens = self._tokenize(value, self._index)
+        self._index += 1
+        if isinstance(values_and_tokens, type(Token)):
+            values_and_tokens = [(value, values_and_tokens)]
+        return values_and_tokens
+
+    def _tokenize(self, value, index):
+        index = min(index, len(self._tokens) - 1)
+        return self._tokens[index]
+
+    def _is_assign(self, value):
+        if value.endswith('='):
+            value = value[:-1].strip()
+        var = VariableSplitter(value, identifiers='$@&')
+        return var.start == 0 and var.end == len(value)
+
+
+class Comment(Tokenizer):
+    _tokens = (COMMENT,)
+
+
+class Setting(Tokenizer):
+    _tokens = (SETTING, ARGUMENT)
+    _keyword_settings = ('suitesetup', 'suiteprecondition', 'suiteteardown',
+                         'suitepostcondition', 'testsetup', 'tasksetup', 'testprecondition',
+                         'testteardown','taskteardown', 'testpostcondition', 'testtemplate', 'tasktemplate')
+    _import_settings = ('library', 'resource', 'variables')
+    _other_settings = ('documentation', 'metadata', 'forcetags', 'defaulttags',
+                       'testtimeout','tasktimeout')
+    _custom_tokenizer = None
+
+    def __init__(self, template_setter=None):
+        Tokenizer.__init__(self)
+        self._template_setter = template_setter
+
+    def _tokenize(self, value, index):
+        if index == 1 and self._template_setter:
+            self._template_setter(value)
+        if index == 0:
+            normalized = normalize(value)
+            if normalized in self._keyword_settings:
+                self._custom_tokenizer = KeywordCall(support_assign=False)
+            elif normalized in self._import_settings:
+                self._custom_tokenizer = ImportSetting()
+            elif normalized not in self._other_settings:
+                return ERROR
+        elif self._custom_tokenizer:
+            return self._custom_tokenizer.tokenize(value)
+        return Tokenizer._tokenize(self, value, index)
+
+
+class ImportSetting(Tokenizer):
+    _tokens = (IMPORT, ARGUMENT)
+
+
+class TestCaseSetting(Setting):
+    _keyword_settings = ('setup', 'precondition', 'teardown', 'postcondition',
+                         'template')
+    _import_settings = ()
+    _other_settings = ('documentation', 'tags', 'timeout')
+
+    def _tokenize(self, value, index):
+        if index == 0:
+            type = Setting._tokenize(self, value[1:-1], index)
+            return [('[', SYNTAX), (value[1:-1], type), (']', SYNTAX)]
+        return Setting._tokenize(self, value, index)
+
+
+class KeywordSetting(TestCaseSetting):
+    _keyword_settings = ('teardown',)
+    _other_settings = ('documentation', 'arguments', 'return', 'timeout', 'tags')
+
+
+class Variable(Tokenizer):
+    _tokens = (SYNTAX, ARGUMENT)
+
+    def _tokenize(self, value, index):
+        if index == 0 and not self._is_assign(value):
+            return ERROR
+        return Tokenizer._tokenize(self, value, index)
+
+
+class KeywordCall(Tokenizer):
+    _tokens = (KEYWORD, ARGUMENT)
+
+    def __init__(self, support_assign=True):
+        Tokenizer.__init__(self)
+        self._keyword_found = not support_assign
+        self._assigns = 0
+
+    def _tokenize(self, value, index):
+        if not self._keyword_found and self._is_assign(value):
+            self._assigns += 1
+            return SYNTAX  # VariableTokenizer tokenizes this later.
+        if self._keyword_found:
+            return Tokenizer._tokenize(self, value, index - self._assigns)
+        self._keyword_found = True
+        return GherkinTokenizer().tokenize(value, KEYWORD)
+
+
+class GherkinTokenizer:
+    _gherkin_prefix = re.compile('^(Given|When|Then|And|But) ', re.IGNORECASE)
+
+    def tokenize(self, value, token):
+        match = self._gherkin_prefix.match(value)
+        if not match:
+            return [(value, token)]
+        end = match.end()
+        return [(value[:end], GHERKIN), (value[end:], token)]
+
+
+class TemplatedKeywordCall(Tokenizer):
+    _tokens = (ARGUMENT,)
+
+
+class ForLoop(Tokenizer):
+
+    def __init__(self):
+        Tokenizer.__init__(self)
+        self._in_arguments = False
+
+    def _tokenize(self, value, index):
+        token = self._in_arguments and ARGUMENT or SYNTAX
+        if value.upper() in ('IN', 'IN RANGE'):
+            self._in_arguments = True
+        return token
+
+
+class _Table:
+    _tokenizer_class = None
+
+    def __init__(self, prev_tokenizer=None):
+        self._tokenizer = self._tokenizer_class()
+        self._prev_tokenizer = prev_tokenizer
+        self._prev_values_on_row = []
+
+    def tokenize(self, value, index):
+        if self._continues(value, index):
+            self._tokenizer = self._prev_tokenizer
+            yield value, SYNTAX
+        else:
+            yield from self._tokenize(value, index)
+        self._prev_values_on_row.append(value)
+
+    def _continues(self, value, index):
+        return value == '...' and all(self._is_empty(t)
+                                      for t in self._prev_values_on_row)
+
+    def _is_empty(self, value):
+        return value in ('', '\\')
+
+    def _tokenize(self, value, index):
+        return self._tokenizer.tokenize(value)
+
+    def end_row(self):
+        self.__init__(prev_tokenizer=self._tokenizer)
+
+
+class UnknownTable(_Table):
+    _tokenizer_class = Comment
+
+    def _continues(self, value, index):
+        return False
+
+
+class VariableTable(_Table):
+    _tokenizer_class = Variable
+
+
+class SettingTable(_Table):
+    _tokenizer_class = Setting
+
+    def __init__(self, template_setter, prev_tokenizer=None):
+        _Table.__init__(self, prev_tokenizer)
+        self._template_setter = template_setter
+
+    def _tokenize(self, value, index):
+        if index == 0 and normalize(value) == 'testtemplate':
+            self._tokenizer = Setting(self._template_setter)
+        return _Table._tokenize(self, value, index)
+
+    def end_row(self):
+        self.__init__(self._template_setter, prev_tokenizer=self._tokenizer)
+
+
+class TestCaseTable(_Table):
+    _setting_class = TestCaseSetting
+    _test_template = None
+    _default_template = None
+
+    @property
+    def _tokenizer_class(self):
+        if self._test_template or (self._default_template and
+                                   self._test_template is not False):
+            return TemplatedKeywordCall
+        return KeywordCall
+
+    def _continues(self, value, index):
+        return index > 0 and _Table._continues(self, value, index)
+
+    def _tokenize(self, value, index):
+        if index == 0:
+            if value:
+                self._test_template = None
+            return GherkinTokenizer().tokenize(value, TC_KW_NAME)
+        if index == 1 and self._is_setting(value):
+            if self._is_template(value):
+                self._test_template = False
+                self._tokenizer = self._setting_class(self.set_test_template)
+            else:
+                self._tokenizer = self._setting_class()
+        if index == 1 and self._is_for_loop(value):
+            self._tokenizer = ForLoop()
+        if index == 1 and self._is_empty(value):
+            return [(value, SYNTAX)]
+        return _Table._tokenize(self, value, index)
+
+    def _is_setting(self, value):
+        return value.startswith('[') and value.endswith(']')
+
+    def _is_template(self, value):
+        return normalize(value) == '[template]'
+
+    def _is_for_loop(self, value):
+        return value.startswith(':') and normalize(value, remove=':') == 'for'
+
+    def set_test_template(self, template):
+        self._test_template = self._is_template_set(template)
+
+    def set_default_template(self, template):
+        self._default_template = self._is_template_set(template)
+
+    def _is_template_set(self, template):
+        return normalize(template) not in ('', '\\', 'none', '${empty}')
+
+
+class KeywordTable(TestCaseTable):
+    _tokenizer_class = KeywordCall
+    _setting_class = KeywordSetting
+
+    def _is_template(self, value):
+        return False
+
+
+# Following code copied directly from Robot Framework 2.7.5.
+
+class VariableSplitter:
+
+    def __init__(self, string, identifiers):
+        self.identifier = None
+        self.base = None
+        self.index = None
+        self.start = -1
+        self.end = -1
+        self._identifiers = identifiers
+        self._may_have_internal_variables = False
+        try:
+            self._split(string)
+        except ValueError:
+            pass
+        else:
+            self._finalize()
+
+    def get_replaced_base(self, variables):
+        if self._may_have_internal_variables:
+            return variables.replace_string(self.base)
+        return self.base
+
+    def _finalize(self):
+        self.identifier = self._variable_chars[0]
+        self.base = ''.join(self._variable_chars[2:-1])
+        self.end = self.start + len(self._variable_chars)
+        if self._has_list_or_dict_variable_index():
+            self.index = ''.join(self._list_and_dict_variable_index_chars[1:-1])
+            self.end += len(self._list_and_dict_variable_index_chars)
+
+    def _has_list_or_dict_variable_index(self):
+        return self._list_and_dict_variable_index_chars\
+        and self._list_and_dict_variable_index_chars[-1] == ']'
+
+    def _split(self, string):
+        start_index, max_index = self._find_variable(string)
+        self.start = start_index
+        self._open_curly = 1
+        self._state = self._variable_state
+        self._variable_chars = [string[start_index], '{']
+        self._list_and_dict_variable_index_chars = []
+        self._string = string
+        start_index += 2
+        for index, char in enumerate(string[start_index:]):
+            index += start_index  # Giving start to enumerate only in Py 2.6+
+            try:
+                self._state(char, index)
+            except StopIteration:
+                return
+            if index  == max_index and not self._scanning_list_variable_index():
+                return
+
+    def _scanning_list_variable_index(self):
+        return self._state in [self._waiting_list_variable_index_state,
+                               self._list_variable_index_state]
+
+    def _find_variable(self, string):
+        max_end_index = string.rfind('}')
+        if max_end_index == -1:
+            raise ValueError('No variable end found')
+        if self._is_escaped(string, max_end_index):
+            return self._find_variable(string[:max_end_index])
+        start_index = self._find_start_index(string, 1, max_end_index)
+        if start_index == -1:
+            raise ValueError('No variable start found')
+        return start_index, max_end_index
+
+    def _find_start_index(self, string, start, end):
+        index = string.find('{', start, end) - 1
+        if index < 0:
+            return -1
+        if self._start_index_is_ok(string, index):
+            return index
+        return self._find_start_index(string, index+2, end)
+
+    def _start_index_is_ok(self, string, index):
+        return string[index] in self._identifiers\
+        and not self._is_escaped(string, index)
+
+    def _is_escaped(self, string, index):
+        escaped = False
+        while index > 0 and string[index-1] == '\\':
+            index -= 1
+            escaped = not escaped
+        return escaped
+
+    def _variable_state(self, char, index):
+        self._variable_chars.append(char)
+        if char == '}' and not self._is_escaped(self._string, index):
+            self._open_curly -= 1
+            if self._open_curly == 0:
+                if not self._is_list_or_dict_variable():
+                    raise StopIteration
+                self._state = self._waiting_list_variable_index_state
+        elif char in self._identifiers:
+            self._state = self._internal_variable_start_state
+
+    def _is_list_or_dict_variable(self):
+        return self._variable_chars[0] in ('@','&')
+
+    def _internal_variable_start_state(self, char, index):
+        self._state = self._variable_state
+        if char == '{':
+            self._variable_chars.append(char)
+            self._open_curly += 1
+            self._may_have_internal_variables = True
+        else:
+            self._variable_state(char, index)
+
+    def _waiting_list_variable_index_state(self, char, index):
+        if char != '[':
+            raise StopIteration
+        self._list_and_dict_variable_index_chars.append(char)
+        self._state = self._list_variable_index_state
+
+    def _list_variable_index_state(self, char, index):
+        self._list_and_dict_variable_index_chars.append(char)
+        if char == ']':
+            raise StopIteration
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/ruby.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/ruby.py
new file mode 100644
index 00000000..72aaeb5f
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/ruby.py
@@ -0,0 +1,518 @@
+"""
+    pygments.lexers.ruby
+    ~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for Ruby and related languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import Lexer, RegexLexer, ExtendedRegexLexer, include, \
+    bygroups, default, LexerContext, do_insertions, words, line_re
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation, Error, Generic, Whitespace
+from pygments.util import shebang_matches
+
+__all__ = ['RubyLexer', 'RubyConsoleLexer', 'FancyLexer']
+
+
+RUBY_OPERATORS = (
+    '*', '**', '-', '+', '-@', '+@', '/', '%', '&', '|', '^', '`', '~',
+    '[]', '[]=', '<<', '>>', '<', '<>', '<=>', '>', '>=', '==', '==='
+)
+
+
+class RubyLexer(ExtendedRegexLexer):
+    """
+    For Ruby source code.
+    """
+
+    name = 'Ruby'
+    url = 'http://www.ruby-lang.org'
+    aliases = ['ruby', 'rb', 'duby']
+    filenames = ['*.rb', '*.rbw', 'Rakefile', '*.rake', '*.gemspec',
+                 '*.rbx', '*.duby', 'Gemfile', 'Vagrantfile']
+    mimetypes = ['text/x-ruby', 'application/x-ruby']
+    version_added = ''
+
+    flags = re.DOTALL | re.MULTILINE
+
+    def heredoc_callback(self, match, ctx):
+        # okay, this is the hardest part of parsing Ruby...
+        # match: 1 = <<[-~]?, 2 = quote? 3 = name 4 = quote? 5 = rest of line
+
+        start = match.start(1)
+        yield start, Operator, match.group(1)        # <<[-~]?
+        yield match.start(2), String.Heredoc, match.group(2)   # quote ", ', `
+        yield match.start(3), String.Delimiter, match.group(3) # heredoc name
+        yield match.start(4), String.Heredoc, match.group(4)   # quote again
+
+        heredocstack = ctx.__dict__.setdefault('heredocstack', [])
+        outermost = not bool(heredocstack)
+        heredocstack.append((match.group(1) in ('<<-', '<<~'), match.group(3)))
+
+        ctx.pos = match.start(5)
+        ctx.end = match.end(5)
+        # this may find other heredocs, so limit the recursion depth
+        if len(heredocstack) < 100:
+            yield from self.get_tokens_unprocessed(context=ctx)
+        else:
+            yield ctx.pos, String.Heredoc, match.group(5)
+        ctx.pos = match.end()
+
+        if outermost:
+            # this is the outer heredoc again, now we can process them all
+            for tolerant, hdname in heredocstack:
+                lines = []
+                for match in line_re.finditer(ctx.text, ctx.pos):
+                    if tolerant:
+                        check = match.group().strip()
+                    else:
+                        check = match.group().rstrip()
+                    if check == hdname:
+                        for amatch in lines:
+                            yield amatch.start(), String.Heredoc, amatch.group()
+                        yield match.start(), String.Delimiter, match.group()
+                        ctx.pos = match.end()
+                        break
+                    else:
+                        lines.append(match)
+                else:
+                    # end of heredoc not found -- error!
+                    for amatch in lines:
+                        yield amatch.start(), Error, amatch.group()
+            ctx.end = len(ctx.text)
+            del heredocstack[:]
+
+    def gen_rubystrings_rules():
+        def intp_regex_callback(self, match, ctx):
+            yield match.start(1), String.Regex, match.group(1)  # begin
+            nctx = LexerContext(match.group(3), 0, ['interpolated-regex'])
+            for i, t, v in self.get_tokens_unprocessed(context=nctx):
+                yield match.start(3)+i, t, v
+            yield match.start(4), String.Regex, match.group(4)  # end[mixounse]*
+            ctx.pos = match.end()
+
+        def intp_string_callback(self, match, ctx):
+            yield match.start(1), String.Other, match.group(1)
+            nctx = LexerContext(match.group(3), 0, ['interpolated-string'])
+            for i, t, v in self.get_tokens_unprocessed(context=nctx):
+                yield match.start(3)+i, t, v
+            yield match.start(4), String.Other, match.group(4)  # end
+            ctx.pos = match.end()
+
+        states = {}
+        states['strings'] = [
+            # easy ones
+            (r'\:@{0,2}[a-zA-Z_]\w*[!?]?', String.Symbol),
+            (words(RUBY_OPERATORS, prefix=r'\:@{0,2}'), String.Symbol),
+            (r":'(\\\\|\\[^\\]|[^'\\])*'", String.Symbol),
+            (r':"', String.Symbol, 'simple-sym'),
+            (r'([a-zA-Z_]\w*)(:)(?!:)',
+             bygroups(String.Symbol, Punctuation)),  # Since Ruby 1.9
+            (r'"', String.Double, 'simple-string-double'),
+            (r"'", String.Single, 'simple-string-single'),
+            (r'(?', '<>', 'ab'):
+            states[name+'-intp-string'] = [
+                (r'\\[\\' + bracecc + ']', String.Other),
+                (lbrace, String.Other, '#push'),
+                (rbrace, String.Other, '#pop'),
+                include('string-intp-escaped'),
+                (r'[\\#' + bracecc + ']', String.Other),
+                (r'[^\\#' + bracecc + ']+', String.Other),
+            ]
+            states['strings'].append((r'%[QWx]?' + lbrace, String.Other,
+                                      name+'-intp-string'))
+            states[name+'-string'] = [
+                (r'\\[\\' + bracecc + ']', String.Other),
+                (lbrace, String.Other, '#push'),
+                (rbrace, String.Other, '#pop'),
+                (r'[\\#' + bracecc + ']', String.Other),
+                (r'[^\\#' + bracecc + ']+', String.Other),
+            ]
+            states['strings'].append((r'%[qsw]' + lbrace, String.Other,
+                                      name+'-string'))
+            states[name+'-regex'] = [
+                (r'\\[\\' + bracecc + ']', String.Regex),
+                (lbrace, String.Regex, '#push'),
+                (rbrace + '[mixounse]*', String.Regex, '#pop'),
+                include('string-intp'),
+                (r'[\\#' + bracecc + ']', String.Regex),
+                (r'[^\\#' + bracecc + ']+', String.Regex),
+            ]
+            states['strings'].append((r'%r' + lbrace, String.Regex,
+                                      name+'-regex'))
+
+        # these must come after %!
+        states['strings'] += [
+            # %r regex
+            (r'(%r([\W_]))((?:\\\2|(?!\2).)*)(\2[mixounse]*)',
+             intp_regex_callback),
+            # regular fancy strings with qsw
+            (r'%[qsw]([\W_])((?:\\\1|(?!\1).)*)\1', String.Other),
+            (r'(%[QWx]([\W_]))((?:\\\2|(?!\2).)*)(\2)',
+             intp_string_callback),
+            # special forms of fancy strings after operators or
+            # in method calls with braces
+            (r'(?<=[-+/*%=<>&!^|~,(])(\s*)(%([\t ])(?:(?:\\\3|(?!\3).)*)\3)',
+             bygroups(Whitespace, String.Other, None)),
+            # and because of fixed width lookbehinds the whole thing a
+            # second time for line startings...
+            (r'^(\s*)(%([\t ])(?:(?:\\\3|(?!\3).)*)\3)',
+             bygroups(Whitespace, String.Other, None)),
+            # all regular fancy strings without qsw
+            (r'(%([^a-zA-Z0-9\s]))((?:\\\2|(?!\2).)*)(\2)',
+             intp_string_callback),
+        ]
+
+        return states
+
+    tokens = {
+        'root': [
+            (r'\A#!.+?$', Comment.Hashbang),
+            (r'#.*?$', Comment.Single),
+            (r'=begin\s.*?\n=end.*?$', Comment.Multiline),
+            # keywords
+            (words((
+                'BEGIN', 'END', 'alias', 'begin', 'break', 'case', 'defined?',
+                'do', 'else', 'elsif', 'end', 'ensure', 'for', 'if', 'in', 'next', 'redo',
+                'rescue', 'raise', 'retry', 'return', 'super', 'then', 'undef',
+                'unless', 'until', 'when', 'while', 'yield'), suffix=r'\b'),
+             Keyword),
+            # start of function, class and module names
+            (r'(module)(\s+)([a-zA-Z_]\w*'
+             r'(?:::[a-zA-Z_]\w*)*)',
+             bygroups(Keyword, Whitespace, Name.Namespace)),
+            (r'(def)(\s+)', bygroups(Keyword, Whitespace), 'funcname'),
+            (r'def(?=[*%&^`~+-/\[<>=])', Keyword, 'funcname'),
+            (r'(class)(\s+)', bygroups(Keyword, Whitespace), 'classname'),
+            # special methods
+            (words((
+                'initialize', 'new', 'loop', 'include', 'extend', 'raise', 'attr_reader',
+                'attr_writer', 'attr_accessor', 'attr', 'catch', 'throw', 'private',
+                'module_function', 'public', 'protected', 'true', 'false', 'nil'),
+                suffix=r'\b'),
+             Keyword.Pseudo),
+            (r'(not|and|or)\b', Operator.Word),
+            (words((
+                'autoload', 'block_given', 'const_defined', 'eql', 'equal', 'frozen', 'include',
+                'instance_of', 'is_a', 'iterator', 'kind_of', 'method_defined', 'nil',
+                'private_method_defined', 'protected_method_defined',
+                'public_method_defined', 'respond_to', 'tainted'), suffix=r'\?'),
+             Name.Builtin),
+            (r'(chomp|chop|exit|gsub|sub)!', Name.Builtin),
+            (words((
+                'Array', 'Float', 'Integer', 'String', '__id__', '__send__', 'abort',
+                'ancestors', 'at_exit', 'autoload', 'binding', 'callcc', 'caller',
+                'catch', 'chomp', 'chop', 'class_eval', 'class_variables',
+                'clone', 'const_defined?', 'const_get', 'const_missing', 'const_set',
+                'constants', 'display', 'dup', 'eval', 'exec', 'exit', 'extend', 'fail', 'fork',
+                'format', 'freeze', 'getc', 'gets', 'global_variables', 'gsub',
+                'hash', 'id', 'included_modules', 'inspect', 'instance_eval',
+                'instance_method', 'instance_methods',
+                'instance_variable_get', 'instance_variable_set', 'instance_variables',
+                'lambda', 'load', 'local_variables', 'loop',
+                'method', 'method_missing', 'methods', 'module_eval', 'name',
+                'object_id', 'open', 'p', 'print', 'printf', 'private_class_method',
+                'private_instance_methods',
+                'private_methods', 'proc', 'protected_instance_methods',
+                'protected_methods', 'public_class_method',
+                'public_instance_methods', 'public_methods',
+                'putc', 'puts', 'raise', 'rand', 'readline', 'readlines', 'require',
+                'scan', 'select', 'self', 'send', 'set_trace_func', 'singleton_methods', 'sleep',
+                'split', 'sprintf', 'srand', 'sub', 'syscall', 'system', 'taint',
+                'test', 'throw', 'to_a', 'to_s', 'trace_var', 'trap', 'untaint',
+                'untrace_var', 'warn'), prefix=r'(?~!:])|'
+             r'(?<=(?:\s|;)when\s)|'
+             r'(?<=(?:\s|;)or\s)|'
+             r'(?<=(?:\s|;)and\s)|'
+             r'(?<=\.index\s)|'
+             r'(?<=\.scan\s)|'
+             r'(?<=\.sub\s)|'
+             r'(?<=\.sub!\s)|'
+             r'(?<=\.gsub\s)|'
+             r'(?<=\.gsub!\s)|'
+             r'(?<=\.match\s)|'
+             r'(?<=(?:\s|;)if\s)|'
+             r'(?<=(?:\s|;)elsif\s)|'
+             r'(?<=^when\s)|'
+             r'(?<=^index\s)|'
+             r'(?<=^scan\s)|'
+             r'(?<=^sub\s)|'
+             r'(?<=^gsub\s)|'
+             r'(?<=^sub!\s)|'
+             r'(?<=^gsub!\s)|'
+             r'(?<=^match\s)|'
+             r'(?<=^if\s)|'
+             r'(?<=^elsif\s)'
+             r')(\s*)(/)', bygroups(Text, String.Regex), 'multiline-regex'),
+            # multiline regex (in method calls or subscripts)
+            (r'(?<=\(|,|\[)/', String.Regex, 'multiline-regex'),
+            # multiline regex (this time the funny no whitespace rule)
+            (r'(\s+)(/)(?![\s=])', bygroups(Whitespace, String.Regex),
+             'multiline-regex'),
+            # lex numbers and ignore following regular expressions which
+            # are division operators in fact (grrrr. i hate that. any
+            # better ideas?)
+            # since pygments 0.7 we also eat a "?" operator after numbers
+            # so that the char operator does not work. Chars are not allowed
+            # there so that you can use the ternary operator.
+            # stupid example:
+            #   x>=0?n[x]:""
+            (r'(0_?[0-7]+(?:_[0-7]+)*)(\s*)([/?])?',
+             bygroups(Number.Oct, Whitespace, Operator)),
+            (r'(0x[0-9A-Fa-f]+(?:_[0-9A-Fa-f]+)*)(\s*)([/?])?',
+             bygroups(Number.Hex, Whitespace, Operator)),
+            (r'(0b[01]+(?:_[01]+)*)(\s*)([/?])?',
+             bygroups(Number.Bin, Whitespace, Operator)),
+            (r'([\d]+(?:_\d+)*)(\s*)([/?])?',
+             bygroups(Number.Integer, Whitespace, Operator)),
+            # Names
+            (r'@@[a-zA-Z_]\w*', Name.Variable.Class),
+            (r'@[a-zA-Z_]\w*', Name.Variable.Instance),
+            (r'\$\w+', Name.Variable.Global),
+            (r'\$[!@&`\'+~=/\\,;.<>_*$?:"^-]', Name.Variable.Global),
+            (r'\$-[0adFiIlpvw]', Name.Variable.Global),
+            (r'::', Operator),
+            include('strings'),
+            # chars
+            (r'\?(\\[MC]-)*'  # modifiers
+             r'(\\([\\abefnrstv#"\']|x[a-fA-F0-9]{1,2}|[0-7]{1,3})|\S)'
+             r'(?!\w)',
+             String.Char),
+            (r'[A-Z]\w+', Name.Constant),
+            # this is needed because ruby attributes can look
+            # like keywords (class) or like this: ` ?!?
+            (words(RUBY_OPERATORS, prefix=r'(\.|::)'),
+             bygroups(Operator, Name.Operator)),
+            (r'(\.|::)([a-zA-Z_]\w*[!?]?|[*%&^`~+\-/\[<>=])',
+             bygroups(Operator, Name)),
+            (r'[a-zA-Z_]\w*[!?]?', Name),
+            (r'(\[|\]|\*\*|<>?|>=|<=|<=>|=~|={3}|'
+             r'!~|&&?|\|\||\.{1,3})', Operator),
+            (r'[-+/*%=<>&!^|~]=?', Operator),
+            (r'[(){};,/?:\\]', Punctuation),
+            (r'\s+', Whitespace)
+        ],
+        'funcname': [
+            (r'\(', Punctuation, 'defexpr'),
+            (r'(?:([a-zA-Z_]\w*)(\.))?'  # optional scope name, like "self."
+             r'('
+                r'[a-zA-Z\u0080-\uffff][a-zA-Z0-9_\u0080-\uffff]*[!?=]?'  # method name
+                r'|!=|!~|=~|\*\*?|[-+!~]@?|[/%&|^]|<=>|<[<=]?|>[>=]?|===?'  # or operator override
+                r'|\[\]=?'  # or element reference/assignment override
+                r'|`'  # or the undocumented backtick override
+             r')',
+             bygroups(Name.Class, Operator, Name.Function), '#pop'),
+            default('#pop')
+        ],
+        'classname': [
+            (r'\(', Punctuation, 'defexpr'),
+            (r'<<', Operator, '#pop'),
+            (r'[A-Z_]\w*', Name.Class, '#pop'),
+            default('#pop')
+        ],
+        'defexpr': [
+            (r'(\))(\.|::)?', bygroups(Punctuation, Operator), '#pop'),
+            (r'\(', Operator, '#push'),
+            include('root')
+        ],
+        'in-intp': [
+            (r'\{', String.Interpol, '#push'),
+            (r'\}', String.Interpol, '#pop'),
+            include('root'),
+        ],
+        'string-intp': [
+            (r'#\{', String.Interpol, 'in-intp'),
+            (r'#@@?[a-zA-Z_]\w*', String.Interpol),
+            (r'#\$[a-zA-Z_]\w*', String.Interpol)
+        ],
+        'string-intp-escaped': [
+            include('string-intp'),
+            (r'\\([\\abefnrstv#"\']|x[a-fA-F0-9]{1,2}|[0-7]{1,3})',
+             String.Escape)
+        ],
+        'interpolated-regex': [
+            include('string-intp'),
+            (r'[\\#]', String.Regex),
+            (r'[^\\#]+', String.Regex),
+        ],
+        'interpolated-string': [
+            include('string-intp'),
+            (r'[\\#]', String.Other),
+            (r'[^\\#]+', String.Other),
+        ],
+        'multiline-regex': [
+            include('string-intp'),
+            (r'\\\\', String.Regex),
+            (r'\\/', String.Regex),
+            (r'[\\#]', String.Regex),
+            (r'[^\\/#]+', String.Regex),
+            (r'/[mixounse]*', String.Regex, '#pop'),
+        ],
+        'end-part': [
+            (r'.+', Comment.Preproc, '#pop')
+        ]
+    }
+    tokens.update(gen_rubystrings_rules())
+
+    def analyse_text(text):
+        return shebang_matches(text, r'ruby(1\.\d)?')
+
+
+class RubyConsoleLexer(Lexer):
+    """
+    For Ruby interactive console (**irb**) output.
+    """
+    name = 'Ruby irb session'
+    aliases = ['rbcon', 'irb']
+    mimetypes = ['text/x-ruby-shellsession']
+    url = 'https://www.ruby-lang.org'
+    version_added = ''
+    _example = 'rbcon/console'
+
+    _prompt_re = re.compile(r'irb\([a-zA-Z_]\w*\):\d{3}:\d+[>*"\'] '
+                            r'|>> |\?> ')
+
+    def get_tokens_unprocessed(self, text):
+        rblexer = RubyLexer(**self.options)
+
+        curcode = ''
+        insertions = []
+        for match in line_re.finditer(text):
+            line = match.group()
+            m = self._prompt_re.match(line)
+            if m is not None:
+                end = m.end()
+                insertions.append((len(curcode),
+                                   [(0, Generic.Prompt, line[:end])]))
+                curcode += line[end:]
+            else:
+                if curcode:
+                    yield from do_insertions(
+                        insertions, rblexer.get_tokens_unprocessed(curcode))
+                    curcode = ''
+                    insertions = []
+                yield match.start(), Generic.Output, line
+        if curcode:
+            yield from do_insertions(
+                insertions, rblexer.get_tokens_unprocessed(curcode))
+
+
+class FancyLexer(RegexLexer):
+    """
+    Pygments Lexer For Fancy.
+
+    Fancy is a self-hosted, pure object-oriented, dynamic,
+    class-based, concurrent general-purpose programming language
+    running on Rubinius, the Ruby VM.
+    """
+    name = 'Fancy'
+    url = 'https://github.com/bakkdoor/fancy'
+    filenames = ['*.fy', '*.fancypack']
+    aliases = ['fancy', 'fy']
+    mimetypes = ['text/x-fancysrc']
+    version_added = '1.5'
+
+    tokens = {
+        # copied from PerlLexer:
+        'balanced-regex': [
+            (r'/(\\\\|\\[^\\]|[^/\\])*/[egimosx]*', String.Regex, '#pop'),
+            (r'!(\\\\|\\[^\\]|[^!\\])*![egimosx]*', String.Regex, '#pop'),
+            (r'\\(\\\\|[^\\])*\\[egimosx]*', String.Regex, '#pop'),
+            (r'\{(\\\\|\\[^\\]|[^}\\])*\}[egimosx]*', String.Regex, '#pop'),
+            (r'<(\\\\|\\[^\\]|[^>\\])*>[egimosx]*', String.Regex, '#pop'),
+            (r'\[(\\\\|\\[^\\]|[^\]\\])*\][egimosx]*', String.Regex, '#pop'),
+            (r'\((\\\\|\\[^\\]|[^)\\])*\)[egimosx]*', String.Regex, '#pop'),
+            (r'@(\\\\|\\[^\\]|[^@\\])*@[egimosx]*', String.Regex, '#pop'),
+            (r'%(\\\\|\\[^\\]|[^%\\])*%[egimosx]*', String.Regex, '#pop'),
+            (r'\$(\\\\|\\[^\\]|[^$\\])*\$[egimosx]*', String.Regex, '#pop'),
+        ],
+        'root': [
+            (r'\s+', Whitespace),
+
+            # balanced delimiters (copied from PerlLexer):
+            (r's\{(\\\\|\\[^\\]|[^}\\])*\}\s*', String.Regex, 'balanced-regex'),
+            (r's<(\\\\|\\[^\\]|[^>\\])*>\s*', String.Regex, 'balanced-regex'),
+            (r's\[(\\\\|\\[^\\]|[^\]\\])*\]\s*', String.Regex, 'balanced-regex'),
+            (r's\((\\\\|\\[^\\]|[^)\\])*\)\s*', String.Regex, 'balanced-regex'),
+            (r'm?/(\\\\|\\[^\\]|[^///\n])*/[gcimosx]*', String.Regex),
+            (r'm(?=[/!\\{<\[(@%$])', String.Regex, 'balanced-regex'),
+
+            # Comments
+            (r'#(.*?)\n', Comment.Single),
+            # Symbols
+            (r'\'([^\'\s\[\](){}]+|\[\])', String.Symbol),
+            # Multi-line DoubleQuotedString
+            (r'"""(\\\\|\\[^\\]|[^\\])*?"""', String),
+            # DoubleQuotedString
+            (r'"(\\\\|\\[^\\]|[^"\\])*"', String),
+            # keywords
+            (r'(def|class|try|catch|finally|retry|return|return_local|match|'
+             r'case|->|=>)\b', Keyword),
+            # constants
+            (r'(self|super|nil|false|true)\b', Name.Constant),
+            (r'[(){};,/?|:\\]', Punctuation),
+            # names
+            (words((
+                'Object', 'Array', 'Hash', 'Directory', 'File', 'Class', 'String',
+                'Number', 'Enumerable', 'FancyEnumerable', 'Block', 'TrueClass',
+                'NilClass', 'FalseClass', 'Tuple', 'Symbol', 'Stack', 'Set',
+                'FancySpec', 'Method', 'Package', 'Range'), suffix=r'\b'),
+             Name.Builtin),
+            # functions
+            (r'[a-zA-Z](\w|[-+?!=*/^><%])*:', Name.Function),
+            # operators, must be below functions
+            (r'[-+*/~,<>=&!?%^\[\].$]+', Operator),
+            (r'[A-Z]\w*', Name.Constant),
+            (r'@[a-zA-Z_]\w*', Name.Variable.Instance),
+            (r'@@[a-zA-Z_]\w*', Name.Variable.Class),
+            ('@@?', Operator),
+            (r'[a-zA-Z_]\w*', Name),
+            # numbers - / checks are necessary to avoid mismarking regexes,
+            # see comment in RubyLexer
+            (r'(0[oO]?[0-7]+(?:_[0-7]+)*)(\s*)([/?])?',
+             bygroups(Number.Oct, Whitespace, Operator)),
+            (r'(0[xX][0-9A-Fa-f]+(?:_[0-9A-Fa-f]+)*)(\s*)([/?])?',
+             bygroups(Number.Hex, Whitespace, Operator)),
+            (r'(0[bB][01]+(?:_[01]+)*)(\s*)([/?])?',
+             bygroups(Number.Bin, Whitespace, Operator)),
+            (r'([\d]+(?:_\d+)*)(\s*)([/?])?',
+             bygroups(Number.Integer, Whitespace, Operator)),
+            (r'\d+([eE][+-]?[0-9]+)|\d+\.\d+([eE][+-]?[0-9]+)?', Number.Float),
+            (r'\d+', Number.Integer)
+        ]
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rust.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rust.py
new file mode 100644
index 00000000..63410475
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/rust.py
@@ -0,0 +1,222 @@
+"""
+    pygments.lexers.rust
+    ~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for the Rust language.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, include, bygroups, words, default
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation, Whitespace
+
+__all__ = ['RustLexer']
+
+
+class RustLexer(RegexLexer):
+    """
+    Lexer for the Rust programming language (version 1.47).
+    """
+    name = 'Rust'
+    url = 'https://www.rust-lang.org/'
+    filenames = ['*.rs', '*.rs.in']
+    aliases = ['rust', 'rs']
+    mimetypes = ['text/rust', 'text/x-rust']
+    version_added = '1.6'
+
+    keyword_types = (words((
+        'u8', 'u16', 'u32', 'u64', 'u128', 'i8', 'i16', 'i32', 'i64', 'i128',
+        'usize', 'isize', 'f32', 'f64', 'char', 'str', 'bool',
+    ), suffix=r'\b'), Keyword.Type)
+
+    builtin_funcs_types = (words((
+        'Copy', 'Send', 'Sized', 'Sync', 'Unpin',
+        'Drop', 'Fn', 'FnMut', 'FnOnce', 'drop',
+        'Box', 'ToOwned', 'Clone',
+        'PartialEq', 'PartialOrd', 'Eq', 'Ord',
+        'AsRef', 'AsMut', 'Into', 'From', 'Default',
+        'Iterator', 'Extend', 'IntoIterator', 'DoubleEndedIterator',
+        'ExactSizeIterator',
+        'Option', 'Some', 'None',
+        'Result', 'Ok', 'Err',
+        'String', 'ToString', 'Vec',
+    ), suffix=r'\b'), Name.Builtin)
+
+    builtin_macros = (words((
+        'asm', 'assert', 'assert_eq', 'assert_ne', 'cfg', 'column',
+        'compile_error', 'concat', 'concat_idents', 'dbg', 'debug_assert',
+        'debug_assert_eq', 'debug_assert_ne', 'env', 'eprint', 'eprintln',
+        'file', 'format', 'format_args', 'format_args_nl', 'global_asm',
+        'include', 'include_bytes', 'include_str',
+        'is_aarch64_feature_detected',
+        'is_arm_feature_detected',
+        'is_mips64_feature_detected',
+        'is_mips_feature_detected',
+        'is_powerpc64_feature_detected',
+        'is_powerpc_feature_detected',
+        'is_x86_feature_detected',
+        'line', 'llvm_asm', 'log_syntax', 'macro_rules', 'matches',
+        'module_path', 'option_env', 'panic', 'print', 'println', 'stringify',
+        'thread_local', 'todo', 'trace_macros', 'unimplemented', 'unreachable',
+        'vec', 'write', 'writeln',
+    ), suffix=r'!'), Name.Function.Magic)
+
+    tokens = {
+        'root': [
+            # rust allows a file to start with a shebang, but if the first line
+            # starts with #![ then it's not a shebang but a crate attribute.
+            (r'#![^[\r\n].*$', Comment.Preproc),
+            default('base'),
+        ],
+        'base': [
+            # Whitespace and Comments
+            (r'\n', Whitespace),
+            (r'\s+', Whitespace),
+            (r'//!.*?\n', String.Doc),
+            (r'///(\n|[^/].*?\n)', String.Doc),
+            (r'//(.*?)\n', Comment.Single),
+            (r'/\*\*(\n|[^/*])', String.Doc, 'doccomment'),
+            (r'/\*!', String.Doc, 'doccomment'),
+            (r'/\*', Comment.Multiline, 'comment'),
+
+            # Macro parameters
+            (r"""\$([a-zA-Z_]\w*|\(,?|\),?|,?)""", Comment.Preproc),
+            # Keywords
+            (words(('as', 'async', 'await', 'box', 'const', 'crate', 'dyn',
+                    'else', 'extern', 'for', 'if', 'impl', 'in', 'loop',
+                    'match', 'move', 'mut', 'pub', 'ref', 'return', 'static',
+                    'super', 'trait', 'unsafe', 'use', 'where', 'while'),
+                   suffix=r'\b'), Keyword),
+            (words(('abstract', 'become', 'do', 'final', 'macro', 'override',
+                    'priv', 'typeof', 'try', 'unsized', 'virtual', 'yield'),
+                   suffix=r'\b'), Keyword.Reserved),
+            (r'(true|false)\b', Keyword.Constant),
+            (r'self\b', Name.Builtin.Pseudo),
+            (r'mod\b', Keyword, 'modname'),
+            (r'let\b', Keyword.Declaration),
+            (r'fn\b', Keyword, 'funcname'),
+            (r'(struct|enum|type|union)\b', Keyword, 'typename'),
+            (r'(default)(\s+)(type|fn)\b', bygroups(Keyword, Whitespace, Keyword)),
+            keyword_types,
+            (r'[sS]elf\b', Name.Builtin.Pseudo),
+            # Prelude (taken from Rust's src/libstd/prelude.rs)
+            builtin_funcs_types,
+            builtin_macros,
+            # Path separators, so types don't catch them.
+            (r'::\b', Punctuation),
+            # Types in positions.
+            (r'(?::|->)', Punctuation, 'typename'),
+            # Labels
+            (r'(break|continue)(\b\s*)(\'[A-Za-z_]\w*)?',
+             bygroups(Keyword, Text.Whitespace, Name.Label)),
+
+            # Character literals
+            (r"""'(\\['"\\nrt]|\\x[0-7][0-9a-fA-F]|\\0"""
+             r"""|\\u\{[0-9a-fA-F]{1,6}\}|.)'""",
+             String.Char),
+            (r"""b'(\\['"\\nrt]|\\x[0-9a-fA-F]{2}|\\0"""
+             r"""|\\u\{[0-9a-fA-F]{1,6}\}|.)'""",
+             String.Char),
+
+            # Binary literals
+            (r'0b[01_]+', Number.Bin, 'number_lit'),
+            # Octal literals
+            (r'0o[0-7_]+', Number.Oct, 'number_lit'),
+            # Hexadecimal literals
+            (r'0[xX][0-9a-fA-F_]+', Number.Hex, 'number_lit'),
+            # Decimal literals
+            (r'[0-9][0-9_]*(\.[0-9_]+[eE][+\-]?[0-9_]+|'
+             r'\.[0-9_]*(?!\.)|[eE][+\-]?[0-9_]+)', Number.Float,
+             'number_lit'),
+            (r'[0-9][0-9_]*', Number.Integer, 'number_lit'),
+
+            # String literals
+            (r'b"', String, 'bytestring'),
+            (r'"', String, 'string'),
+            (r'(?s)b?r(#*)".*?"\1', String),
+
+            # Lifetime names
+            (r"'", Operator, 'lifetime'),
+
+            # Operators and Punctuation
+            (r'\.\.=?', Operator),
+            (r'[{}()\[\],.;]', Punctuation),
+            (r'[+\-*/%&|<>^!~@=:?]', Operator),
+
+            # Identifiers
+            (r'[a-zA-Z_]\w*', Name),
+            # Raw identifiers
+            (r'r#[a-zA-Z_]\w*', Name),
+
+            # Attributes
+            (r'#!?\[', Comment.Preproc, 'attribute['),
+
+            # Misc
+            # Lone hashes: not used in Rust syntax, but allowed in macro
+            # arguments, most famously for quote::quote!()
+            (r'#', Punctuation),
+        ],
+        'comment': [
+            (r'[^*/]+', Comment.Multiline),
+            (r'/\*', Comment.Multiline, '#push'),
+            (r'\*/', Comment.Multiline, '#pop'),
+            (r'[*/]', Comment.Multiline),
+        ],
+        'doccomment': [
+            (r'[^*/]+', String.Doc),
+            (r'/\*', String.Doc, '#push'),
+            (r'\*/', String.Doc, '#pop'),
+            (r'[*/]', String.Doc),
+        ],
+        'modname': [
+            (r'\s+', Whitespace),
+            (r'[a-zA-Z_]\w*', Name.Namespace, '#pop'),
+            default('#pop'),
+        ],
+        'funcname': [
+            (r'\s+', Whitespace),
+            (r'[a-zA-Z_]\w*', Name.Function, '#pop'),
+            default('#pop'),
+        ],
+        'typename': [
+            (r'\s+', Whitespace),
+            (r'&', Keyword.Pseudo),
+            (r"'", Operator, 'lifetime'),
+            builtin_funcs_types,
+            keyword_types,
+            (r'[a-zA-Z_]\w*', Name.Class, '#pop'),
+            default('#pop'),
+        ],
+        'lifetime': [
+            (r"(static|_)", Name.Builtin),
+            (r"[a-zA-Z_]+\w*", Name.Attribute),
+            default('#pop'),
+        ],
+        'number_lit': [
+            (r'[ui](8|16|32|64|size)', Keyword, '#pop'),
+            (r'f(32|64)', Keyword, '#pop'),
+            default('#pop'),
+        ],
+        'string': [
+            (r'"', String, '#pop'),
+            (r"""\\['"\\nrt]|\\x[0-7][0-9a-fA-F]|\\0"""
+             r"""|\\u\{[0-9a-fA-F]{1,6}\}""", String.Escape),
+            (r'[^\\"]+', String),
+            (r'\\', String),
+        ],
+        'bytestring': [
+            (r"""\\x[89a-fA-F][0-9a-fA-F]""", String.Escape),
+            include('string'),
+        ],
+        'attribute_common': [
+            (r'"', String, 'string'),
+            (r'\[', Comment.Preproc, 'attribute['),
+        ],
+        'attribute[': [
+            include('attribute_common'),
+            (r'\]', Comment.Preproc, '#pop'),
+            (r'[^"\]\[]+', Comment.Preproc),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/sas.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/sas.py
new file mode 100644
index 00000000..1b2ad432
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/sas.py
@@ -0,0 +1,227 @@
+"""
+    pygments.lexers.sas
+    ~~~~~~~~~~~~~~~~~~~
+
+    Lexer for SAS.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+from pygments.lexer import RegexLexer, include, words
+from pygments.token import Comment, Keyword, Name, Number, String, Text, \
+    Other, Generic
+
+__all__ = ['SASLexer']
+
+
+class SASLexer(RegexLexer):
+    """
+    For SAS files.
+    """
+    # Syntax from syntax/sas.vim by James Kidd 
+
+    name      = 'SAS'
+    aliases   = ['sas']
+    filenames = ['*.SAS', '*.sas']
+    mimetypes = ['text/x-sas', 'text/sas', 'application/x-sas']
+    url = 'https://en.wikipedia.org/wiki/SAS_(software)'
+    version_added = '2.2'
+    flags     = re.IGNORECASE | re.MULTILINE
+
+    builtins_macros = (
+        "bquote", "nrbquote", "cmpres", "qcmpres", "compstor", "datatyp",
+        "display", "do", "else", "end", "eval", "global", "goto", "if",
+        "index", "input", "keydef", "label", "left", "length", "let",
+        "local", "lowcase", "macro", "mend", "nrquote",
+        "nrstr", "put", "qleft", "qlowcase", "qscan",
+        "qsubstr", "qsysfunc", "qtrim", "quote", "qupcase", "scan",
+        "str", "substr", "superq", "syscall", "sysevalf", "sysexec",
+        "sysfunc", "sysget", "syslput", "sysprod", "sysrc", "sysrput",
+        "then", "to", "trim", "unquote", "until", "upcase", "verify",
+        "while", "window"
+    )
+
+    builtins_conditionals = (
+        "do", "if", "then", "else", "end", "until", "while"
+    )
+
+    builtins_statements = (
+        "abort", "array", "attrib", "by", "call", "cards", "cards4",
+        "catname", "continue", "datalines", "datalines4", "delete", "delim",
+        "delimiter", "display", "dm", "drop", "endsas", "error", "file",
+        "filename", "footnote", "format", "goto", "in", "infile", "informat",
+        "input", "keep", "label", "leave", "length", "libname", "link",
+        "list", "lostcard", "merge", "missing", "modify", "options", "output",
+        "out", "page", "put", "redirect", "remove", "rename", "replace",
+        "retain", "return", "select", "set", "skip", "startsas", "stop",
+        "title", "update", "waitsas", "where", "window", "x", "systask"
+    )
+
+    builtins_sql = (
+        "add", "and", "alter", "as", "cascade", "check", "create",
+        "delete", "describe", "distinct", "drop", "foreign", "from",
+        "group", "having", "index", "insert", "into", "in", "key", "like",
+        "message", "modify", "msgtype", "not", "null", "on", "or",
+        "order", "primary", "references", "reset", "restrict", "select",
+        "set", "table", "unique", "update", "validate", "view", "where"
+    )
+
+    builtins_functions = (
+        "abs", "addr", "airy", "arcos", "arsin", "atan", "attrc",
+        "attrn", "band", "betainv", "blshift", "bnot", "bor",
+        "brshift", "bxor", "byte", "cdf", "ceil", "cexist", "cinv",
+        "close", "cnonct", "collate", "compbl", "compound",
+        "compress", "cos", "cosh", "css", "curobs", "cv", "daccdb",
+        "daccdbsl", "daccsl", "daccsyd", "dacctab", "dairy", "date",
+        "datejul", "datepart", "datetime", "day", "dclose", "depdb",
+        "depdbsl", "depsl", "depsyd",
+        "deptab", "dequote", "dhms", "dif", "digamma",
+        "dim", "dinfo", "dnum", "dopen", "doptname", "doptnum",
+        "dread", "dropnote", "dsname", "erf", "erfc", "exist", "exp",
+        "fappend", "fclose", "fcol", "fdelete", "fetch", "fetchobs",
+        "fexist", "fget", "fileexist", "filename", "fileref",
+        "finfo", "finv", "fipname", "fipnamel", "fipstate", "floor",
+        "fnonct", "fnote", "fopen", "foptname", "foptnum", "fpoint",
+        "fpos", "fput", "fread", "frewind", "frlen", "fsep", "fuzz",
+        "fwrite", "gaminv", "gamma", "getoption", "getvarc", "getvarn",
+        "hbound", "hms", "hosthelp", "hour", "ibessel", "index",
+        "indexc", "indexw", "input", "inputc", "inputn", "int",
+        "intck", "intnx", "intrr", "irr", "jbessel", "juldate",
+        "kurtosis", "lag", "lbound", "left", "length", "lgamma",
+        "libname", "libref", "log", "log10", "log2", "logpdf", "logpmf",
+        "logsdf", "lowcase", "max", "mdy", "mean", "min", "minute",
+        "mod", "month", "mopen", "mort", "n", "netpv", "nmiss",
+        "normal", "note", "npv", "open", "ordinal", "pathname",
+        "pdf", "peek", "peekc", "pmf", "point", "poisson", "poke",
+        "probbeta", "probbnml", "probchi", "probf", "probgam",
+        "probhypr", "probit", "probnegb", "probnorm", "probt",
+        "put", "putc", "putn", "qtr", "quote", "ranbin", "rancau",
+        "ranexp", "rangam", "range", "rank", "rannor", "ranpoi",
+        "rantbl", "rantri", "ranuni", "repeat", "resolve", "reverse",
+        "rewind", "right", "round", "saving", "scan", "sdf", "second",
+        "sign", "sin", "sinh", "skewness", "soundex", "spedis",
+        "sqrt", "std", "stderr", "stfips", "stname", "stnamel",
+        "substr", "sum", "symget", "sysget", "sysmsg", "sysprod",
+        "sysrc", "system", "tan", "tanh", "time", "timepart", "tinv",
+        "tnonct", "today", "translate", "tranwrd", "trigamma",
+        "trim", "trimn", "trunc", "uniform", "upcase", "uss", "var",
+        "varfmt", "varinfmt", "varlabel", "varlen", "varname",
+        "varnum", "varray", "varrayx", "vartype", "verify", "vformat",
+        "vformatd", "vformatdx", "vformatn", "vformatnx", "vformatw",
+        "vformatwx", "vformatx", "vinarray", "vinarrayx", "vinformat",
+        "vinformatd", "vinformatdx", "vinformatn", "vinformatnx",
+        "vinformatw", "vinformatwx", "vinformatx", "vlabel",
+        "vlabelx", "vlength", "vlengthx", "vname", "vnamex", "vtype",
+        "vtypex", "weekday", "year", "yyq", "zipfips", "zipname",
+        "zipnamel", "zipstate"
+    )
+
+    tokens = {
+        'root': [
+            include('comments'),
+            include('proc-data'),
+            include('cards-datalines'),
+            include('logs'),
+            include('general'),
+            (r'.', Text),
+        ],
+        # SAS is multi-line regardless, but * is ended by ;
+        'comments': [
+            (r'^\s*\*.*?;', Comment),
+            (r'/\*.*?\*/', Comment),
+            (r'^\s*\*(.|\n)*?;', Comment.Multiline),
+            (r'/[*](.|\n)*?[*]/', Comment.Multiline),
+        ],
+        # Special highlight for proc, data, quit, run
+        'proc-data': [
+            (r'(^|;)\s*(proc \w+|data|run|quit)[\s;]',
+             Keyword.Reserved),
+        ],
+        # Special highlight cards and datalines
+        'cards-datalines': [
+            (r'^\s*(datalines|cards)\s*;\s*$', Keyword, 'data'),
+        ],
+        'data': [
+            (r'(.|\n)*^\s*;\s*$', Other, '#pop'),
+        ],
+        # Special highlight for put NOTE|ERROR|WARNING (order matters)
+        'logs': [
+            (r'\n?^\s*%?put ', Keyword, 'log-messages'),
+        ],
+        'log-messages': [
+            (r'NOTE(:|-).*', Generic, '#pop'),
+            (r'WARNING(:|-).*', Generic.Emph, '#pop'),
+            (r'ERROR(:|-).*', Generic.Error, '#pop'),
+            include('general'),
+        ],
+        'general': [
+            include('keywords'),
+            include('vars-strings'),
+            include('special'),
+            include('numbers'),
+        ],
+        # Keywords, statements, functions, macros
+        'keywords': [
+            (words(builtins_statements,
+                   prefix = r'\b',
+                   suffix = r'\b'),
+             Keyword),
+            (words(builtins_sql,
+                   prefix = r'\b',
+                   suffix = r'\b'),
+             Keyword),
+            (words(builtins_conditionals,
+                   prefix = r'\b',
+                   suffix = r'\b'),
+             Keyword),
+            (words(builtins_macros,
+                   prefix = r'%',
+                   suffix = r'\b'),
+             Name.Builtin),
+            (words(builtins_functions,
+                   prefix = r'\b',
+                   suffix = r'\('),
+             Name.Builtin),
+        ],
+        # Strings and user-defined variables and macros (order matters)
+        'vars-strings': [
+            (r'&[a-z_]\w{0,31}\.?', Name.Variable),
+            (r'%[a-z_]\w{0,31}', Name.Function),
+            (r'\'', String, 'string_squote'),
+            (r'"', String, 'string_dquote'),
+        ],
+        'string_squote': [
+            ('\'', String, '#pop'),
+            (r'\\\\|\\"|\\\n', String.Escape),
+            # AFAIK, macro variables are not evaluated in single quotes
+            # (r'&', Name.Variable, 'validvar'),
+            (r'[^$\'\\]+', String),
+            (r'[$\'\\]', String),
+        ],
+        'string_dquote': [
+            (r'"', String, '#pop'),
+            (r'\\\\|\\"|\\\n', String.Escape),
+            (r'&', Name.Variable, 'validvar'),
+            (r'[^$&"\\]+', String),
+            (r'[$"\\]', String),
+        ],
+        'validvar': [
+            (r'[a-z_]\w{0,31}\.?', Name.Variable, '#pop'),
+        ],
+        # SAS numbers and special variables
+        'numbers': [
+            (r'\b[+-]?([0-9]+(\.[0-9]+)?|\.[0-9]+|\.)(E[+-]?[0-9]+)?i?\b',
+             Number),
+        ],
+        'special': [
+            (r'(null|missing|_all_|_automatic_|_character_|_n_|'
+             r'_infile_|_name_|_null_|_numeric_|_user_|_webout_)',
+             Keyword.Constant),
+        ],
+        # 'operators': [
+        #     (r'(-|=|<=|>=|<|>|<>|&|!=|'
+        #      r'\||\*|\+|\^|/|!|~|~=)', Operator)
+        # ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/savi.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/savi.py
new file mode 100644
index 00000000..1e443ae3
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/savi.py
@@ -0,0 +1,171 @@
+"""
+    pygments.lexers.savi
+    ~~~~~~~~~~~~~~~~~~~~
+
+    Lexer for Savi.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, bygroups, include
+from pygments.token import Whitespace, Keyword, Name, String, Number, \
+  Operator, Punctuation, Comment, Generic, Error
+
+__all__ = ['SaviLexer']
+
+
+# The canonical version of this file can be found in the following repository,
+# where it is kept in sync with any language changes, as well as the other
+# pygments-like lexers that are maintained for use with other tools:
+# - https://github.com/savi-lang/savi/blob/main/tooling/pygments/lexers/savi.py
+#
+# If you're changing this file in the pygments repository, please ensure that
+# any changes you make are also propagated to the official Savi repository,
+# in order to avoid accidental clobbering of your changes later when an update
+# from the Savi repository flows forward into the pygments repository.
+#
+# If you're changing this file in the Savi repository, please ensure that
+# any changes you make are also reflected in the other pygments-like lexers
+# (rouge, vscode, etc) so that all of the lexers can be kept cleanly in sync.
+
+class SaviLexer(RegexLexer):
+    """
+    For Savi source code.
+
+    .. versionadded: 2.10
+    """
+
+    name = 'Savi'
+    url = 'https://github.com/savi-lang/savi'
+    aliases = ['savi']
+    filenames = ['*.savi']
+    version_added = ''
+
+    tokens = {
+      "root": [
+        # Line Comment
+        (r'//.*?$', Comment.Single),
+
+        # Doc Comment
+        (r'::.*?$', Comment.Single),
+
+        # Capability Operator
+        (r'(\')(\w+)(?=[^\'])', bygroups(Operator, Name)),
+
+        # Double-Quote String
+        (r'\w?"', String.Double, "string.double"),
+
+        # Single-Char String
+        (r"'", String.Char, "string.char"),
+
+        # Type Name
+        (r'(_?[A-Z]\w*)', Name.Class),
+
+        # Nested Type Name
+        (r'(\.)(\s*)(_?[A-Z]\w*)', bygroups(Punctuation, Whitespace, Name.Class)),
+
+        # Declare
+        (r'^([ \t]*)(:\w+)',
+          bygroups(Whitespace, Name.Tag),
+          "decl"),
+
+        # Error-Raising Calls/Names
+        (r'((\w+|\+|\-|\*)\!)', Generic.Deleted),
+
+        # Numeric Values
+        (r'\b\d([\d_]*(\.[\d_]+)?)\b', Number),
+
+        # Hex Numeric Values
+        (r'\b0x([0-9a-fA-F_]+)\b', Number.Hex),
+
+        # Binary Numeric Values
+        (r'\b0b([01_]+)\b', Number.Bin),
+
+        # Function Call (with braces)
+        (r'\w+(?=\()', Name.Function),
+
+        # Function Call (with receiver)
+        (r'(\.)(\s*)(\w+)', bygroups(Punctuation, Whitespace, Name.Function)),
+
+        # Function Call (with self receiver)
+        (r'(@)(\w+)', bygroups(Punctuation, Name.Function)),
+
+        # Parenthesis
+        (r'\(', Punctuation, "root"),
+        (r'\)', Punctuation, "#pop"),
+
+        # Brace
+        (r'\{', Punctuation, "root"),
+        (r'\}', Punctuation, "#pop"),
+
+        # Bracket
+        (r'\[', Punctuation, "root"),
+        (r'(\])(\!)', bygroups(Punctuation, Generic.Deleted), "#pop"),
+        (r'\]', Punctuation, "#pop"),
+
+        # Punctuation
+        (r'[,;:\.@]', Punctuation),
+
+        # Piping Operators
+        (r'(\|\>)', Operator),
+
+        # Branching Operators
+        (r'(\&\&|\|\||\?\?|\&\?|\|\?|\.\?)', Operator),
+
+        # Comparison Operators
+        (r'(\<\=\>|\=\~|\=\=|\<\=|\>\=|\<|\>)', Operator),
+
+        # Arithmetic Operators
+        (r'(\+|\-|\/|\*|\%)', Operator),
+
+        # Assignment Operators
+        (r'(\=)', Operator),
+
+        # Other Operators
+        (r'(\!|\<\<|\<|\&|\|)', Operator),
+
+        # Identifiers
+        (r'\b\w+\b', Name),
+
+        # Whitespace
+        (r'[ \t\r]+\n*|\n+', Whitespace),
+      ],
+
+      # Declare (nested rules)
+      "decl": [
+        (r'\b[a-z_]\w*\b(?!\!)', Keyword.Declaration),
+        (r':', Punctuation, "#pop"),
+        (r'\n', Whitespace, "#pop"),
+        include("root"),
+      ],
+
+      # Double-Quote String (nested rules)
+      "string.double": [
+        (r'\\\(', String.Interpol, "string.interpolation"),
+        (r'\\u[0-9a-fA-F]{4}', String.Escape),
+        (r'\\x[0-9a-fA-F]{2}', String.Escape),
+        (r'\\[bfnrt\\\']', String.Escape),
+        (r'\\"', String.Escape),
+        (r'"', String.Double, "#pop"),
+        (r'[^\\"]+', String.Double),
+        (r'.', Error),
+      ],
+
+      # Single-Char String (nested rules)
+      "string.char": [
+        (r'\\u[0-9a-fA-F]{4}', String.Escape),
+        (r'\\x[0-9a-fA-F]{2}', String.Escape),
+        (r'\\[bfnrt\\\']', String.Escape),
+        (r"\\'", String.Escape),
+        (r"'", String.Char, "#pop"),
+        (r"[^\\']+", String.Char),
+        (r'.', Error),
+      ],
+
+      # Interpolation inside String (nested rules)
+      "string.interpolation": [
+        (r"\)", String.Interpol, "#pop"),
+        include("root"),
+      ]
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/scdoc.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/scdoc.py
new file mode 100644
index 00000000..8e850d02
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/scdoc.py
@@ -0,0 +1,85 @@
+"""
+    pygments.lexers.scdoc
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    Lexer for scdoc, a simple man page generator.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, include, bygroups, using, this
+from pygments.token import Text, Comment, Keyword, String, Generic
+
+__all__ = ['ScdocLexer']
+
+
+class ScdocLexer(RegexLexer):
+    """
+    `scdoc` is a simple man page generator for POSIX systems written in C99.
+    """
+    name = 'scdoc'
+    url = 'https://git.sr.ht/~sircmpwn/scdoc'
+    aliases = ['scdoc', 'scd']
+    filenames = ['*.scd', '*.scdoc']
+    version_added = '2.5'
+    flags = re.MULTILINE
+
+    tokens = {
+        'root': [
+            # comment
+            (r'^(;.+\n)', bygroups(Comment)),
+
+            # heading with pound prefix
+            (r'^(#)([^#].+\n)', bygroups(Generic.Heading, Text)),
+            (r'^(#{2})(.+\n)', bygroups(Generic.Subheading, Text)),
+            # bulleted lists
+            (r'^(\s*)([*-])(\s)(.+\n)',
+            bygroups(Text, Keyword, Text, using(this, state='inline'))),
+            # numbered lists
+            (r'^(\s*)(\.+\.)( .+\n)',
+            bygroups(Text, Keyword, using(this, state='inline'))),
+            # quote
+            (r'^(\s*>\s)(.+\n)', bygroups(Keyword, Generic.Emph)),
+            # text block
+            (r'^(```\n)([\w\W]*?)(^```$)', bygroups(String, Text, String)),
+
+            include('inline'),
+        ],
+        'inline': [
+            # escape
+            (r'\\.', Text),
+            # underlines
+            (r'(\s)(_[^_]+_)(\W|\n)', bygroups(Text, Generic.Emph, Text)),
+            # bold
+            (r'(\s)(\*[^*]+\*)(\W|\n)', bygroups(Text, Generic.Strong, Text)),
+            # inline code
+            (r'`[^`]+`', String.Backtick),
+
+            # general text, must come last!
+            (r'[^\\\s]+', Text),
+            (r'.', Text),
+        ],
+    }
+
+    def analyse_text(text):
+        """We checks for bold and underline text with * and _. Also
+        every scdoc file must start with a strictly defined first line."""
+        result = 0
+
+        if '*' in text:
+            result += 0.01
+
+        if '_' in text:
+            result += 0.01
+
+        # name(section) ["left_footer" ["center_header"]]
+        first_line = text.partition('\n')[0]
+        scdoc_preamble_pattern = r'^.*\([1-7]\)( "[^"]+"){0,2}$'
+
+        if re.search(scdoc_preamble_pattern, first_line):
+            result += 0.5
+
+        return result
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/scripting.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/scripting.py
new file mode 100644
index 00000000..6e494c33
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/scripting.py
@@ -0,0 +1,1616 @@
+"""
+    pygments.lexers.scripting
+    ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexer for scripting and embedded languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, include, bygroups, default, combined, \
+    words
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation, Error, Whitespace, Other
+from pygments.util import get_bool_opt, get_list_opt
+
+__all__ = ['LuaLexer', 'LuauLexer', 'MoonScriptLexer', 'ChaiscriptLexer', 'LSLLexer',
+           'AppleScriptLexer', 'RexxLexer', 'MOOCodeLexer', 'HybrisLexer',
+           'EasytrieveLexer', 'JclLexer', 'MiniScriptLexer']
+
+
+def all_lua_builtins():
+    from pygments.lexers._lua_builtins import MODULES
+    return [w for values in MODULES.values() for w in values]
+
+class LuaLexer(RegexLexer):
+    """
+    For Lua source code.
+
+    Additional options accepted:
+
+    `func_name_highlighting`
+        If given and ``True``, highlight builtin function names
+        (default: ``True``).
+    `disabled_modules`
+        If given, must be a list of module names whose function names
+        should not be highlighted. By default all modules are highlighted.
+
+        To get a list of allowed modules have a look into the
+        `_lua_builtins` module:
+
+        .. sourcecode:: pycon
+
+            >>> from pygments.lexers._lua_builtins import MODULES
+            >>> MODULES.keys()
+            ['string', 'coroutine', 'modules', 'io', 'basic', ...]
+    """
+
+    name = 'Lua'
+    url = 'https://www.lua.org/'
+    aliases = ['lua']
+    filenames = ['*.lua', '*.wlua']
+    mimetypes = ['text/x-lua', 'application/x-lua']
+    version_added = ''
+
+    _comment_multiline = r'(?:--\[(?P=*)\[[\w\W]*?\](?P=level)\])'
+    _comment_single = r'(?:--.*$)'
+    _space = r'(?:\s+(?!\s))'
+    _s = rf'(?:{_comment_multiline}|{_comment_single}|{_space})'
+    _name = r'(?:[^\W\d]\w*)'
+
+    tokens = {
+        'root': [
+            # Lua allows a file to start with a shebang.
+            (r'#!.*', Comment.Preproc),
+            default('base'),
+        ],
+        'ws': [
+            (_comment_multiline, Comment.Multiline),
+            (_comment_single, Comment.Single),
+            (_space, Whitespace),
+        ],
+        'base': [
+            include('ws'),
+
+            (r'(?i)0x[\da-f]*(\.[\da-f]*)?(p[+-]?\d+)?', Number.Hex),
+            (r'(?i)(\d*\.\d+|\d+\.\d*)(e[+-]?\d+)?', Number.Float),
+            (r'(?i)\d+e[+-]?\d+', Number.Float),
+            (r'\d+', Number.Integer),
+
+            # multiline strings
+            (r'(?s)\[(=*)\[.*?\]\1\]', String),
+
+            (r'::', Punctuation, 'label'),
+            (r'\.{3}', Punctuation),
+            (r'[=<>|~&+\-*/%#^]+|\.\.', Operator),
+            (r'[\[\]{}().,:;]+', Punctuation),
+            (r'(and|or|not)\b', Operator.Word),
+
+            (words([
+                'break', 'do', 'else', 'elseif', 'end', 'for', 'if', 'in',
+                'repeat', 'return', 'then', 'until', 'while'
+            ], suffix=r'\b'), Keyword.Reserved),
+            (r'goto\b', Keyword.Reserved, 'goto'),
+            (r'(local)\b', Keyword.Declaration),
+            (r'(true|false|nil)\b', Keyword.Constant),
+
+            (r'(function)\b', Keyword.Reserved, 'funcname'),
+
+            (words(all_lua_builtins(), suffix=r"\b"), Name.Builtin),
+            (fr'[A-Za-z_]\w*(?={_s}*[.:])', Name.Variable, 'varname'),
+            (fr'[A-Za-z_]\w*(?={_s}*\()', Name.Function),
+            (r'[A-Za-z_]\w*', Name.Variable),
+
+            ("'", String.Single, combined('stringescape', 'sqs')),
+            ('"', String.Double, combined('stringescape', 'dqs'))
+        ],
+
+        'varname': [
+            include('ws'),
+            (r'\.\.', Operator, '#pop'),
+            (r'[.:]', Punctuation),
+            (rf'{_name}(?={_s}*[.:])', Name.Property),
+            (rf'{_name}(?={_s}*\()', Name.Function, '#pop'),
+            (_name, Name.Property, '#pop'),
+        ],
+
+        'funcname': [
+            include('ws'),
+            (r'[.:]', Punctuation),
+            (rf'{_name}(?={_s}*[.:])', Name.Class),
+            (_name, Name.Function, '#pop'),
+            # inline function
+            (r'\(', Punctuation, '#pop'),
+        ],
+
+        'goto': [
+            include('ws'),
+            (_name, Name.Label, '#pop'),
+        ],
+
+        'label': [
+            include('ws'),
+            (r'::', Punctuation, '#pop'),
+            (_name, Name.Label),
+        ],
+
+        'stringescape': [
+            (r'\\([abfnrtv\\"\']|[\r\n]{1,2}|z\s*|x[0-9a-fA-F]{2}|\d{1,3}|'
+             r'u\{[0-9a-fA-F]+\})', String.Escape),
+        ],
+
+        'sqs': [
+            (r"'", String.Single, '#pop'),
+            (r"[^\\']+", String.Single),
+        ],
+
+        'dqs': [
+            (r'"', String.Double, '#pop'),
+            (r'[^\\"]+', String.Double),
+        ]
+    }
+
+    def __init__(self, **options):
+        self.func_name_highlighting = get_bool_opt(
+            options, 'func_name_highlighting', True)
+        self.disabled_modules = get_list_opt(options, 'disabled_modules', [])
+
+        self._functions = set()
+        if self.func_name_highlighting:
+            from pygments.lexers._lua_builtins import MODULES
+            for mod, func in MODULES.items():
+                if mod not in self.disabled_modules:
+                    self._functions.update(func)
+        RegexLexer.__init__(self, **options)
+
+    def get_tokens_unprocessed(self, text):
+        for index, token, value in \
+                RegexLexer.get_tokens_unprocessed(self, text):
+            if token is Name.Builtin and value not in self._functions:
+                if '.' in value:
+                    a, b = value.split('.')
+                    yield index, Name, a
+                    yield index + len(a), Punctuation, '.'
+                    yield index + len(a) + 1, Name, b
+                else:
+                    yield index, Name, value
+                continue
+            yield index, token, value
+
+def _luau_make_expression(should_pop, _s):
+    temp_list = [
+        (r'0[xX][\da-fA-F_]*', Number.Hex, '#pop'),
+        (r'0[bB][\d_]*', Number.Bin, '#pop'),
+        (r'\.?\d[\d_]*(?:\.[\d_]*)?(?:[eE][+-]?[\d_]+)?', Number.Float, '#pop'),
+
+        (words((
+            'true', 'false', 'nil'
+        ), suffix=r'\b'), Keyword.Constant, '#pop'),
+
+        (r'\[(=*)\[[.\n]*?\]\1\]', String, '#pop'),
+
+        (r'(\.)([a-zA-Z_]\w*)(?=%s*[({"\'])', bygroups(Punctuation, Name.Function), '#pop'),
+        (r'(\.)([a-zA-Z_]\w*)', bygroups(Punctuation, Name.Variable), '#pop'),
+
+        (rf'[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*(?={_s}*[({{"\'])', Name.Other, '#pop'),
+        (r'[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*', Name, '#pop'),
+    ]
+    if should_pop:
+        return temp_list
+    return [entry[:2] for entry in temp_list]
+
+def _luau_make_expression_special(should_pop):
+    temp_list = [
+        (r'\{', Punctuation, ('#pop', 'closing_brace_base', 'expression')),
+        (r'\(', Punctuation, ('#pop', 'closing_parenthesis_base', 'expression')),
+
+        (r'::?', Punctuation, ('#pop', 'type_end', 'type_start')),
+
+        (r"'", String.Single, ('#pop', 'string_single')),
+        (r'"', String.Double, ('#pop', 'string_double')),
+        (r'`', String.Backtick, ('#pop', 'string_interpolated')),
+    ]
+    if should_pop:
+        return temp_list
+    return [(entry[0], entry[1], entry[2][1:]) for entry in temp_list]
+
+class LuauLexer(RegexLexer):
+    """
+    For Luau source code.
+
+    Additional options accepted:
+
+    `include_luau_builtins`
+        If given and ``True``, automatically highlight Luau builtins
+        (default: ``True``).
+    `include_roblox_builtins`
+        If given and ``True``, automatically highlight Roblox-specific builtins
+        (default: ``False``).
+    `additional_builtins`
+        If given, must be a list of additional builtins to highlight.
+    `disabled_builtins`
+        If given, must be a list of builtins that will not be highlighted.
+    """
+
+    name = 'Luau'
+    url = 'https://luau-lang.org/'
+    aliases = ['luau']
+    filenames = ['*.luau']
+    version_added = '2.18'
+
+    _comment_multiline = r'(?:--\[(?P=*)\[[\w\W]*?\](?P=level)\])'
+    _comment_single = r'(?:--.*$)'
+    _s = r'(?:{}|{}|{})'.format(_comment_multiline, _comment_single, r'\s+')
+
+    tokens = {
+        'root': [
+            (r'#!.*', Comment.Hashbang, 'base'),
+            default('base'),
+        ],
+
+        'ws': [
+            (_comment_multiline, Comment.Multiline),
+            (_comment_single, Comment.Single),
+            (r'\s+', Whitespace),
+        ],
+
+        'base': [
+            include('ws'),
+
+            *_luau_make_expression_special(False),
+            (r'\.\.\.', Punctuation),
+
+            (rf'type\b(?={_s}+[a-zA-Z_])', Keyword.Reserved, 'type_declaration'),
+            (rf'export\b(?={_s}+[a-zA-Z_])', Keyword.Reserved),
+
+            (r'(?:\.\.|//|[+\-*\/%^<>=])=?', Operator, 'expression'),
+            (r'~=', Operator, 'expression'),
+
+            (words((
+                'and', 'or', 'not'
+            ), suffix=r'\b'), Operator.Word, 'expression'),
+
+            (words((
+                'elseif', 'for', 'if', 'in', 'repeat', 'return', 'until',
+                'while'), suffix=r'\b'), Keyword.Reserved, 'expression'),
+            (r'local\b', Keyword.Declaration, 'expression'),
+
+            (r'function\b', Keyword.Reserved, ('expression', 'func_name')),
+
+            (r'[\])};]+', Punctuation),
+
+            include('expression_static'),
+            *_luau_make_expression(False, _s),
+
+            (r'[\[.,]', Punctuation, 'expression'),
+        ],
+        'expression_static': [
+            (words((
+                'break', 'continue', 'do', 'else', 'elseif', 'end', 'for',
+                'if', 'in', 'repeat', 'return', 'then', 'until', 'while'),
+                suffix=r'\b'), Keyword.Reserved),
+        ],
+        'expression': [
+            include('ws'),
+
+            (r'if\b', Keyword.Reserved, ('ternary', 'expression')),
+
+            (r'local\b', Keyword.Declaration),
+            *_luau_make_expression_special(True),
+            (r'\.\.\.', Punctuation, '#pop'),
+
+            (r'function\b', Keyword.Reserved, 'func_name'),
+
+            include('expression_static'),
+            *_luau_make_expression(True, _s),
+
+            default('#pop'),
+        ],
+        'ternary': [
+            include('ws'),
+
+            (r'else\b', Keyword.Reserved, '#pop'),
+            (words((
+                'then', 'elseif',
+            ), suffix=r'\b'), Operator.Reserved, 'expression'),
+
+            default('#pop'),
+        ],
+
+        'closing_brace_pop': [
+            (r'\}', Punctuation, '#pop'),
+        ],
+        'closing_parenthesis_pop': [
+            (r'\)', Punctuation, '#pop'),
+        ],
+        'closing_gt_pop': [
+            (r'>', Punctuation, '#pop'),
+        ],
+
+        'closing_parenthesis_base': [
+            include('closing_parenthesis_pop'),
+            include('base'),
+        ],
+        'closing_parenthesis_type': [
+            include('closing_parenthesis_pop'),
+            include('type'),
+        ],
+        'closing_brace_base': [
+            include('closing_brace_pop'),
+            include('base'),
+        ],
+        'closing_brace_type': [
+            include('closing_brace_pop'),
+            include('type'),
+        ],
+        'closing_gt_type': [
+            include('closing_gt_pop'),
+            include('type'),
+        ],
+
+        'string_escape': [
+            (r'\\z\s*', String.Escape),
+            (r'\\(?:[abfnrtvz\\"\'`\{\n])|[\r\n]{1,2}|x[\da-fA-F]{2}|\d{1,3}|'
+             r'u\{\}[\da-fA-F]*\}', String.Escape),
+        ],
+        'string_single': [
+            include('string_escape'),
+
+            (r"'", String.Single, "#pop"),
+            (r"[^\\']+", String.Single),
+        ],
+        'string_double': [
+            include('string_escape'),
+
+            (r'"', String.Double, "#pop"),
+            (r'[^\\"]+', String.Double),
+        ],
+        'string_interpolated': [
+            include('string_escape'),
+
+            (r'\{', Punctuation, ('closing_brace_base', 'expression')),
+
+            (r'`', String.Backtick, "#pop"),
+            (r'[^\\`\{]+', String.Backtick),
+        ],
+
+        'func_name': [
+            include('ws'),
+
+            (r'[.:]', Punctuation),
+            (rf'[a-zA-Z_]\w*(?={_s}*[.:])', Name.Class),
+            (r'[a-zA-Z_]\w*', Name.Function),
+
+            (r'<', Punctuation, 'closing_gt_type'),
+
+            (r'\(', Punctuation, '#pop'),
+        ],
+
+        'type': [
+            include('ws'),
+
+            (r'\(', Punctuation, 'closing_parenthesis_type'),
+            (r'\{', Punctuation, 'closing_brace_type'),
+            (r'<', Punctuation, 'closing_gt_type'),
+
+            (r"'", String.Single, 'string_single'),
+            (r'"', String.Double, 'string_double'),
+
+            (r'[|&\.,\[\]:=]+', Punctuation),
+            (r'->', Punctuation),
+
+            (r'typeof\(', Name.Builtin, ('closing_parenthesis_base',
+                                         'expression')),
+            (r'[a-zA-Z_]\w*', Name.Class),
+        ],
+        'type_start': [
+            include('ws'),
+
+            (r'\(', Punctuation, ('#pop', 'closing_parenthesis_type')),
+            (r'\{', Punctuation, ('#pop', 'closing_brace_type')),
+            (r'<', Punctuation, ('#pop', 'closing_gt_type')),
+
+            (r"'", String.Single, ('#pop', 'string_single')),
+            (r'"', String.Double, ('#pop', 'string_double')),
+
+            (r'typeof\(', Name.Builtin, ('#pop', 'closing_parenthesis_base',
+                                         'expression')),
+            (r'[a-zA-Z_]\w*', Name.Class, '#pop'),
+        ],
+        'type_end': [
+            include('ws'),
+
+            (r'[|&\.]', Punctuation, 'type_start'),
+            (r'->', Punctuation, 'type_start'),
+
+            (r'<', Punctuation, 'closing_gt_type'),
+
+            default('#pop'),
+        ],
+        'type_declaration': [
+            include('ws'),
+
+            (r'[a-zA-Z_]\w*', Name.Class),
+            (r'<', Punctuation, 'closing_gt_type'),
+
+            (r'=', Punctuation, ('#pop', 'type_end', 'type_start')),
+        ],
+    }
+
+    def __init__(self, **options):
+        self.include_luau_builtins = get_bool_opt(
+            options, 'include_luau_builtins', True)
+        self.include_roblox_builtins = get_bool_opt(
+            options, 'include_roblox_builtins', False)
+        self.additional_builtins = get_list_opt(options, 'additional_builtins', [])
+        self.disabled_builtins = get_list_opt(options, 'disabled_builtins', [])
+
+        self._builtins = set(self.additional_builtins)
+        if self.include_luau_builtins:
+            from pygments.lexers._luau_builtins import LUAU_BUILTINS
+            self._builtins.update(LUAU_BUILTINS)
+        if self.include_roblox_builtins:
+            from pygments.lexers._luau_builtins import ROBLOX_BUILTINS
+            self._builtins.update(ROBLOX_BUILTINS)
+        if self.additional_builtins:
+            self._builtins.update(self.additional_builtins)
+        self._builtins.difference_update(self.disabled_builtins)
+
+        RegexLexer.__init__(self, **options)
+
+    def get_tokens_unprocessed(self, text):
+        for index, token, value in \
+                RegexLexer.get_tokens_unprocessed(self, text):
+            if token is Name or token is Name.Other:
+                split_value = value.split('.')
+                complete_value = []
+                new_index = index
+                for position in range(len(split_value), 0, -1):
+                    potential_string = '.'.join(split_value[:position])
+                    if potential_string in self._builtins:
+                        yield index, Name.Builtin, potential_string
+                        new_index += len(potential_string)
+
+                        if complete_value:
+                            yield new_index, Punctuation, '.'
+                            new_index += 1
+                        break
+                    complete_value.insert(0, split_value[position - 1])
+
+                for position, substring in enumerate(complete_value):
+                    if position + 1 == len(complete_value):
+                        if token is Name:
+                            yield new_index, Name.Variable, substring
+                            continue
+                        yield new_index, Name.Function, substring
+                        continue
+                    yield new_index, Name.Variable, substring
+                    new_index += len(substring)
+                    yield new_index, Punctuation, '.'
+                    new_index += 1
+
+                continue
+            yield index, token, value
+
+class MoonScriptLexer(LuaLexer):
+    """
+    For MoonScript source code.
+    """
+
+    name = 'MoonScript'
+    url = 'http://moonscript.org'
+    aliases = ['moonscript', 'moon']
+    filenames = ['*.moon']
+    mimetypes = ['text/x-moonscript', 'application/x-moonscript']
+    version_added = '1.5'
+
+    tokens = {
+        'root': [
+            (r'#!(.*?)$', Comment.Preproc),
+            default('base'),
+        ],
+        'base': [
+            ('--.*$', Comment.Single),
+            (r'(?i)(\d*\.\d+|\d+\.\d*)(e[+-]?\d+)?', Number.Float),
+            (r'(?i)\d+e[+-]?\d+', Number.Float),
+            (r'(?i)0x[0-9a-f]*', Number.Hex),
+            (r'\d+', Number.Integer),
+            (r'\n', Whitespace),
+            (r'[^\S\n]+', Text),
+            (r'(?s)\[(=*)\[.*?\]\1\]', String),
+            (r'(->|=>)', Name.Function),
+            (r':[a-zA-Z_]\w*', Name.Variable),
+            (r'(==|!=|~=|<=|>=|\.\.\.|\.\.|[=+\-*/%^<>#!.\\:])', Operator),
+            (r'[;,]', Punctuation),
+            (r'[\[\]{}()]', Keyword.Type),
+            (r'[a-zA-Z_]\w*:', Name.Variable),
+            (words((
+                'class', 'extends', 'if', 'then', 'super', 'do', 'with',
+                'import', 'export', 'while', 'elseif', 'return', 'for', 'in',
+                'from', 'when', 'using', 'else', 'and', 'or', 'not', 'switch',
+                'break'), suffix=r'\b'),
+             Keyword),
+            (r'(true|false|nil)\b', Keyword.Constant),
+            (r'(and|or|not)\b', Operator.Word),
+            (r'(self)\b', Name.Builtin.Pseudo),
+            (r'@@?([a-zA-Z_]\w*)?', Name.Variable.Class),
+            (r'[A-Z]\w*', Name.Class),  # proper name
+            (words(all_lua_builtins(), suffix=r"\b"), Name.Builtin),
+            (r'[A-Za-z_]\w*', Name),
+            ("'", String.Single, combined('stringescape', 'sqs')),
+            ('"', String.Double, combined('stringescape', 'dqs'))
+        ],
+        'stringescape': [
+            (r'''\\([abfnrtv\\"']|\d{1,3})''', String.Escape)
+        ],
+        'sqs': [
+            ("'", String.Single, '#pop'),
+            ("[^']+", String)
+        ],
+        'dqs': [
+            ('"', String.Double, '#pop'),
+            ('[^"]+', String)
+        ]
+    }
+
+    def get_tokens_unprocessed(self, text):
+        # set . as Operator instead of Punctuation
+        for index, token, value in LuaLexer.get_tokens_unprocessed(self, text):
+            if token == Punctuation and value == ".":
+                token = Operator
+            yield index, token, value
+
+
+class ChaiscriptLexer(RegexLexer):
+    """
+    For ChaiScript source code.
+    """
+
+    name = 'ChaiScript'
+    url = 'http://chaiscript.com/'
+    aliases = ['chaiscript', 'chai']
+    filenames = ['*.chai']
+    mimetypes = ['text/x-chaiscript', 'application/x-chaiscript']
+    version_added = '2.0'
+
+    flags = re.DOTALL | re.MULTILINE
+
+    tokens = {
+        'commentsandwhitespace': [
+            (r'\s+', Text),
+            (r'//.*?\n', Comment.Single),
+            (r'/\*.*?\*/', Comment.Multiline),
+            (r'^\#.*?\n', Comment.Single)
+        ],
+        'slashstartsregex': [
+            include('commentsandwhitespace'),
+            (r'/(\\.|[^[/\\\n]|\[(\\.|[^\]\\\n])*])+/'
+             r'([gim]+\b|\B)', String.Regex, '#pop'),
+            (r'(?=/)', Text, ('#pop', 'badregex')),
+            default('#pop')
+        ],
+        'badregex': [
+            (r'\n', Text, '#pop')
+        ],
+        'root': [
+            include('commentsandwhitespace'),
+            (r'\n', Text),
+            (r'[^\S\n]+', Text),
+            (r'\+\+|--|~|&&|\?|:|\|\||\\(?=\n)|\.\.'
+             r'(<<|>>>?|==?|!=?|[-<>+*%&|^/])=?', Operator, 'slashstartsregex'),
+            (r'[{(\[;,]', Punctuation, 'slashstartsregex'),
+            (r'[})\].]', Punctuation),
+            (r'[=+\-*/]', Operator),
+            (r'(for|in|while|do|break|return|continue|if|else|'
+             r'throw|try|catch'
+             r')\b', Keyword, 'slashstartsregex'),
+            (r'(var)\b', Keyword.Declaration, 'slashstartsregex'),
+            (r'(attr|def|fun)\b', Keyword.Reserved),
+            (r'(true|false)\b', Keyword.Constant),
+            (r'(eval|throw)\b', Name.Builtin),
+            (r'`\S+`', Name.Builtin),
+            (r'[$a-zA-Z_]\w*', Name.Other),
+            (r'[0-9][0-9]*\.[0-9]+([eE][0-9]+)?[fd]?', Number.Float),
+            (r'0x[0-9a-fA-F]+', Number.Hex),
+            (r'[0-9]+', Number.Integer),
+            (r'"', String.Double, 'dqstring'),
+            (r"'(\\\\|\\[^\\]|[^'\\])*'", String.Single),
+        ],
+        'dqstring': [
+            (r'\$\{[^"}]+?\}', String.Interpol),
+            (r'\$', String.Double),
+            (r'\\\\', String.Double),
+            (r'\\"', String.Double),
+            (r'[^\\"$]+', String.Double),
+            (r'"', String.Double, '#pop'),
+        ],
+    }
+
+
+class LSLLexer(RegexLexer):
+    """
+    For Second Life's Linden Scripting Language source code.
+    """
+
+    name = 'LSL'
+    aliases = ['lsl']
+    filenames = ['*.lsl']
+    mimetypes = ['text/x-lsl']
+    url = 'https://wiki.secondlife.com/wiki/Linden_Scripting_Language'
+    version_added = '2.0'
+
+    flags = re.MULTILINE
+
+    lsl_keywords = r'\b(?:do|else|for|if|jump|return|while)\b'
+    lsl_types = r'\b(?:float|integer|key|list|quaternion|rotation|string|vector)\b'
+    lsl_states = r'\b(?:(?:state)\s+\w+|default)\b'
+    lsl_events = r'\b(?:state_(?:entry|exit)|touch(?:_(?:start|end))?|(?:land_)?collision(?:_(?:start|end))?|timer|listen|(?:no_)?sensor|control|(?:not_)?at_(?:rot_)?target|money|email|run_time_permissions|changed|attach|dataserver|moving_(?:start|end)|link_message|(?:on|object)_rez|remote_data|http_re(?:sponse|quest)|path_update|transaction_result)\b'
+    lsl_functions_builtin = r'\b(?:ll(?:ReturnObjectsBy(?:ID|Owner)|Json(?:2List|[GS]etValue|ValueType)|Sin|Cos|Tan|Atan2|Sqrt|Pow|Abs|Fabs|Frand|Floor|Ceil|Round|Vec(?:Mag|Norm|Dist)|Rot(?:Between|2(?:Euler|Fwd|Left|Up))|(?:Euler|Axes)2Rot|Whisper|(?:Region|Owner)?Say|Shout|Listen(?:Control|Remove)?|Sensor(?:Repeat|Remove)?|Detected(?:Name|Key|Owner|Type|Pos|Vel|Grab|Rot|Group|LinkNumber)|Die|Ground|Wind|(?:[GS]et)(?:AnimationOverride|MemoryLimit|PrimMediaParams|ParcelMusicURL|Object(?:Desc|Name)|PhysicsMaterial|Status|Scale|Color|Alpha|Texture|Pos|Rot|Force|Torque)|ResetAnimationOverride|(?:Scale|Offset|Rotate)Texture|(?:Rot)?Target(?:Remove)?|(?:Stop)?MoveToTarget|Apply(?:Rotational)?Impulse|Set(?:KeyframedMotion|ContentType|RegionPos|(?:Angular)?Velocity|Buoyancy|HoverHeight|ForceAndTorque|TimerEvent|ScriptState|Damage|TextureAnim|Sound(?:Queueing|Radius)|Vehicle(?:Type|(?:Float|Vector|Rotation)Param)|(?:Touch|Sit)?Text|Camera(?:Eye|At)Offset|PrimitiveParams|ClickAction|Link(?:Alpha|Color|PrimitiveParams(?:Fast)?|Texture(?:Anim)?|Camera|Media)|RemoteScriptAccessPin|PayPrice|LocalRot)|ScaleByFactor|Get(?:(?:Max|Min)ScaleFactor|ClosestNavPoint|StaticPath|SimStats|Env|PrimitiveParams|Link(?:PrimitiveParams|Number(?:OfSides)?|Key|Name|Media)|HTTPHeader|FreeURLs|Object(?:Details|PermMask|PrimCount)|Parcel(?:MaxPrims|Details|Prim(?:Count|Owners))|Attached|(?:SPMax|Free|Used)Memory|Region(?:Name|TimeDilation|FPS|Corner|AgentCount)|Root(?:Position|Rotation)|UnixTime|(?:Parcel|Region)Flags|(?:Wall|GMT)clock|SimulatorHostname|BoundingBox|GeometricCenter|Creator|NumberOf(?:Prims|NotecardLines|Sides)|Animation(?:List)?|(?:Camera|Local)(?:Pos|Rot)|Vel|Accel|Omega|Time(?:stamp|OfDay)|(?:Object|CenterOf)?Mass|MassMKS|Energy|Owner|(?:Owner)?Key|SunDirection|Texture(?:Offset|Scale|Rot)|Inventory(?:Number|Name|Key|Type|Creator|PermMask)|Permissions(?:Key)?|StartParameter|List(?:Length|EntryType)|Date|Agent(?:Size|Info|Language|List)|LandOwnerAt|NotecardLine|Script(?:Name|State))|(?:Get|Reset|GetAndReset)Time|PlaySound(?:Slave)?|LoopSound(?:Master|Slave)?|(?:Trigger|Stop|Preload)Sound|(?:(?:Get|Delete)Sub|Insert)String|To(?:Upper|Lower)|Give(?:InventoryList|Money)|RezObject|(?:Stop)?LookAt|Sleep|CollisionFilter|(?:Take|Release)Controls|DetachFromAvatar|AttachToAvatar(?:Temp)?|InstantMessage|(?:GetNext)?Email|StopHover|MinEventDelay|RotLookAt|String(?:Length|Trim)|(?:Start|Stop)Animation|TargetOmega|RequestPermissions|(?:Create|Break)Link|BreakAllLinks|(?:Give|Remove)Inventory|Water|PassTouches|Request(?:Agent|Inventory)Data|TeleportAgent(?:Home|GlobalCoords)?|ModifyLand|CollisionSound|ResetScript|MessageLinked|PushObject|PassCollisions|AxisAngle2Rot|Rot2(?:Axis|Angle)|A(?:cos|sin)|AngleBetween|AllowInventoryDrop|SubStringIndex|List2(?:CSV|Integer|Json|Float|String|Key|Vector|Rot|List(?:Strided)?)|DeleteSubList|List(?:Statistics|Sort|Randomize|(?:Insert|Find|Replace)List)|EdgeOfWorld|AdjustSoundVolume|Key2Name|TriggerSoundLimited|EjectFromLand|(?:CSV|ParseString)2List|OverMyLand|SameGroup|UnSit|Ground(?:Slope|Normal|Contour)|GroundRepel|(?:Set|Remove)VehicleFlags|(?:AvatarOn)?(?:Link)?SitTarget|Script(?:Danger|Profiler)|Dialog|VolumeDetect|ResetOtherScript|RemoteLoadScriptPin|(?:Open|Close)RemoteDataChannel|SendRemoteData|RemoteDataReply|(?:Integer|String)ToBase64|XorBase64|Log(?:10)?|Base64To(?:String|Integer)|ParseStringKeepNulls|RezAtRoot|RequestSimulatorData|ForceMouselook|(?:Load|Release|(?:E|Une)scape)URL|ParcelMedia(?:CommandList|Query)|ModPow|MapDestination|(?:RemoveFrom|AddTo|Reset)Land(?:Pass|Ban)List|(?:Set|Clear)CameraParams|HTTP(?:Request|Response)|TextBox|DetectedTouch(?:UV|Face|Pos|(?:N|Bin)ormal|ST)|(?:MD5|SHA1|DumpList2)String|Request(?:Secure)?URL|Clear(?:Prim|Link)Media|(?:Link)?ParticleSystem|(?:Get|Request)(?:Username|DisplayName)|RegionSayTo|CastRay|GenerateKey|TransferLindenDollars|ManageEstateAccess|(?:Create|Delete)Character|ExecCharacterCmd|Evade|FleeFrom|NavigateTo|PatrolPoints|Pursue|UpdateCharacter|WanderWithin))\b'
+    lsl_constants_float = r'\b(?:DEG_TO_RAD|PI(?:_BY_TWO)?|RAD_TO_DEG|SQRT2|TWO_PI)\b'
+    lsl_constants_integer = r'\b(?:JSON_APPEND|STATUS_(?:PHYSICS|ROTATE_[XYZ]|PHANTOM|SANDBOX|BLOCK_GRAB(?:_OBJECT)?|(?:DIE|RETURN)_AT_EDGE|CAST_SHADOWS|OK|MALFORMED_PARAMS|TYPE_MISMATCH|BOUNDS_ERROR|NOT_(?:FOUND|SUPPORTED)|INTERNAL_ERROR|WHITELIST_FAILED)|AGENT(?:_(?:BY_(?:LEGACY_|USER)NAME|FLYING|ATTACHMENTS|SCRIPTED|MOUSELOOK|SITTING|ON_OBJECT|AWAY|WALKING|IN_AIR|TYPING|CROUCHING|BUSY|ALWAYS_RUN|AUTOPILOT|LIST_(?:PARCEL(?:_OWNER)?|REGION)))?|CAMERA_(?:PITCH|DISTANCE|BEHINDNESS_(?:ANGLE|LAG)|(?:FOCUS|POSITION)(?:_(?:THRESHOLD|LOCKED|LAG))?|FOCUS_OFFSET|ACTIVE)|ANIM_ON|LOOP|REVERSE|PING_PONG|SMOOTH|ROTATE|SCALE|ALL_SIDES|LINK_(?:ROOT|SET|ALL_(?:OTHERS|CHILDREN)|THIS)|ACTIVE|PASSIVE|SCRIPTED|CONTROL_(?:FWD|BACK|(?:ROT_)?(?:LEFT|RIGHT)|UP|DOWN|(?:ML_)?LBUTTON)|PERMISSION_(?:RETURN_OBJECTS|DEBIT|OVERRIDE_ANIMATIONS|SILENT_ESTATE_MANAGEMENT|TAKE_CONTROLS|TRIGGER_ANIMATION|ATTACH|CHANGE_LINKS|(?:CONTROL|TRACK)_CAMERA|TELEPORT)|INVENTORY_(?:TEXTURE|SOUND|OBJECT|SCRIPT|LANDMARK|CLOTHING|NOTECARD|BODYPART|ANIMATION|GESTURE|ALL|NONE)|CHANGED_(?:INVENTORY|COLOR|SHAPE|SCALE|TEXTURE|LINK|ALLOWED_DROP|OWNER|REGION(?:_START)?|TELEPORT|MEDIA)|OBJECT_(?:(?:PHYSICS|SERVER|STREAMING)_COST|UNKNOWN_DETAIL|CHARACTER_TIME|PHANTOM|PHYSICS|TEMP_ON_REZ|NAME|DESC|POS|PRIM_EQUIVALENCE|RETURN_(?:PARCEL(?:_OWNER)?|REGION)|ROO?T|VELOCITY|OWNER|GROUP|CREATOR|ATTACHED_POINT|RENDER_WEIGHT|PATHFINDING_TYPE|(?:RUNNING|TOTAL)_SCRIPT_COUNT|SCRIPT_(?:MEMORY|TIME))|TYPE_(?:INTEGER|FLOAT|STRING|KEY|VECTOR|ROTATION|INVALID)|(?:DEBUG|PUBLIC)_CHANNEL|ATTACH_(?:AVATAR_CENTER|CHEST|HEAD|BACK|PELVIS|MOUTH|CHIN|NECK|NOSE|BELLY|[LR](?:SHOULDER|HAND|FOOT|EAR|EYE|[UL](?:ARM|LEG)|HIP)|(?:LEFT|RIGHT)_PEC|HUD_(?:CENTER_[12]|TOP_(?:RIGHT|CENTER|LEFT)|BOTTOM(?:_(?:RIGHT|LEFT))?))|LAND_(?:LEVEL|RAISE|LOWER|SMOOTH|NOISE|REVERT)|DATA_(?:ONLINE|NAME|BORN|SIM_(?:POS|STATUS|RATING)|PAYINFO)|PAYMENT_INFO_(?:ON_FILE|USED)|REMOTE_DATA_(?:CHANNEL|REQUEST|REPLY)|PSYS_(?:PART_(?:BF_(?:ZERO|ONE(?:_MINUS_(?:DEST_COLOR|SOURCE_(ALPHA|COLOR)))?|DEST_COLOR|SOURCE_(ALPHA|COLOR))|BLEND_FUNC_(DEST|SOURCE)|FLAGS|(?:START|END)_(?:COLOR|ALPHA|SCALE|GLOW)|MAX_AGE|(?:RIBBON|WIND|INTERP_(?:COLOR|SCALE)|BOUNCE|FOLLOW_(?:SRC|VELOCITY)|TARGET_(?:POS|LINEAR)|EMISSIVE)_MASK)|SRC_(?:MAX_AGE|PATTERN|ANGLE_(?:BEGIN|END)|BURST_(?:RATE|PART_COUNT|RADIUS|SPEED_(?:MIN|MAX))|ACCEL|TEXTURE|TARGET_KEY|OMEGA|PATTERN_(?:DROP|EXPLODE|ANGLE(?:_CONE(?:_EMPTY)?)?)))|VEHICLE_(?:REFERENCE_FRAME|TYPE_(?:NONE|SLED|CAR|BOAT|AIRPLANE|BALLOON)|(?:LINEAR|ANGULAR)_(?:FRICTION_TIMESCALE|MOTOR_DIRECTION)|LINEAR_MOTOR_OFFSET|HOVER_(?:HEIGHT|EFFICIENCY|TIMESCALE)|BUOYANCY|(?:LINEAR|ANGULAR)_(?:DEFLECTION_(?:EFFICIENCY|TIMESCALE)|MOTOR_(?:DECAY_)?TIMESCALE)|VERTICAL_ATTRACTION_(?:EFFICIENCY|TIMESCALE)|BANKING_(?:EFFICIENCY|MIX|TIMESCALE)|FLAG_(?:NO_DEFLECTION_UP|LIMIT_(?:ROLL_ONLY|MOTOR_UP)|HOVER_(?:(?:WATER|TERRAIN|UP)_ONLY|GLOBAL_HEIGHT)|MOUSELOOK_(?:STEER|BANK)|CAMERA_DECOUPLED))|PRIM_(?:TYPE(?:_(?:BOX|CYLINDER|PRISM|SPHERE|TORUS|TUBE|RING|SCULPT))?|HOLE_(?:DEFAULT|CIRCLE|SQUARE|TRIANGLE)|MATERIAL(?:_(?:STONE|METAL|GLASS|WOOD|FLESH|PLASTIC|RUBBER))?|SHINY_(?:NONE|LOW|MEDIUM|HIGH)|BUMP_(?:NONE|BRIGHT|DARK|WOOD|BARK|BRICKS|CHECKER|CONCRETE|TILE|STONE|DISKS|GRAVEL|BLOBS|SIDING|LARGETILE|STUCCO|SUCTION|WEAVE)|TEXGEN_(?:DEFAULT|PLANAR)|SCULPT_(?:TYPE_(?:SPHERE|TORUS|PLANE|CYLINDER|MASK)|FLAG_(?:MIRROR|INVERT))|PHYSICS(?:_(?:SHAPE_(?:CONVEX|NONE|PRIM|TYPE)))?|(?:POS|ROT)_LOCAL|SLICE|TEXT|FLEXIBLE|POINT_LIGHT|TEMP_ON_REZ|PHANTOM|POSITION|SIZE|ROTATION|TEXTURE|NAME|OMEGA|DESC|LINK_TARGET|COLOR|BUMP_SHINY|FULLBRIGHT|TEXGEN|GLOW|MEDIA_(?:ALT_IMAGE_ENABLE|CONTROLS|(?:CURRENT|HOME)_URL|AUTO_(?:LOOP|PLAY|SCALE|ZOOM)|FIRST_CLICK_INTERACT|(?:WIDTH|HEIGHT)_PIXELS|WHITELIST(?:_ENABLE)?|PERMS_(?:INTERACT|CONTROL)|PARAM_MAX|CONTROLS_(?:STANDARD|MINI)|PERM_(?:NONE|OWNER|GROUP|ANYONE)|MAX_(?:URL_LENGTH|WHITELIST_(?:SIZE|COUNT)|(?:WIDTH|HEIGHT)_PIXELS)))|MASK_(?:BASE|OWNER|GROUP|EVERYONE|NEXT)|PERM_(?:TRANSFER|MODIFY|COPY|MOVE|ALL)|PARCEL_(?:MEDIA_COMMAND_(?:STOP|PAUSE|PLAY|LOOP|TEXTURE|URL|TIME|AGENT|UNLOAD|AUTO_ALIGN|TYPE|SIZE|DESC|LOOP_SET)|FLAG_(?:ALLOW_(?:FLY|(?:GROUP_)?SCRIPTS|LANDMARK|TERRAFORM|DAMAGE|CREATE_(?:GROUP_)?OBJECTS)|USE_(?:ACCESS_(?:GROUP|LIST)|BAN_LIST|LAND_PASS_LIST)|LOCAL_SOUND_ONLY|RESTRICT_PUSHOBJECT|ALLOW_(?:GROUP|ALL)_OBJECT_ENTRY)|COUNT_(?:TOTAL|OWNER|GROUP|OTHER|SELECTED|TEMP)|DETAILS_(?:NAME|DESC|OWNER|GROUP|AREA|ID|SEE_AVATARS))|LIST_STAT_(?:MAX|MIN|MEAN|MEDIAN|STD_DEV|SUM(?:_SQUARES)?|NUM_COUNT|GEOMETRIC_MEAN|RANGE)|PAY_(?:HIDE|DEFAULT)|REGION_FLAG_(?:ALLOW_DAMAGE|FIXED_SUN|BLOCK_TERRAFORM|SANDBOX|DISABLE_(?:COLLISIONS|PHYSICS)|BLOCK_FLY|ALLOW_DIRECT_TELEPORT|RESTRICT_PUSHOBJECT)|HTTP_(?:METHOD|MIMETYPE|BODY_(?:MAXLENGTH|TRUNCATED)|CUSTOM_HEADER|PRAGMA_NO_CACHE|VERBOSE_THROTTLE|VERIFY_CERT)|STRING_(?:TRIM(?:_(?:HEAD|TAIL))?)|CLICK_ACTION_(?:NONE|TOUCH|SIT|BUY|PAY|OPEN(?:_MEDIA)?|PLAY|ZOOM)|TOUCH_INVALID_FACE|PROFILE_(?:NONE|SCRIPT_MEMORY)|RC_(?:DATA_FLAGS|DETECT_PHANTOM|GET_(?:LINK_NUM|NORMAL|ROOT_KEY)|MAX_HITS|REJECT_(?:TYPES|AGENTS|(?:NON)?PHYSICAL|LAND))|RCERR_(?:CAST_TIME_EXCEEDED|SIM_PERF_LOW|UNKNOWN)|ESTATE_ACCESS_(?:ALLOWED_(?:AGENT|GROUP)_(?:ADD|REMOVE)|BANNED_AGENT_(?:ADD|REMOVE))|DENSITY|FRICTION|RESTITUTION|GRAVITY_MULTIPLIER|KFM_(?:COMMAND|CMD_(?:PLAY|STOP|PAUSE|SET_MODE)|MODE|FORWARD|LOOP|PING_PONG|REVERSE|DATA|ROTATION|TRANSLATION)|ERR_(?:GENERIC|PARCEL_PERMISSIONS|MALFORMED_PARAMS|RUNTIME_PERMISSIONS|THROTTLED)|CHARACTER_(?:CMD_(?:(?:SMOOTH_)?STOP|JUMP)|DESIRED_(?:TURN_)?SPEED|RADIUS|STAY_WITHIN_PARCEL|LENGTH|ORIENTATION|ACCOUNT_FOR_SKIPPED_FRAMES|AVOIDANCE_MODE|TYPE(?:_(?:[A-D]|NONE))?|MAX_(?:DECEL|TURN_RADIUS|(?:ACCEL|SPEED)))|PURSUIT_(?:OFFSET|FUZZ_FACTOR|GOAL_TOLERANCE|INTERCEPT)|REQUIRE_LINE_OF_SIGHT|FORCE_DIRECT_PATH|VERTICAL|HORIZONTAL|AVOID_(?:CHARACTERS|DYNAMIC_OBSTACLES|NONE)|PU_(?:EVADE_(?:HIDDEN|SPOTTED)|FAILURE_(?:DYNAMIC_PATHFINDING_DISABLED|INVALID_(?:GOAL|START)|NO_(?:NAVMESH|VALID_DESTINATION)|OTHER|TARGET_GONE|(?:PARCEL_)?UNREACHABLE)|(?:GOAL|SLOWDOWN_DISTANCE)_REACHED)|TRAVERSAL_TYPE(?:_(?:FAST|NONE|SLOW))?|CONTENT_TYPE_(?:ATOM|FORM|HTML|JSON|LLSD|RSS|TEXT|XHTML|XML)|GCNP_(?:RADIUS|STATIC)|(?:PATROL|WANDER)_PAUSE_AT_WAYPOINTS|OPT_(?:AVATAR|CHARACTER|EXCLUSION_VOLUME|LEGACY_LINKSET|MATERIAL_VOLUME|OTHER|STATIC_OBSTACLE|WALKABLE)|SIM_STAT_PCT_CHARS_STEPPED)\b'
+    lsl_constants_integer_boolean = r'\b(?:FALSE|TRUE)\b'
+    lsl_constants_rotation = r'\b(?:ZERO_ROTATION)\b'
+    lsl_constants_string = r'\b(?:EOF|JSON_(?:ARRAY|DELETE|FALSE|INVALID|NULL|NUMBER|OBJECT|STRING|TRUE)|NULL_KEY|TEXTURE_(?:BLANK|DEFAULT|MEDIA|PLYWOOD|TRANSPARENT)|URL_REQUEST_(?:GRANTED|DENIED))\b'
+    lsl_constants_vector = r'\b(?:TOUCH_INVALID_(?:TEXCOORD|VECTOR)|ZERO_VECTOR)\b'
+    lsl_invalid_broken = r'\b(?:LAND_(?:LARGE|MEDIUM|SMALL)_BRUSH)\b'
+    lsl_invalid_deprecated = r'\b(?:ATTACH_[LR]PEC|DATA_RATING|OBJECT_ATTACHMENT_(?:GEOMETRY_BYTES|SURFACE_AREA)|PRIM_(?:CAST_SHADOWS|MATERIAL_LIGHT|TYPE_LEGACY)|PSYS_SRC_(?:INNER|OUTER)ANGLE|VEHICLE_FLAG_NO_FLY_UP|ll(?:Cloud|Make(?:Explosion|Fountain|Smoke|Fire)|RemoteDataSetRegion|Sound(?:Preload)?|XorBase64Strings(?:Correct)?))\b'
+    lsl_invalid_illegal = r'\b(?:event)\b'
+    lsl_invalid_unimplemented = r'\b(?:CHARACTER_(?:MAX_ANGULAR_(?:ACCEL|SPEED)|TURN_SPEED_MULTIPLIER)|PERMISSION_(?:CHANGE_(?:JOINTS|PERMISSIONS)|RELEASE_OWNERSHIP|REMAP_CONTROLS)|PRIM_PHYSICS_MATERIAL|PSYS_SRC_OBJ_REL_MASK|ll(?:CollisionSprite|(?:Stop)?PointAt|(?:(?:Refresh|Set)Prim)URL|(?:Take|Release)Camera|RemoteLoadScript))\b'
+    lsl_reserved_godmode = r'\b(?:ll(?:GodLikeRezObject|Set(?:Inventory|Object)PermMask))\b'
+    lsl_reserved_log = r'\b(?:print)\b'
+    lsl_operators = r'\+\+|\-\-|<<|>>|&&?|\|\|?|\^|~|[!%<>=*+\-/]=?'
+
+    tokens = {
+        'root':
+        [
+            (r'//.*?\n',                          Comment.Single),
+            (r'/\*',                              Comment.Multiline, 'comment'),
+            (r'"',                                String.Double, 'string'),
+            (lsl_keywords,                        Keyword),
+            (lsl_types,                           Keyword.Type),
+            (lsl_states,                          Name.Class),
+            (lsl_events,                          Name.Builtin),
+            (lsl_functions_builtin,               Name.Function),
+            (lsl_constants_float,                 Keyword.Constant),
+            (lsl_constants_integer,               Keyword.Constant),
+            (lsl_constants_integer_boolean,       Keyword.Constant),
+            (lsl_constants_rotation,              Keyword.Constant),
+            (lsl_constants_string,                Keyword.Constant),
+            (lsl_constants_vector,                Keyword.Constant),
+            (lsl_invalid_broken,                  Error),
+            (lsl_invalid_deprecated,              Error),
+            (lsl_invalid_illegal,                 Error),
+            (lsl_invalid_unimplemented,           Error),
+            (lsl_reserved_godmode,                Keyword.Reserved),
+            (lsl_reserved_log,                    Keyword.Reserved),
+            (r'\b([a-zA-Z_]\w*)\b',     Name.Variable),
+            (r'(\d+\.\d*|\.\d+|\d+)[eE][+-]?\d*', Number.Float),
+            (r'(\d+\.\d*|\.\d+)',                 Number.Float),
+            (r'0[xX][0-9a-fA-F]+',                Number.Hex),
+            (r'\d+',                              Number.Integer),
+            (lsl_operators,                       Operator),
+            (r':=?',                              Error),
+            (r'[,;{}()\[\]]',                     Punctuation),
+            (r'\n+',                              Whitespace),
+            (r'\s+',                              Whitespace)
+        ],
+        'comment':
+        [
+            (r'[^*/]+',                           Comment.Multiline),
+            (r'/\*',                              Comment.Multiline, '#push'),
+            (r'\*/',                              Comment.Multiline, '#pop'),
+            (r'[*/]',                             Comment.Multiline)
+        ],
+        'string':
+        [
+            (r'\\([nt"\\])',                      String.Escape),
+            (r'"',                                String.Double, '#pop'),
+            (r'\\.',                              Error),
+            (r'[^"\\]+',                          String.Double),
+        ]
+    }
+
+
+class AppleScriptLexer(RegexLexer):
+    """
+    For AppleScript source code,
+    including `AppleScript Studio
+    `_.
+    Contributed by Andreas Amann .
+    """
+
+    name = 'AppleScript'
+    url = 'https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/introduction/ASLR_intro.html'
+    aliases = ['applescript']
+    filenames = ['*.applescript']
+    version_added = '1.0'
+
+    flags = re.MULTILINE | re.DOTALL
+
+    Identifiers = r'[a-zA-Z]\w*'
+
+    # XXX: use words() for all of these
+    Literals = ('AppleScript', 'current application', 'false', 'linefeed',
+                'missing value', 'pi', 'quote', 'result', 'return', 'space',
+                'tab', 'text item delimiters', 'true', 'version')
+    Classes = ('alias ', 'application ', 'boolean ', 'class ', 'constant ',
+               'date ', 'file ', 'integer ', 'list ', 'number ', 'POSIX file ',
+               'real ', 'record ', 'reference ', 'RGB color ', 'script ',
+               'text ', 'unit types', '(?:Unicode )?text', 'string')
+    BuiltIn = ('attachment', 'attribute run', 'character', 'day', 'month',
+               'paragraph', 'word', 'year')
+    HandlerParams = ('about', 'above', 'against', 'apart from', 'around',
+                     'aside from', 'at', 'below', 'beneath', 'beside',
+                     'between', 'for', 'given', 'instead of', 'on', 'onto',
+                     'out of', 'over', 'since')
+    Commands = ('ASCII (character|number)', 'activate', 'beep', 'choose URL',
+                'choose application', 'choose color', 'choose file( name)?',
+                'choose folder', 'choose from list',
+                'choose remote application', 'clipboard info',
+                'close( access)?', 'copy', 'count', 'current date', 'delay',
+                'delete', 'display (alert|dialog)', 'do shell script',
+                'duplicate', 'exists', 'get eof', 'get volume settings',
+                'info for', 'launch', 'list (disks|folder)', 'load script',
+                'log', 'make', 'mount volume', 'new', 'offset',
+                'open( (for access|location))?', 'path to', 'print', 'quit',
+                'random number', 'read', 'round', 'run( script)?',
+                'say', 'scripting components',
+                'set (eof|the clipboard to|volume)', 'store script',
+                'summarize', 'system attribute', 'system info',
+                'the clipboard', 'time to GMT', 'write', 'quoted form')
+    References = ('(in )?back of', '(in )?front of', '[0-9]+(st|nd|rd|th)',
+                  'first', 'second', 'third', 'fourth', 'fifth', 'sixth',
+                  'seventh', 'eighth', 'ninth', 'tenth', 'after', 'back',
+                  'before', 'behind', 'every', 'front', 'index', 'last',
+                  'middle', 'some', 'that', 'through', 'thru', 'where', 'whose')
+    Operators = ("and", "or", "is equal", "equals", "(is )?equal to", "is not",
+                 "isn't", "isn't equal( to)?", "is not equal( to)?",
+                 "doesn't equal", "does not equal", "(is )?greater than",
+                 "comes after", "is not less than or equal( to)?",
+                 "isn't less than or equal( to)?", "(is )?less than",
+                 "comes before", "is not greater than or equal( to)?",
+                 "isn't greater than or equal( to)?",
+                 "(is  )?greater than or equal( to)?", "is not less than",
+                 "isn't less than", "does not come before",
+                 "doesn't come before", "(is )?less than or equal( to)?",
+                 "is not greater than", "isn't greater than",
+                 "does not come after", "doesn't come after", "starts? with",
+                 "begins? with", "ends? with", "contains?", "does not contain",
+                 "doesn't contain", "is in", "is contained by", "is not in",
+                 "is not contained by", "isn't contained by", "div", "mod",
+                 "not", "(a  )?(ref( to)?|reference to)", "is", "does")
+    Control = ('considering', 'else', 'error', 'exit', 'from', 'if',
+               'ignoring', 'in', 'repeat', 'tell', 'then', 'times', 'to',
+               'try', 'until', 'using terms from', 'while', 'whith',
+               'with timeout( of)?', 'with transaction', 'by', 'continue',
+               'end', 'its?', 'me', 'my', 'return', 'of', 'as')
+    Declarations = ('global', 'local', 'prop(erty)?', 'set', 'get')
+    Reserved = ('but', 'put', 'returning', 'the')
+    StudioClasses = ('action cell', 'alert reply', 'application', 'box',
+                     'browser( cell)?', 'bundle', 'button( cell)?', 'cell',
+                     'clip view', 'color well', 'color-panel',
+                     'combo box( item)?', 'control',
+                     'data( (cell|column|item|row|source))?', 'default entry',
+                     'dialog reply', 'document', 'drag info', 'drawer',
+                     'event', 'font(-panel)?', 'formatter',
+                     'image( (cell|view))?', 'matrix', 'menu( item)?', 'item',
+                     'movie( view)?', 'open-panel', 'outline view', 'panel',
+                     'pasteboard', 'plugin', 'popup button',
+                     'progress indicator', 'responder', 'save-panel',
+                     'scroll view', 'secure text field( cell)?', 'slider',
+                     'sound', 'split view', 'stepper', 'tab view( item)?',
+                     'table( (column|header cell|header view|view))',
+                     'text( (field( cell)?|view))?', 'toolbar( item)?',
+                     'user-defaults', 'view', 'window')
+    StudioEvents = ('accept outline drop', 'accept table drop', 'action',
+                    'activated', 'alert ended', 'awake from nib', 'became key',
+                    'became main', 'begin editing', 'bounds changed',
+                    'cell value', 'cell value changed', 'change cell value',
+                    'change item value', 'changed', 'child of item',
+                    'choose menu item', 'clicked', 'clicked toolbar item',
+                    'closed', 'column clicked', 'column moved',
+                    'column resized', 'conclude drop', 'data representation',
+                    'deminiaturized', 'dialog ended', 'document nib name',
+                    'double clicked', 'drag( (entered|exited|updated))?',
+                    'drop', 'end editing', 'exposed', 'idle', 'item expandable',
+                    'item value', 'item value changed', 'items changed',
+                    'keyboard down', 'keyboard up', 'launched',
+                    'load data representation', 'miniaturized', 'mouse down',
+                    'mouse dragged', 'mouse entered', 'mouse exited',
+                    'mouse moved', 'mouse up', 'moved',
+                    'number of browser rows', 'number of items',
+                    'number of rows', 'open untitled', 'opened', 'panel ended',
+                    'parameters updated', 'plugin loaded', 'prepare drop',
+                    'prepare outline drag', 'prepare outline drop',
+                    'prepare table drag', 'prepare table drop',
+                    'read from file', 'resigned active', 'resigned key',
+                    'resigned main', 'resized( sub views)?',
+                    'right mouse down', 'right mouse dragged',
+                    'right mouse up', 'rows changed', 'scroll wheel',
+                    'selected tab view item', 'selection changed',
+                    'selection changing', 'should begin editing',
+                    'should close', 'should collapse item',
+                    'should end editing', 'should expand item',
+                    'should open( untitled)?',
+                    'should quit( after last window closed)?',
+                    'should select column', 'should select item',
+                    'should select row', 'should select tab view item',
+                    'should selection change', 'should zoom', 'shown',
+                    'update menu item', 'update parameters',
+                    'update toolbar item', 'was hidden', 'was miniaturized',
+                    'will become active', 'will close', 'will dismiss',
+                    'will display browser cell', 'will display cell',
+                    'will display item cell', 'will display outline cell',
+                    'will finish launching', 'will hide', 'will miniaturize',
+                    'will move', 'will open', 'will pop up', 'will quit',
+                    'will resign active', 'will resize( sub views)?',
+                    'will select tab view item', 'will show', 'will zoom',
+                    'write to file', 'zoomed')
+    StudioCommands = ('animate', 'append', 'call method', 'center',
+                      'close drawer', 'close panel', 'display',
+                      'display alert', 'display dialog', 'display panel', 'go',
+                      'hide', 'highlight', 'increment', 'item for',
+                      'load image', 'load movie', 'load nib', 'load panel',
+                      'load sound', 'localized string', 'lock focus', 'log',
+                      'open drawer', 'path for', 'pause', 'perform action',
+                      'play', 'register', 'resume', 'scroll', 'select( all)?',
+                      'show', 'size to fit', 'start', 'step back',
+                      'step forward', 'stop', 'synchronize', 'unlock focus',
+                      'update')
+    StudioProperties = ('accepts arrow key', 'action method', 'active',
+                        'alignment', 'allowed identifiers',
+                        'allows branch selection', 'allows column reordering',
+                        'allows column resizing', 'allows column selection',
+                        'allows customization',
+                        'allows editing text attributes',
+                        'allows empty selection', 'allows mixed state',
+                        'allows multiple selection', 'allows reordering',
+                        'allows undo', 'alpha( value)?', 'alternate image',
+                        'alternate increment value', 'alternate title',
+                        'animation delay', 'associated file name',
+                        'associated object', 'auto completes', 'auto display',
+                        'auto enables items', 'auto repeat',
+                        'auto resizes( outline column)?',
+                        'auto save expanded items', 'auto save name',
+                        'auto save table columns', 'auto saves configuration',
+                        'auto scroll', 'auto sizes all columns to fit',
+                        'auto sizes cells', 'background color', 'bezel state',
+                        'bezel style', 'bezeled', 'border rect', 'border type',
+                        'bordered', 'bounds( rotation)?', 'box type',
+                        'button returned', 'button type',
+                        'can choose directories', 'can choose files',
+                        'can draw', 'can hide',
+                        'cell( (background color|size|type))?', 'characters',
+                        'class', 'click count', 'clicked( data)? column',
+                        'clicked data item', 'clicked( data)? row',
+                        'closeable', 'collating', 'color( (mode|panel))',
+                        'command key down', 'configuration',
+                        'content(s| (size|view( margins)?))?', 'context',
+                        'continuous', 'control key down', 'control size',
+                        'control tint', 'control view',
+                        'controller visible', 'coordinate system',
+                        'copies( on scroll)?', 'corner view', 'current cell',
+                        'current column', 'current( field)?  editor',
+                        'current( menu)? item', 'current row',
+                        'current tab view item', 'data source',
+                        'default identifiers', 'delta (x|y|z)',
+                        'destination window', 'directory', 'display mode',
+                        'displayed cell', 'document( (edited|rect|view))?',
+                        'double value', 'dragged column', 'dragged distance',
+                        'dragged items', 'draws( cell)? background',
+                        'draws grid', 'dynamically scrolls', 'echos bullets',
+                        'edge', 'editable', 'edited( data)? column',
+                        'edited data item', 'edited( data)? row', 'enabled',
+                        'enclosing scroll view', 'ending page',
+                        'error handling', 'event number', 'event type',
+                        'excluded from windows menu', 'executable path',
+                        'expanded', 'fax number', 'field editor', 'file kind',
+                        'file name', 'file type', 'first responder',
+                        'first visible column', 'flipped', 'floating',
+                        'font( panel)?', 'formatter', 'frameworks path',
+                        'frontmost', 'gave up', 'grid color', 'has data items',
+                        'has horizontal ruler', 'has horizontal scroller',
+                        'has parent data item', 'has resize indicator',
+                        'has shadow', 'has sub menu', 'has vertical ruler',
+                        'has vertical scroller', 'header cell', 'header view',
+                        'hidden', 'hides when deactivated', 'highlights by',
+                        'horizontal line scroll', 'horizontal page scroll',
+                        'horizontal ruler view', 'horizontally resizable',
+                        'icon image', 'id', 'identifier',
+                        'ignores multiple clicks',
+                        'image( (alignment|dims when disabled|frame style|scaling))?',
+                        'imports graphics', 'increment value',
+                        'indentation per level', 'indeterminate', 'index',
+                        'integer value', 'intercell spacing', 'item height',
+                        'key( (code|equivalent( modifier)?|window))?',
+                        'knob thickness', 'label', 'last( visible)? column',
+                        'leading offset', 'leaf', 'level', 'line scroll',
+                        'loaded', 'localized sort', 'location', 'loop mode',
+                        'main( (bunde|menu|window))?', 'marker follows cell',
+                        'matrix mode', 'maximum( content)? size',
+                        'maximum visible columns',
+                        'menu( form representation)?', 'miniaturizable',
+                        'miniaturized', 'minimized image', 'minimized title',
+                        'minimum column width', 'minimum( content)? size',
+                        'modal', 'modified', 'mouse down state',
+                        'movie( (controller|file|rect))?', 'muted', 'name',
+                        'needs display', 'next state', 'next text',
+                        'number of tick marks', 'only tick mark values',
+                        'opaque', 'open panel', 'option key down',
+                        'outline table column', 'page scroll', 'pages across',
+                        'pages down', 'palette label', 'pane splitter',
+                        'parent data item', 'parent window', 'pasteboard',
+                        'path( (names|separator))?', 'playing',
+                        'plays every frame', 'plays selection only', 'position',
+                        'preferred edge', 'preferred type', 'pressure',
+                        'previous text', 'prompt', 'properties',
+                        'prototype cell', 'pulls down', 'rate',
+                        'released when closed', 'repeated',
+                        'requested print time', 'required file type',
+                        'resizable', 'resized column', 'resource path',
+                        'returns records', 'reuses columns', 'rich text',
+                        'roll over', 'row height', 'rulers visible',
+                        'save panel', 'scripts path', 'scrollable',
+                        'selectable( identifiers)?', 'selected cell',
+                        'selected( data)? columns?', 'selected data items?',
+                        'selected( data)? rows?', 'selected item identifier',
+                        'selection by rect', 'send action on arrow key',
+                        'sends action when done editing', 'separates columns',
+                        'separator item', 'sequence number', 'services menu',
+                        'shared frameworks path', 'shared support path',
+                        'sheet', 'shift key down', 'shows alpha',
+                        'shows state by', 'size( mode)?',
+                        'smart insert delete enabled', 'sort case sensitivity',
+                        'sort column', 'sort order', 'sort type',
+                        'sorted( data rows)?', 'sound', 'source( mask)?',
+                        'spell checking enabled', 'starting page', 'state',
+                        'string value', 'sub menu', 'super menu', 'super view',
+                        'tab key traverses cells', 'tab state', 'tab type',
+                        'tab view', 'table view', 'tag', 'target( printer)?',
+                        'text color', 'text container insert',
+                        'text container origin', 'text returned',
+                        'tick mark position', 'time stamp',
+                        'title(d| (cell|font|height|position|rect))?',
+                        'tool tip', 'toolbar', 'trailing offset', 'transparent',
+                        'treat packages as directories', 'truncated labels',
+                        'types', 'unmodified characters', 'update views',
+                        'use sort indicator', 'user defaults',
+                        'uses data source', 'uses ruler',
+                        'uses threaded animation',
+                        'uses title from previous column', 'value wraps',
+                        'version',
+                        'vertical( (line scroll|page scroll|ruler view))?',
+                        'vertically resizable', 'view',
+                        'visible( document rect)?', 'volume', 'width', 'window',
+                        'windows menu', 'wraps', 'zoomable', 'zoomed')
+
+    tokens = {
+        'root': [
+            (r'\s+', Text),
+            (r'¬\n', String.Escape),
+            (r"'s\s+", Text),  # This is a possessive, consider moving
+            (r'(--|#).*?$', Comment),
+            (r'\(\*', Comment.Multiline, 'comment'),
+            (r'[(){}!,.:]', Punctuation),
+            (r'(«)([^»]+)(»)',
+             bygroups(Text, Name.Builtin, Text)),
+            (r'\b((?:considering|ignoring)\s*)'
+             r'(application responses|case|diacriticals|hyphens|'
+             r'numeric strings|punctuation|white space)',
+             bygroups(Keyword, Name.Builtin)),
+            (r'(-|\*|\+|&|≠|>=?|<=?|=|≥|≤|/|÷|\^)', Operator),
+            (r"\b({})\b".format('|'.join(Operators)), Operator.Word),
+            (r'^(\s*(?:on|end)\s+)'
+             r'({})'.format('|'.join(StudioEvents[::-1])),
+             bygroups(Keyword, Name.Function)),
+            (r'^(\s*)(in|on|script|to)(\s+)', bygroups(Text, Keyword, Text)),
+            (r'\b(as )({})\b'.format('|'.join(Classes)),
+             bygroups(Keyword, Name.Class)),
+            (r'\b({})\b'.format('|'.join(Literals)), Name.Constant),
+            (r'\b({})\b'.format('|'.join(Commands)), Name.Builtin),
+            (r'\b({})\b'.format('|'.join(Control)), Keyword),
+            (r'\b({})\b'.format('|'.join(Declarations)), Keyword),
+            (r'\b({})\b'.format('|'.join(Reserved)), Name.Builtin),
+            (r'\b({})s?\b'.format('|'.join(BuiltIn)), Name.Builtin),
+            (r'\b({})\b'.format('|'.join(HandlerParams)), Name.Builtin),
+            (r'\b({})\b'.format('|'.join(StudioProperties)), Name.Attribute),
+            (r'\b({})s?\b'.format('|'.join(StudioClasses)), Name.Builtin),
+            (r'\b({})\b'.format('|'.join(StudioCommands)), Name.Builtin),
+            (r'\b({})\b'.format('|'.join(References)), Name.Builtin),
+            (r'"(\\\\|\\[^\\]|[^"\\])*"', String.Double),
+            (rf'\b({Identifiers})\b', Name.Variable),
+            (r'[-+]?(\d+\.\d*|\d*\.\d+)(E[-+][0-9]+)?', Number.Float),
+            (r'[-+]?\d+', Number.Integer),
+        ],
+        'comment': [
+            (r'\(\*', Comment.Multiline, '#push'),
+            (r'\*\)', Comment.Multiline, '#pop'),
+            ('[^*(]+', Comment.Multiline),
+            ('[*(]', Comment.Multiline),
+        ],
+    }
+
+
+class RexxLexer(RegexLexer):
+    """
+    Rexx is a scripting language available for
+    a wide range of different platforms with its roots found on mainframe
+    systems. It is popular for I/O- and data based tasks and can act as glue
+    language to bind different applications together.
+    """
+    name = 'Rexx'
+    url = 'http://www.rexxinfo.org/'
+    aliases = ['rexx', 'arexx']
+    filenames = ['*.rexx', '*.rex', '*.rx', '*.arexx']
+    mimetypes = ['text/x-rexx']
+    version_added = '2.0'
+    flags = re.IGNORECASE
+
+    tokens = {
+        'root': [
+            (r'\s+', Whitespace),
+            (r'/\*', Comment.Multiline, 'comment'),
+            (r'"', String, 'string_double'),
+            (r"'", String, 'string_single'),
+            (r'[0-9]+(\.[0-9]+)?(e[+-]?[0-9])?', Number),
+            (r'([a-z_]\w*)(\s*)(:)(\s*)(procedure)\b',
+             bygroups(Name.Function, Whitespace, Operator, Whitespace,
+                      Keyword.Declaration)),
+            (r'([a-z_]\w*)(\s*)(:)',
+             bygroups(Name.Label, Whitespace, Operator)),
+            include('function'),
+            include('keyword'),
+            include('operator'),
+            (r'[a-z_]\w*', Text),
+        ],
+        'function': [
+            (words((
+                'abbrev', 'abs', 'address', 'arg', 'b2x', 'bitand', 'bitor', 'bitxor',
+                'c2d', 'c2x', 'center', 'charin', 'charout', 'chars', 'compare',
+                'condition', 'copies', 'd2c', 'd2x', 'datatype', 'date', 'delstr',
+                'delword', 'digits', 'errortext', 'form', 'format', 'fuzz', 'insert',
+                'lastpos', 'left', 'length', 'linein', 'lineout', 'lines', 'max',
+                'min', 'overlay', 'pos', 'queued', 'random', 'reverse', 'right', 'sign',
+                'sourceline', 'space', 'stream', 'strip', 'substr', 'subword', 'symbol',
+                'time', 'trace', 'translate', 'trunc', 'value', 'verify', 'word',
+                'wordindex', 'wordlength', 'wordpos', 'words', 'x2b', 'x2c', 'x2d',
+                'xrange'), suffix=r'(\s*)(\()'),
+             bygroups(Name.Builtin, Whitespace, Operator)),
+        ],
+        'keyword': [
+            (r'(address|arg|by|call|do|drop|else|end|exit|for|forever|if|'
+             r'interpret|iterate|leave|nop|numeric|off|on|options|parse|'
+             r'pull|push|queue|return|say|select|signal|to|then|trace|until|'
+             r'while)\b', Keyword.Reserved),
+        ],
+        'operator': [
+            (r'(-|//|/|\(|\)|\*\*|\*|\\<<|\\<|\\==|\\=|\\>>|\\>|\\|\|\||\||'
+             r'&&|&|%|\+|<<=|<<|<=|<>|<|==|=|><|>=|>>=|>>|>|¬<<|¬<|¬==|¬=|'
+             r'¬>>|¬>|¬|\.|,)', Operator),
+        ],
+        'string_double': [
+            (r'[^"\n]+', String),
+            (r'""', String),
+            (r'"', String, '#pop'),
+            (r'\n', Text, '#pop'),  # Stray linefeed also terminates strings.
+        ],
+        'string_single': [
+            (r'[^\'\n]+', String),
+            (r'\'\'', String),
+            (r'\'', String, '#pop'),
+            (r'\n', Text, '#pop'),  # Stray linefeed also terminates strings.
+        ],
+        'comment': [
+            (r'[^*]+', Comment.Multiline),
+            (r'\*/', Comment.Multiline, '#pop'),
+            (r'\*', Comment.Multiline),
+        ]
+    }
+
+    def _c(s):
+        return re.compile(s, re.MULTILINE)
+    _ADDRESS_COMMAND_PATTERN = _c(r'^\s*address\s+command\b')
+    _ADDRESS_PATTERN = _c(r'^\s*address\s+')
+    _DO_WHILE_PATTERN = _c(r'^\s*do\s+while\b')
+    _IF_THEN_DO_PATTERN = _c(r'^\s*if\b.+\bthen\s+do\s*$')
+    _PROCEDURE_PATTERN = _c(r'^\s*([a-z_]\w*)(\s*)(:)(\s*)(procedure)\b')
+    _ELSE_DO_PATTERN = _c(r'\belse\s+do\s*$')
+    _PARSE_ARG_PATTERN = _c(r'^\s*parse\s+(upper\s+)?(arg|value)\b')
+    PATTERNS_AND_WEIGHTS = (
+        (_ADDRESS_COMMAND_PATTERN, 0.2),
+        (_ADDRESS_PATTERN, 0.05),
+        (_DO_WHILE_PATTERN, 0.1),
+        (_ELSE_DO_PATTERN, 0.1),
+        (_IF_THEN_DO_PATTERN, 0.1),
+        (_PROCEDURE_PATTERN, 0.5),
+        (_PARSE_ARG_PATTERN, 0.2),
+    )
+
+    def analyse_text(text):
+        """
+        Check for initial comment and patterns that distinguish Rexx from other
+        C-like languages.
+        """
+        if re.search(r'/\*\**\s*rexx', text, re.IGNORECASE):
+            # Header matches MVS Rexx requirements, this is certainly a Rexx
+            # script.
+            return 1.0
+        elif text.startswith('/*'):
+            # Header matches general Rexx requirements; the source code might
+            # still be any language using C comments such as C++, C# or Java.
+            lowerText = text.lower()
+            result = sum(weight
+                         for (pattern, weight) in RexxLexer.PATTERNS_AND_WEIGHTS
+                         if pattern.search(lowerText)) + 0.01
+            return min(result, 1.0)
+
+
+class MOOCodeLexer(RegexLexer):
+    """
+    For MOOCode (the MOO scripting language).
+    """
+    name = 'MOOCode'
+    url = 'http://www.moo.mud.org/'
+    filenames = ['*.moo']
+    aliases = ['moocode', 'moo']
+    mimetypes = ['text/x-moocode']
+    version_added = '0.9'
+
+    tokens = {
+        'root': [
+            # Numbers
+            (r'(0|[1-9][0-9_]*)', Number.Integer),
+            # Strings
+            (r'"(\\\\|\\[^\\]|[^"\\])*"', String),
+            # exceptions
+            (r'(E_PERM|E_DIV)', Name.Exception),
+            # db-refs
+            (r'((#[-0-9]+)|(\$\w+))', Name.Entity),
+            # Keywords
+            (r'\b(if|else|elseif|endif|for|endfor|fork|endfork|while'
+             r'|endwhile|break|continue|return|try'
+             r'|except|endtry|finally|in)\b', Keyword),
+            # builtins
+            (r'(random|length)', Name.Builtin),
+            # special variables
+            (r'(player|caller|this|args)', Name.Variable.Instance),
+            # skip whitespace
+            (r'\s+', Text),
+            (r'\n', Text),
+            # other operators
+            (r'([!;=,{}&|:.\[\]@()<>?]+)', Operator),
+            # function call
+            (r'(\w+)(\()', bygroups(Name.Function, Operator)),
+            # variables
+            (r'(\w+)', Text),
+        ]
+    }
+
+
+class HybrisLexer(RegexLexer):
+    """
+    For Hybris source code.
+    """
+
+    name = 'Hybris'
+    aliases = ['hybris']
+    filenames = ['*.hyb']
+    mimetypes = ['text/x-hybris', 'application/x-hybris']
+    url = 'https://github.com/evilsocket/hybris'
+    version_added = '1.4'
+
+    flags = re.MULTILINE | re.DOTALL
+
+    tokens = {
+        'root': [
+            # method names
+            (r'^(\s*(?:function|method|operator\s+)+?)'
+             r'([a-zA-Z_]\w*)'
+             r'(\s*)(\()', bygroups(Keyword, Name.Function, Text, Operator)),
+            (r'[^\S\n]+', Text),
+            (r'//.*?\n', Comment.Single),
+            (r'/\*.*?\*/', Comment.Multiline),
+            (r'@[a-zA-Z_][\w.]*', Name.Decorator),
+            (r'(break|case|catch|next|default|do|else|finally|for|foreach|of|'
+             r'unless|if|new|return|switch|me|throw|try|while)\b', Keyword),
+            (r'(extends|private|protected|public|static|throws|function|method|'
+             r'operator)\b', Keyword.Declaration),
+            (r'(true|false|null|__FILE__|__LINE__|__VERSION__|__LIB_PATH__|'
+             r'__INC_PATH__)\b', Keyword.Constant),
+            (r'(class|struct)(\s+)',
+             bygroups(Keyword.Declaration, Text), 'class'),
+            (r'(import|include)(\s+)',
+             bygroups(Keyword.Namespace, Text), 'import'),
+            (words((
+                'gc_collect', 'gc_mm_items', 'gc_mm_usage', 'gc_collect_threshold',
+                'urlencode', 'urldecode', 'base64encode', 'base64decode', 'sha1', 'crc32',
+                'sha2', 'md5', 'md5_file', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos',
+                'cosh', 'exp', 'fabs', 'floor', 'fmod', 'log', 'log10', 'pow', 'sin',
+                'sinh', 'sqrt', 'tan', 'tanh', 'isint', 'isfloat', 'ischar', 'isstring',
+                'isarray', 'ismap', 'isalias', 'typeof', 'sizeof', 'toint', 'tostring',
+                'fromxml', 'toxml', 'binary', 'pack', 'load', 'eval', 'var_names',
+                'var_values', 'user_functions', 'dyn_functions', 'methods', 'call',
+                'call_method', 'mknod', 'mkfifo', 'mount', 'umount2', 'umount', 'ticks',
+                'usleep', 'sleep', 'time', 'strtime', 'strdate', 'dllopen', 'dlllink',
+                'dllcall', 'dllcall_argv', 'dllclose', 'env', 'exec', 'fork', 'getpid',
+                'wait', 'popen', 'pclose', 'exit', 'kill', 'pthread_create',
+                'pthread_create_argv', 'pthread_exit', 'pthread_join', 'pthread_kill',
+                'smtp_send', 'http_get', 'http_post', 'http_download', 'socket', 'bind',
+                'listen', 'accept', 'getsockname', 'getpeername', 'settimeout', 'connect',
+                'server', 'recv', 'send', 'close', 'print', 'println', 'printf', 'input',
+                'readline', 'serial_open', 'serial_fcntl', 'serial_get_attr',
+                'serial_get_ispeed', 'serial_get_ospeed', 'serial_set_attr',
+                'serial_set_ispeed', 'serial_set_ospeed', 'serial_write', 'serial_read',
+                'serial_close', 'xml_load', 'xml_parse', 'fopen', 'fseek', 'ftell',
+                'fsize', 'fread', 'fwrite', 'fgets', 'fclose', 'file', 'readdir',
+                'pcre_replace', 'size', 'pop', 'unmap', 'has', 'keys', 'values',
+                'length', 'find', 'substr', 'replace', 'split', 'trim', 'remove',
+                'contains', 'join'), suffix=r'\b'),
+             Name.Builtin),
+            (words((
+                'MethodReference', 'Runner', 'Dll', 'Thread', 'Pipe', 'Process',
+                'Runnable', 'CGI', 'ClientSocket', 'Socket', 'ServerSocket',
+                'File', 'Console', 'Directory', 'Exception'), suffix=r'\b'),
+             Keyword.Type),
+            (r'"(\\\\|\\[^\\]|[^"\\])*"', String),
+            (r"'\\.'|'[^\\]'|'\\u[0-9a-f]{4}'", String.Char),
+            (r'(\.)([a-zA-Z_]\w*)',
+             bygroups(Operator, Name.Attribute)),
+            (r'[a-zA-Z_]\w*:', Name.Label),
+            (r'[a-zA-Z_$]\w*', Name),
+            (r'[~^*!%&\[\](){}<>|+=:;,./?\-@]+', Operator),
+            (r'[0-9][0-9]*\.[0-9]+([eE][0-9]+)?[fd]?', Number.Float),
+            (r'0x[0-9a-f]+', Number.Hex),
+            (r'[0-9]+L?', Number.Integer),
+            (r'\n', Text),
+        ],
+        'class': [
+            (r'[a-zA-Z_]\w*', Name.Class, '#pop')
+        ],
+        'import': [
+            (r'[\w.]+\*?', Name.Namespace, '#pop')
+        ],
+    }
+
+    def analyse_text(text):
+        """public method and private method don't seem to be quite common
+        elsewhere."""
+        result = 0
+        if re.search(r'\b(?:public|private)\s+method\b', text):
+            result += 0.01
+        return result
+
+
+
+class EasytrieveLexer(RegexLexer):
+    """
+    Easytrieve Plus is a programming language for extracting, filtering and
+    converting sequential data. Furthermore it can layout data for reports.
+    It is mainly used on mainframe platforms and can access several of the
+    mainframe's native file formats. It is somewhat comparable to awk.
+    """
+    name = 'Easytrieve'
+    aliases = ['easytrieve']
+    filenames = ['*.ezt', '*.mac']
+    mimetypes = ['text/x-easytrieve']
+    url = 'https://www.broadcom.com/products/mainframe/application-development/easytrieve-report-generator'
+    version_added = '2.1'
+    flags = 0
+
+    # Note: We cannot use r'\b' at the start and end of keywords because
+    # Easytrieve Plus delimiter characters are:
+    #
+    #   * space ( )
+    #   * apostrophe (')
+    #   * period (.)
+    #   * comma (,)
+    #   * parenthesis ( and )
+    #   * colon (:)
+    #
+    # Additionally words end once a '*' appears, indicatins a comment.
+    _DELIMITERS = r' \'.,():\n'
+    _DELIMITERS_OR_COMENT = _DELIMITERS + '*'
+    _DELIMITER_PATTERN = '[' + _DELIMITERS + ']'
+    _DELIMITER_PATTERN_CAPTURE = '(' + _DELIMITER_PATTERN + ')'
+    _NON_DELIMITER_OR_COMMENT_PATTERN = '[^' + _DELIMITERS_OR_COMENT + ']'
+    _OPERATORS_PATTERN = '[.+\\-/=\\[\\](){}<>;,&%¬]'
+    _KEYWORDS = [
+        'AFTER-BREAK', 'AFTER-LINE', 'AFTER-SCREEN', 'AIM', 'AND', 'ATTR',
+        'BEFORE', 'BEFORE-BREAK', 'BEFORE-LINE', 'BEFORE-SCREEN', 'BUSHU',
+        'BY', 'CALL', 'CASE', 'CHECKPOINT', 'CHKP', 'CHKP-STATUS', 'CLEAR',
+        'CLOSE', 'COL', 'COLOR', 'COMMIT', 'CONTROL', 'COPY', 'CURSOR', 'D',
+        'DECLARE', 'DEFAULT', 'DEFINE', 'DELETE', 'DENWA', 'DISPLAY', 'DLI',
+        'DO', 'DUPLICATE', 'E', 'ELSE', 'ELSE-IF', 'END', 'END-CASE',
+        'END-DO', 'END-IF', 'END-PROC', 'ENDPAGE', 'ENDTABLE', 'ENTER', 'EOF',
+        'EQ', 'ERROR', 'EXIT', 'EXTERNAL', 'EZLIB', 'F1', 'F10', 'F11', 'F12',
+        'F13', 'F14', 'F15', 'F16', 'F17', 'F18', 'F19', 'F2', 'F20', 'F21',
+        'F22', 'F23', 'F24', 'F25', 'F26', 'F27', 'F28', 'F29', 'F3', 'F30',
+        'F31', 'F32', 'F33', 'F34', 'F35', 'F36', 'F4', 'F5', 'F6', 'F7',
+        'F8', 'F9', 'FETCH', 'FILE-STATUS', 'FILL', 'FINAL', 'FIRST',
+        'FIRST-DUP', 'FOR', 'GE', 'GET', 'GO', 'GOTO', 'GQ', 'GR', 'GT',
+        'HEADING', 'HEX', 'HIGH-VALUES', 'IDD', 'IDMS', 'IF', 'IN', 'INSERT',
+        'JUSTIFY', 'KANJI-DATE', 'KANJI-DATE-LONG', 'KANJI-TIME', 'KEY',
+        'KEY-PRESSED', 'KOKUGO', 'KUN', 'LAST-DUP', 'LE', 'LEVEL', 'LIKE',
+        'LINE', 'LINE-COUNT', 'LINE-NUMBER', 'LINK', 'LIST', 'LOW-VALUES',
+        'LQ', 'LS', 'LT', 'MACRO', 'MASK', 'MATCHED', 'MEND', 'MESSAGE',
+        'MOVE', 'MSTART', 'NE', 'NEWPAGE', 'NOMASK', 'NOPRINT', 'NOT',
+        'NOTE', 'NOVERIFY', 'NQ', 'NULL', 'OF', 'OR', 'OTHERWISE', 'PA1',
+        'PA2', 'PA3', 'PAGE-COUNT', 'PAGE-NUMBER', 'PARM-REGISTER',
+        'PATH-ID', 'PATTERN', 'PERFORM', 'POINT', 'POS', 'PRIMARY', 'PRINT',
+        'PROCEDURE', 'PROGRAM', 'PUT', 'READ', 'RECORD', 'RECORD-COUNT',
+        'RECORD-LENGTH', 'REFRESH', 'RELEASE', 'RENUM', 'REPEAT', 'REPORT',
+        'REPORT-INPUT', 'RESHOW', 'RESTART', 'RETRIEVE', 'RETURN-CODE',
+        'ROLLBACK', 'ROW', 'S', 'SCREEN', 'SEARCH', 'SECONDARY', 'SELECT',
+        'SEQUENCE', 'SIZE', 'SKIP', 'SOKAKU', 'SORT', 'SQL', 'STOP', 'SUM',
+        'SYSDATE', 'SYSDATE-LONG', 'SYSIN', 'SYSIPT', 'SYSLST', 'SYSPRINT',
+        'SYSSNAP', 'SYSTIME', 'TALLY', 'TERM-COLUMNS', 'TERM-NAME',
+        'TERM-ROWS', 'TERMINATION', 'TITLE', 'TO', 'TRANSFER', 'TRC',
+        'UNIQUE', 'UNTIL', 'UPDATE', 'UPPERCASE', 'USER', 'USERID', 'VALUE',
+        'VERIFY', 'W', 'WHEN', 'WHILE', 'WORK', 'WRITE', 'X', 'XDM', 'XRST'
+    ]
+
+    tokens = {
+        'root': [
+            (r'\*.*\n', Comment.Single),
+            (r'\n+', Whitespace),
+            # Macro argument
+            (r'&' + _NON_DELIMITER_OR_COMMENT_PATTERN + r'+\.', Name.Variable,
+             'after_macro_argument'),
+            # Macro call
+            (r'%' + _NON_DELIMITER_OR_COMMENT_PATTERN + r'+', Name.Variable),
+            (r'(FILE|MACRO|REPORT)(\s+)',
+             bygroups(Keyword.Declaration, Whitespace), 'after_declaration'),
+            (r'(JOB|PARM)' + r'(' + _DELIMITER_PATTERN + r')',
+             bygroups(Keyword.Declaration, Operator)),
+            (words(_KEYWORDS, suffix=_DELIMITER_PATTERN_CAPTURE),
+             bygroups(Keyword.Reserved, Operator)),
+            (_OPERATORS_PATTERN, Operator),
+            # Procedure declaration
+            (r'(' + _NON_DELIMITER_OR_COMMENT_PATTERN + r'+)(\s*)(\.?)(\s*)(PROC)(\s*\n)',
+             bygroups(Name.Function, Whitespace, Operator, Whitespace,
+                      Keyword.Declaration, Whitespace)),
+            (r'[0-9]+\.[0-9]*', Number.Float),
+            (r'[0-9]+', Number.Integer),
+            (r"'(''|[^'])*'", String),
+            (r'\s+', Whitespace),
+            # Everything else just belongs to a name
+            (_NON_DELIMITER_OR_COMMENT_PATTERN + r'+', Name),
+         ],
+        'after_declaration': [
+            (_NON_DELIMITER_OR_COMMENT_PATTERN + r'+', Name.Function),
+            default('#pop'),
+        ],
+        'after_macro_argument': [
+            (r'\*.*\n', Comment.Single, '#pop'),
+            (r'\s+', Whitespace, '#pop'),
+            (_OPERATORS_PATTERN, Operator, '#pop'),
+            (r"'(''|[^'])*'", String, '#pop'),
+            # Everything else just belongs to a name
+            (_NON_DELIMITER_OR_COMMENT_PATTERN + r'+', Name),
+        ],
+    }
+    _COMMENT_LINE_REGEX = re.compile(r'^\s*\*')
+    _MACRO_HEADER_REGEX = re.compile(r'^\s*MACRO')
+
+    def analyse_text(text):
+        """
+        Perform a structural analysis for basic Easytrieve constructs.
+        """
+        result = 0.0
+        lines = text.split('\n')
+        hasEndProc = False
+        hasHeaderComment = False
+        hasFile = False
+        hasJob = False
+        hasProc = False
+        hasParm = False
+        hasReport = False
+
+        def isCommentLine(line):
+            return EasytrieveLexer._COMMENT_LINE_REGEX.match(lines[0]) is not None
+
+        def isEmptyLine(line):
+            return not bool(line.strip())
+
+        # Remove possible empty lines and header comments.
+        while lines and (isEmptyLine(lines[0]) or isCommentLine(lines[0])):
+            if not isEmptyLine(lines[0]):
+                hasHeaderComment = True
+            del lines[0]
+
+        if EasytrieveLexer._MACRO_HEADER_REGEX.match(lines[0]):
+            # Looks like an Easytrieve macro.
+            result = 0.4
+            if hasHeaderComment:
+                result += 0.4
+        else:
+            # Scan the source for lines starting with indicators.
+            for line in lines:
+                words = line.split()
+                if (len(words) >= 2):
+                    firstWord = words[0]
+                    if not hasReport:
+                        if not hasJob:
+                            if not hasFile:
+                                if not hasParm:
+                                    if firstWord == 'PARM':
+                                        hasParm = True
+                                if firstWord == 'FILE':
+                                    hasFile = True
+                            if firstWord == 'JOB':
+                                hasJob = True
+                        elif firstWord == 'PROC':
+                            hasProc = True
+                        elif firstWord == 'END-PROC':
+                            hasEndProc = True
+                        elif firstWord == 'REPORT':
+                            hasReport = True
+
+            # Weight the findings.
+            if hasJob and (hasProc == hasEndProc):
+                if hasHeaderComment:
+                    result += 0.1
+                if hasParm:
+                    if hasProc:
+                        # Found PARM, JOB and PROC/END-PROC:
+                        # pretty sure this is Easytrieve.
+                        result += 0.8
+                    else:
+                        # Found PARAM and  JOB: probably this is Easytrieve
+                        result += 0.5
+                else:
+                    # Found JOB and possibly other keywords: might be Easytrieve
+                    result += 0.11
+                    if hasParm:
+                        # Note: PARAM is not a proper English word, so this is
+                        # regarded a much better indicator for Easytrieve than
+                        # the other words.
+                        result += 0.2
+                    if hasFile:
+                        result += 0.01
+                    if hasReport:
+                        result += 0.01
+        assert 0.0 <= result <= 1.0
+        return result
+
+
+class JclLexer(RegexLexer):
+    """
+    Job Control Language (JCL)
+    is a scripting language used on mainframe platforms to instruct the system
+    on how to run a batch job or start a subsystem. It is somewhat
+    comparable to MS DOS batch and Unix shell scripts.
+    """
+    name = 'JCL'
+    aliases = ['jcl']
+    filenames = ['*.jcl']
+    mimetypes = ['text/x-jcl']
+    url = 'https://en.wikipedia.org/wiki/Job_Control_Language'
+    version_added = '2.1'
+
+    flags = re.IGNORECASE
+
+    tokens = {
+        'root': [
+            (r'//\*.*\n', Comment.Single),
+            (r'//', Keyword.Pseudo, 'statement'),
+            (r'/\*', Keyword.Pseudo, 'jes2_statement'),
+            # TODO: JES3 statement
+            (r'.*\n', Other)  # Input text or inline code in any language.
+        ],
+        'statement': [
+            (r'\s*\n', Whitespace, '#pop'),
+            (r'([a-z]\w*)(\s+)(exec|job)(\s*)',
+             bygroups(Name.Label, Whitespace, Keyword.Reserved, Whitespace),
+             'option'),
+            (r'[a-z]\w*', Name.Variable, 'statement_command'),
+            (r'\s+', Whitespace, 'statement_command'),
+        ],
+        'statement_command': [
+            (r'\s+(command|cntl|dd|endctl|endif|else|include|jcllib|'
+             r'output|pend|proc|set|then|xmit)\s+', Keyword.Reserved, 'option'),
+            include('option')
+        ],
+        'jes2_statement': [
+            (r'\s*\n', Whitespace, '#pop'),
+            (r'\$', Keyword, 'option'),
+            (r'\b(jobparam|message|netacct|notify|output|priority|route|'
+             r'setup|signoff|xeq|xmit)\b', Keyword, 'option'),
+        ],
+        'option': [
+            # (r'\n', Text, 'root'),
+            (r'\*', Name.Builtin),
+            (r'[\[\](){}<>;,]', Punctuation),
+            (r'[-+*/=&%]', Operator),
+            (r'[a-z_]\w*', Name),
+            (r'\d+\.\d*', Number.Float),
+            (r'\.\d+', Number.Float),
+            (r'\d+', Number.Integer),
+            (r"'", String, 'option_string'),
+            (r'[ \t]+', Whitespace, 'option_comment'),
+            (r'\.', Punctuation),
+        ],
+        'option_string': [
+            (r"(\n)(//)", bygroups(Text, Keyword.Pseudo)),
+            (r"''", String),
+            (r"[^']", String),
+            (r"'", String, '#pop'),
+        ],
+        'option_comment': [
+            # (r'\n', Text, 'root'),
+            (r'.+', Comment.Single),
+        ]
+    }
+
+    _JOB_HEADER_PATTERN = re.compile(r'^//[a-z#$@][a-z0-9#$@]{0,7}\s+job(\s+.*)?$',
+                                     re.IGNORECASE)
+
+    def analyse_text(text):
+        """
+        Recognize JCL job by header.
+        """
+        result = 0.0
+        lines = text.split('\n')
+        if len(lines) > 0:
+            if JclLexer._JOB_HEADER_PATTERN.match(lines[0]):
+                result = 1.0
+        assert 0.0 <= result <= 1.0
+        return result
+
+
+class MiniScriptLexer(RegexLexer):
+    """
+    For MiniScript source code.
+    """
+
+    name = 'MiniScript'
+    url = 'https://miniscript.org'
+    aliases = ['miniscript', 'ms']
+    filenames = ['*.ms']
+    mimetypes = ['text/x-minicript', 'application/x-miniscript']
+    version_added = '2.6'
+
+    tokens = {
+        'root': [
+            (r'#!(.*?)$', Comment.Preproc),
+            default('base'),
+        ],
+        'base': [
+            ('//.*$', Comment.Single),
+            (r'(?i)(\d*\.\d+|\d+\.\d*)(e[+-]?\d+)?', Number),
+            (r'(?i)\d+e[+-]?\d+', Number),
+            (r'\d+', Number),
+            (r'\n', Text),
+            (r'[^\S\n]+', Text),
+            (r'"', String, 'string_double'),
+            (r'(==|!=|<=|>=|[=+\-*/%^<>.:])', Operator),
+            (r'[;,\[\]{}()]', Punctuation),
+            (words((
+                'break', 'continue', 'else', 'end', 'for', 'function', 'if',
+                'in', 'isa', 'then', 'repeat', 'return', 'while'), suffix=r'\b'),
+             Keyword),
+            (words((
+                'abs', 'acos', 'asin', 'atan', 'ceil', 'char', 'cos', 'floor',
+                'log', 'round', 'rnd', 'pi', 'sign', 'sin', 'sqrt', 'str', 'tan',
+                'hasIndex', 'indexOf', 'len', 'val', 'code', 'remove', 'lower',
+                'upper', 'replace', 'split', 'indexes', 'values', 'join', 'sum',
+                'sort', 'shuffle', 'push', 'pop', 'pull', 'range',
+                'print', 'input', 'time', 'wait', 'locals', 'globals', 'outer',
+                'yield'), suffix=r'\b'),
+             Name.Builtin),
+            (r'(true|false|null)\b', Keyword.Constant),
+            (r'(and|or|not|new)\b', Operator.Word),
+            (r'(self|super|__isa)\b', Name.Builtin.Pseudo),
+            (r'[a-zA-Z_]\w*', Name.Variable)
+        ],
+        'string_double': [
+            (r'[^"\n]+', String),
+            (r'""', String),
+            (r'"', String, '#pop'),
+            (r'\n', Text, '#pop'),  # Stray linefeed also terminates strings.
+        ]
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/sgf.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/sgf.py
new file mode 100644
index 00000000..f0e56cba
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/sgf.py
@@ -0,0 +1,59 @@
+"""
+    pygments.lexers.sgf
+    ~~~~~~~~~~~~~~~~~~~
+
+    Lexer for Smart Game Format (sgf) file format.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, bygroups
+from pygments.token import Name, Literal, String, Punctuation, Whitespace
+
+__all__ = ["SmartGameFormatLexer"]
+
+
+class SmartGameFormatLexer(RegexLexer):
+    """
+    Lexer for Smart Game Format (sgf) file format.
+
+    The format is used to store game records of board games for two players
+    (mainly Go game).
+    """
+    name = 'SmartGameFormat'
+    url = 'https://www.red-bean.com/sgf/'
+    aliases = ['sgf']
+    filenames = ['*.sgf']
+    version_added = '2.4'
+
+    tokens = {
+        'root': [
+            (r'[():;]+', Punctuation),
+            # tokens:
+            (r'(A[BW]|AE|AN|AP|AR|AS|[BW]L|BM|[BW]R|[BW]S|[BW]T|CA|CH|CP|CR|'
+             r'DD|DM|DO|DT|EL|EV|EX|FF|FG|G[BW]|GC|GM|GN|HA|HO|ID|IP|IT|IY|KM|'
+             r'KO|LB|LN|LT|L|MA|MN|M|N|OB|OM|ON|OP|OT|OV|P[BW]|PC|PL|PM|RE|RG|'
+             r'RO|RU|SO|SC|SE|SI|SL|SO|SQ|ST|SU|SZ|T[BW]|TC|TE|TM|TR|UC|US|VW|'
+             r'V|[BW]|C)',
+             Name.Builtin),
+            # number:
+            (r'(\[)([0-9.]+)(\])',
+             bygroups(Punctuation, Literal.Number, Punctuation)),
+            # date:
+            (r'(\[)([0-9]{4}-[0-9]{2}-[0-9]{2})(\])',
+             bygroups(Punctuation, Literal.Date, Punctuation)),
+            # point:
+            (r'(\[)([a-z]{2})(\])',
+             bygroups(Punctuation, String, Punctuation)),
+            # double points:
+            (r'(\[)([a-z]{2})(:)([a-z]{2})(\])',
+             bygroups(Punctuation, String, Punctuation, String, Punctuation)),
+
+            (r'(\[)([\w\s#()+,\-.:?]+)(\])',
+             bygroups(Punctuation, String, Punctuation)),
+            (r'(\[)(\s.*)(\])',
+             bygroups(Punctuation, Whitespace, Punctuation)),
+            (r'\s+', Whitespace)
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/shell.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/shell.py
new file mode 100644
index 00000000..744767a1
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/shell.py
@@ -0,0 +1,902 @@
+"""
+    pygments.lexers.shell
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for various shells.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pygments.lexer import Lexer, RegexLexer, do_insertions, bygroups, \
+    include, default, this, using, words, line_re
+from pygments.token import Punctuation, Whitespace, \
+    Text, Comment, Operator, Keyword, Name, String, Number, Generic
+from pygments.util import shebang_matches
+
+__all__ = ['BashLexer', 'BashSessionLexer', 'TcshLexer', 'BatchLexer',
+           'SlurmBashLexer', 'MSDOSSessionLexer', 'PowerShellLexer',
+           'PowerShellSessionLexer', 'TcshSessionLexer', 'FishShellLexer',
+           'ExeclineLexer']
+
+
+class BashLexer(RegexLexer):
+    """
+    Lexer for (ba|k|z|)sh shell scripts.
+    """
+
+    name = 'Bash'
+    aliases = ['bash', 'sh', 'ksh', 'zsh', 'shell', 'openrc']
+    filenames = ['*.sh', '*.ksh', '*.bash', '*.ebuild', '*.eclass',
+                 '*.exheres-0', '*.exlib', '*.zsh',
+                 '.bashrc', 'bashrc', '.bash_*', 'bash_*', 'zshrc', '.zshrc',
+                 '.kshrc', 'kshrc',
+                 'PKGBUILD']
+    mimetypes = ['application/x-sh', 'application/x-shellscript', 'text/x-shellscript']
+    url = 'https://en.wikipedia.org/wiki/Unix_shell'
+    version_added = '0.6'
+
+    tokens = {
+        'root': [
+            include('basic'),
+            (r'`', String.Backtick, 'backticks'),
+            include('data'),
+            include('interp'),
+        ],
+        'interp': [
+            (r'\$\(\(', Keyword, 'math'),
+            (r'\$\(', Keyword, 'paren'),
+            (r'\$\{#?', String.Interpol, 'curly'),
+            (r'\$[a-zA-Z_]\w*', Name.Variable),  # user variable
+            (r'\$(?:\d+|[#$?!_*@-])', Name.Variable),      # builtin
+            (r'\$', Text),
+        ],
+        'basic': [
+            (r'\b(if|fi|else|while|in|do|done|for|then|return|function|case|'
+             r'select|break|continue|until|esac|elif)(\s*)\b',
+             bygroups(Keyword, Whitespace)),
+            (r'\b(alias|bg|bind|builtin|caller|cd|command|compgen|'
+             r'complete|declare|dirs|disown|echo|enable|eval|exec|exit|'
+             r'export|false|fc|fg|getopts|hash|help|history|jobs|kill|let|'
+             r'local|logout|popd|printf|pushd|pwd|read|readonly|set|shift|'
+             r'shopt|source|suspend|test|time|times|trap|true|type|typeset|'
+             r'ulimit|umask|unalias|unset|wait)(?=[\s)`])',
+             Name.Builtin),
+            (r'\A#!.+\n', Comment.Hashbang),
+            (r'#.*\n', Comment.Single),
+            (r'\\[\w\W]', String.Escape),
+            (r'(\b\w+)(\s*)(\+?=)', bygroups(Name.Variable, Whitespace, Operator)),
+            (r'[\[\]{}()=]', Operator),
+            (r'<<<', Operator),  # here-string
+            (r'<<-?\s*(\'?)\\?(\w+)[\w\W]+?\2', String),
+            (r'&&|\|\|', Operator),
+        ],
+        'data': [
+            (r'(?s)\$?"(\\.|[^"\\$])*"', String.Double),
+            (r'"', String.Double, 'string'),
+            (r"(?s)\$'(\\\\|\\[0-7]+|\\.|[^'\\])*'", String.Single),
+            (r"(?s)'.*?'", String.Single),
+            (r';', Punctuation),
+            (r'&', Punctuation),
+            (r'\|', Punctuation),
+            (r'\s+', Whitespace),
+            (r'\d+\b', Number),
+            (r'[^=\s\[\]{}()$"\'`\\<&|;]+', Text),
+            (r'<', Text),
+        ],
+        'string': [
+            (r'"', String.Double, '#pop'),
+            (r'(?s)(\\\\|\\[0-7]+|\\.|[^"\\$])+', String.Double),
+            include('interp'),
+        ],
+        'curly': [
+            (r'\}', String.Interpol, '#pop'),
+            (r':-', Keyword),
+            (r'\w+', Name.Variable),
+            (r'[^}:"\'`$\\]+', Punctuation),
+            (r':', Punctuation),
+            include('root'),
+        ],
+        'paren': [
+            (r'\)', Keyword, '#pop'),
+            include('root'),
+        ],
+        'math': [
+            (r'\)\)', Keyword, '#pop'),
+            (r'\*\*|\|\||<<|>>|[-+*/%^|&<>]', Operator),
+            (r'\d+#[\da-zA-Z]+', Number),
+            (r'\d+#(?! )', Number),
+            (r'0[xX][\da-fA-F]+', Number),
+            (r'\d+', Number),
+            (r'[a-zA-Z_]\w*', Name.Variable),  # user variable
+            include('root'),
+        ],
+        'backticks': [
+            (r'`', String.Backtick, '#pop'),
+            include('root'),
+        ],
+    }
+
+    def analyse_text(text):
+        if shebang_matches(text, r'(ba|z|)sh'):
+            return 1
+        if text.startswith('$ '):
+            return 0.2
+
+
+class SlurmBashLexer(BashLexer):
+    """
+    Lexer for (ba|k|z|)sh Slurm scripts.
+    """
+
+    name = 'Slurm'
+    aliases = ['slurm', 'sbatch']
+    filenames = ['*.sl']
+    mimetypes = []
+    version_added = '2.4'
+    EXTRA_KEYWORDS = {'srun'}
+
+    def get_tokens_unprocessed(self, text):
+        for index, token, value in BashLexer.get_tokens_unprocessed(self, text):
+            if token is Text and value in self.EXTRA_KEYWORDS:
+                yield index, Name.Builtin, value
+            elif token is Comment.Single and 'SBATCH' in value:
+                yield index, Keyword.Pseudo, value
+            else:
+                yield index, token, value
+
+
+class ShellSessionBaseLexer(Lexer):
+    """
+    Base lexer for shell sessions.
+
+    .. versionadded:: 2.1
+    """
+
+    _bare_continuation = False
+    _venv = re.compile(r'^(\([^)]*\))(\s*)')
+
+    def get_tokens_unprocessed(self, text):
+        innerlexer = self._innerLexerCls(**self.options)
+
+        pos = 0
+        curcode = ''
+        insertions = []
+        backslash_continuation = False
+
+        for match in line_re.finditer(text):
+            line = match.group()
+
+            venv_match = self._venv.match(line)
+            if venv_match:
+                venv = venv_match.group(1)
+                venv_whitespace = venv_match.group(2)
+                insertions.append((len(curcode),
+                                   [(0, Generic.Prompt.VirtualEnv, venv)]))
+                if venv_whitespace:
+                    insertions.append((len(curcode),
+                                       [(0, Text, venv_whitespace)]))
+                line = line[venv_match.end():]
+
+            m = self._ps1rgx.match(line)
+            if m:
+                # To support output lexers (say diff output), the output
+                # needs to be broken by prompts whenever the output lexer
+                # changes.
+                if not insertions:
+                    pos = match.start()
+
+                insertions.append((len(curcode),
+                                   [(0, Generic.Prompt, m.group(1))]))
+                curcode += m.group(2)
+                backslash_continuation = curcode.endswith('\\\n')
+            elif backslash_continuation:
+                if line.startswith(self._ps2):
+                    insertions.append((len(curcode),
+                                       [(0, Generic.Prompt,
+                                         line[:len(self._ps2)])]))
+                    curcode += line[len(self._ps2):]
+                else:
+                    curcode += line
+                backslash_continuation = curcode.endswith('\\\n')
+            elif self._bare_continuation and line.startswith(self._ps2):
+                insertions.append((len(curcode),
+                                   [(0, Generic.Prompt,
+                                     line[:len(self._ps2)])]))
+                curcode += line[len(self._ps2):]
+            else:
+                if insertions:
+                    toks = innerlexer.get_tokens_unprocessed(curcode)
+                    for i, t, v in do_insertions(insertions, toks):
+                        yield pos+i, t, v
+                yield match.start(), Generic.Output, line
+                insertions = []
+                curcode = ''
+        if insertions:
+            for i, t, v in do_insertions(insertions,
+                                         innerlexer.get_tokens_unprocessed(curcode)):
+                yield pos+i, t, v
+
+
+class BashSessionLexer(ShellSessionBaseLexer):
+    """
+    Lexer for Bash shell sessions, i.e. command lines, including a
+    prompt, interspersed with output.
+    """
+
+    name = 'Bash Session'
+    aliases = ['console', 'shell-session']
+    filenames = ['*.sh-session', '*.shell-session']
+    mimetypes = ['application/x-shell-session', 'application/x-sh-session']
+    url = 'https://en.wikipedia.org/wiki/Unix_shell'
+    version_added = '1.1'
+    _example = "console/example.sh-session"
+
+    _innerLexerCls = BashLexer
+    _ps1rgx = re.compile(
+        r'^((?:(?:\[.*?\])|(?:\(\S+\))?(?:| |sh\S*?|\w+\S+[@:]\S+(?:\s+\S+)' \
+        r'?|\[\S+[@:][^\n]+\].+))\s*[$#%]\s*)(.*\n?)')
+    _ps2 = '> '
+
+
+class BatchLexer(RegexLexer):
+    """
+    Lexer for the DOS/Windows Batch file format.
+    """
+    name = 'Batchfile'
+    aliases = ['batch', 'bat', 'dosbatch', 'winbatch']
+    filenames = ['*.bat', '*.cmd']
+    mimetypes = ['application/x-dos-batch']
+    url = 'https://en.wikipedia.org/wiki/Batch_file'
+    version_added = '0.7'
+
+    flags = re.MULTILINE | re.IGNORECASE
+
+    _nl = r'\n\x1a'
+    _punct = r'&<>|'
+    _ws = r'\t\v\f\r ,;=\xa0'
+    _nlws = r'\s\x1a\xa0,;='
+    _space = rf'(?:(?:(?:\^[{_nl}])?[{_ws}])+)'
+    _keyword_terminator = (rf'(?=(?:\^[{_nl}]?)?[{_ws}+./:[\\\]]|[{_nl}{_punct}(])')
+    _token_terminator = rf'(?=\^?[{_ws}]|[{_punct}{_nl}])'
+    _start_label = rf'((?:(?<=^[^:])|^[^:]?)[{_ws}]*)(:)'
+    _label = rf'(?:(?:[^{_nlws}{_punct}+:^]|\^[{_nl}]?[\w\W])*)'
+    _label_compound = rf'(?:(?:[^{_nlws}{_punct}+:^)]|\^[{_nl}]?[^)])*)'
+    _number = rf'(?:-?(?:0[0-7]+|0x[\da-f]+|\d+){_token_terminator})'
+    _opword = r'(?:equ|geq|gtr|leq|lss|neq)'
+    _string = rf'(?:"[^{_nl}"]*(?:"|(?=[{_nl}])))'
+    _variable = (r'(?:(?:%(?:\*|(?:~[a-z]*(?:\$[^:]+:)?)?\d|'
+                 rf'[^%:{_nl}]+(?::(?:~(?:-?\d+)?(?:,(?:-?\d+)?)?|(?:[^%{_nl}^]|'
+                 rf'\^[^%{_nl}])[^={_nl}]*=(?:[^%{_nl}^]|\^[^%{_nl}])*)?)?%))|'
+                 rf'(?:\^?![^!:{_nl}]+(?::(?:~(?:-?\d+)?(?:,(?:-?\d+)?)?|(?:'
+                 rf'[^!{_nl}^]|\^[^!{_nl}])[^={_nl}]*=(?:[^!{_nl}^]|\^[^!{_nl}])*)?)?\^?!))')
+    _core_token = rf'(?:(?:(?:\^[{_nl}]?)?[^"{_nlws}{_punct}])+)'
+    _core_token_compound = rf'(?:(?:(?:\^[{_nl}]?)?[^"{_nlws}{_punct})])+)'
+    _token = rf'(?:[{_punct}]+|{_core_token})'
+    _token_compound = rf'(?:[{_punct}]+|{_core_token_compound})'
+    _stoken = (rf'(?:[{_punct}]+|(?:{_string}|{_variable}|{_core_token})+)')
+
+    def _make_begin_state(compound, _core_token=_core_token,
+                          _core_token_compound=_core_token_compound,
+                          _keyword_terminator=_keyword_terminator,
+                          _nl=_nl, _punct=_punct, _string=_string,
+                          _space=_space, _start_label=_start_label,
+                          _stoken=_stoken, _token_terminator=_token_terminator,
+                          _variable=_variable, _ws=_ws):
+        rest = '(?:{}|{}|[^"%{}{}{}])*'.format(_string, _variable, _nl, _punct,
+                                            ')' if compound else '')
+        rest_of_line = rf'(?:(?:[^{_nl}^]|\^[{_nl}]?[\w\W])*)'
+        rest_of_line_compound = rf'(?:(?:[^{_nl}^)]|\^[{_nl}]?[^)])*)'
+        set_space = rf'((?:(?:\^[{_nl}]?)?[^\S\n])*)'
+        suffix = ''
+        if compound:
+            _keyword_terminator = rf'(?:(?=\))|{_keyword_terminator})'
+            _token_terminator = rf'(?:(?=\))|{_token_terminator})'
+            suffix = '/compound'
+        return [
+            ((r'\)', Punctuation, '#pop') if compound else
+             (rf'\)((?=\()|{_token_terminator}){rest_of_line}',
+              Comment.Single)),
+            (rf'(?={_start_label})', Text, f'follow{suffix}'),
+            (_space, using(this, state='text')),
+            include(f'redirect{suffix}'),
+            (rf'[{_nl}]+', Text),
+            (r'\(', Punctuation, 'root/compound'),
+            (r'@+', Punctuation),
+            (rf'((?:for|if|rem)(?:(?=(?:\^[{_nl}]?)?/)|(?:(?!\^)|'
+             rf'(?<=m))(?:(?=\()|{_token_terminator})))({_space}?{_core_token_compound if compound else _core_token}?(?:\^[{_nl}]?)?/(?:\^[{_nl}]?)?\?)',
+             bygroups(Keyword, using(this, state='text')),
+             f'follow{suffix}'),
+            (rf'(goto{_keyword_terminator})({rest}(?:\^[{_nl}]?)?/(?:\^[{_nl}]?)?\?{rest})',
+             bygroups(Keyword, using(this, state='text')),
+             f'follow{suffix}'),
+            (words(('assoc', 'break', 'cd', 'chdir', 'cls', 'color', 'copy',
+                    'date', 'del', 'dir', 'dpath', 'echo', 'endlocal', 'erase',
+                    'exit', 'ftype', 'keys', 'md', 'mkdir', 'mklink', 'move',
+                    'path', 'pause', 'popd', 'prompt', 'pushd', 'rd', 'ren',
+                    'rename', 'rmdir', 'setlocal', 'shift', 'start', 'time',
+                    'title', 'type', 'ver', 'verify', 'vol'),
+                   suffix=_keyword_terminator), Keyword, f'follow{suffix}'),
+            (rf'(call)({_space}?)(:)',
+             bygroups(Keyword, using(this, state='text'), Punctuation),
+             f'call{suffix}'),
+            (rf'call{_keyword_terminator}', Keyword),
+            (rf'(for{_token_terminator}(?!\^))({_space})(/f{_token_terminator})',
+             bygroups(Keyword, using(this, state='text'), Keyword),
+             ('for/f', 'for')),
+            (rf'(for{_token_terminator}(?!\^))({_space})(/l{_token_terminator})',
+             bygroups(Keyword, using(this, state='text'), Keyword),
+             ('for/l', 'for')),
+            (rf'for{_token_terminator}(?!\^)', Keyword, ('for2', 'for')),
+            (rf'(goto{_keyword_terminator})({_space}?)(:?)',
+             bygroups(Keyword, using(this, state='text'), Punctuation),
+             f'label{suffix}'),
+            (rf'(if(?:(?=\()|{_token_terminator})(?!\^))({_space}?)((?:/i{_token_terminator})?)({_space}?)((?:not{_token_terminator})?)({_space}?)',
+             bygroups(Keyword, using(this, state='text'), Keyword,
+                      using(this, state='text'), Keyword,
+                      using(this, state='text')), ('(?', 'if')),
+            (rf'rem(((?=\()|{_token_terminator}){_space}?{_stoken}?.*|{_keyword_terminator}{rest_of_line_compound if compound else rest_of_line})',
+             Comment.Single, f'follow{suffix}'),
+            (rf'(set{_keyword_terminator}){set_space}(/a)',
+             bygroups(Keyword, using(this, state='text'), Keyword),
+             f'arithmetic{suffix}'),
+            (r'(set{}){}((?:/p)?){}((?:(?:(?:\^[{}]?)?[^"{}{}^={}]|'
+             r'\^[{}]?[^"=])+)?)((?:(?:\^[{}]?)?=)?)'.format(_keyword_terminator, set_space, set_space, _nl, _nl, _punct,
+              ')' if compound else '', _nl, _nl),
+             bygroups(Keyword, using(this, state='text'), Keyword,
+                      using(this, state='text'), using(this, state='variable'),
+                      Punctuation),
+             f'follow{suffix}'),
+            default(f'follow{suffix}')
+        ]
+
+    def _make_follow_state(compound, _label=_label,
+                           _label_compound=_label_compound, _nl=_nl,
+                           _space=_space, _start_label=_start_label,
+                           _token=_token, _token_compound=_token_compound,
+                           _ws=_ws):
+        suffix = '/compound' if compound else ''
+        state = []
+        if compound:
+            state.append((r'(?=\))', Text, '#pop'))
+        state += [
+            (rf'{_start_label}([{_ws}]*)({_label_compound if compound else _label})(.*)',
+             bygroups(Text, Punctuation, Text, Name.Label, Comment.Single)),
+            include(f'redirect{suffix}'),
+            (rf'(?=[{_nl}])', Text, '#pop'),
+            (r'\|\|?|&&?', Punctuation, '#pop'),
+            include('text')
+        ]
+        return state
+
+    def _make_arithmetic_state(compound, _nl=_nl, _punct=_punct,
+                               _string=_string, _variable=_variable,
+                               _ws=_ws, _nlws=_nlws):
+        op = r'=+\-*/!~'
+        state = []
+        if compound:
+            state.append((r'(?=\))', Text, '#pop'))
+        state += [
+            (r'0[0-7]+', Number.Oct),
+            (r'0x[\da-f]+', Number.Hex),
+            (r'\d+', Number.Integer),
+            (r'[(),]+', Punctuation),
+            (rf'([{op}]|%|\^\^)+', Operator),
+            (r'({}|{}|(\^[{}]?)?[^(){}%\^"{}{}]|\^[{}]?{})+'.format(_string, _variable, _nl, op, _nlws, _punct, _nlws,
+              r'[^)]' if compound else r'[\w\W]'),
+             using(this, state='variable')),
+            (r'(?=[\x00|&])', Text, '#pop'),
+            include('follow')
+        ]
+        return state
+
+    def _make_call_state(compound, _label=_label,
+                         _label_compound=_label_compound):
+        state = []
+        if compound:
+            state.append((r'(?=\))', Text, '#pop'))
+        state.append((r'(:?)(%s)' % (_label_compound if compound else _label),
+                      bygroups(Punctuation, Name.Label), '#pop'))
+        return state
+
+    def _make_label_state(compound, _label=_label,
+                          _label_compound=_label_compound, _nl=_nl,
+                          _punct=_punct, _string=_string, _variable=_variable):
+        state = []
+        if compound:
+            state.append((r'(?=\))', Text, '#pop'))
+        state.append((r'({}?)((?:{}|{}|\^[{}]?{}|[^"%^{}{}{}])*)'.format(_label_compound if compound else _label, _string,
+                       _variable, _nl, r'[^)]' if compound else r'[\w\W]', _nl,
+                       _punct, r')' if compound else ''),
+                      bygroups(Name.Label, Comment.Single), '#pop'))
+        return state
+
+    def _make_redirect_state(compound,
+                             _core_token_compound=_core_token_compound,
+                             _nl=_nl, _punct=_punct, _stoken=_stoken,
+                             _string=_string, _space=_space,
+                             _variable=_variable, _nlws=_nlws):
+        stoken_compound = (rf'(?:[{_punct}]+|(?:{_string}|{_variable}|{_core_token_compound})+)')
+        return [
+            (rf'((?:(?<=[{_nlws}])\d)?)(>>?&|<&)([{_nlws}]*)(\d)',
+             bygroups(Number.Integer, Punctuation, Text, Number.Integer)),
+            (rf'((?:(?<=[{_nlws}])(?>?|<)({_space}?{stoken_compound if compound else _stoken})',
+             bygroups(Number.Integer, Punctuation, using(this, state='text')))
+        ]
+
+    tokens = {
+        'root': _make_begin_state(False),
+        'follow': _make_follow_state(False),
+        'arithmetic': _make_arithmetic_state(False),
+        'call': _make_call_state(False),
+        'label': _make_label_state(False),
+        'redirect': _make_redirect_state(False),
+        'root/compound': _make_begin_state(True),
+        'follow/compound': _make_follow_state(True),
+        'arithmetic/compound': _make_arithmetic_state(True),
+        'call/compound': _make_call_state(True),
+        'label/compound': _make_label_state(True),
+        'redirect/compound': _make_redirect_state(True),
+        'variable-or-escape': [
+            (_variable, Name.Variable),
+            (rf'%%|\^[{_nl}]?(\^!|[\w\W])', String.Escape)
+        ],
+        'string': [
+            (r'"', String.Double, '#pop'),
+            (_variable, Name.Variable),
+            (r'\^!|%%', String.Escape),
+            (rf'[^"%^{_nl}]+|[%^]', String.Double),
+            default('#pop')
+        ],
+        'sqstring': [
+            include('variable-or-escape'),
+            (r'[^%]+|%', String.Single)
+        ],
+        'bqstring': [
+            include('variable-or-escape'),
+            (r'[^%]+|%', String.Backtick)
+        ],
+        'text': [
+            (r'"', String.Double, 'string'),
+            include('variable-or-escape'),
+            (rf'[^"%^{_nlws}{_punct}\d)]+|.', Text)
+        ],
+        'variable': [
+            (r'"', String.Double, 'string'),
+            include('variable-or-escape'),
+            (rf'[^"%^{_nl}]+|.', Name.Variable)
+        ],
+        'for': [
+            (rf'({_space})(in)({_space})(\()',
+             bygroups(using(this, state='text'), Keyword,
+                      using(this, state='text'), Punctuation), '#pop'),
+            include('follow')
+        ],
+        'for2': [
+            (r'\)', Punctuation),
+            (rf'({_space})(do{_token_terminator})',
+             bygroups(using(this, state='text'), Keyword), '#pop'),
+            (rf'[{_nl}]+', Text),
+            include('follow')
+        ],
+        'for/f': [
+            (rf'(")((?:{_variable}|[^"])*?")([{_nlws}]*)(\))',
+             bygroups(String.Double, using(this, state='string'), Text,
+                      Punctuation)),
+            (r'"', String.Double, ('#pop', 'for2', 'string')),
+            (rf"('(?:%%|{_variable}|[\w\W])*?')([{_nlws}]*)(\))",
+             bygroups(using(this, state='sqstring'), Text, Punctuation)),
+            (rf'(`(?:%%|{_variable}|[\w\W])*?`)([{_nlws}]*)(\))',
+             bygroups(using(this, state='bqstring'), Text, Punctuation)),
+            include('for2')
+        ],
+        'for/l': [
+            (r'-?\d+', Number.Integer),
+            include('for2')
+        ],
+        'if': [
+            (rf'((?:cmdextversion|errorlevel){_token_terminator})({_space})(\d+)',
+             bygroups(Keyword, using(this, state='text'),
+                      Number.Integer), '#pop'),
+            (rf'(defined{_token_terminator})({_space})({_stoken})',
+             bygroups(Keyword, using(this, state='text'),
+                      using(this, state='variable')), '#pop'),
+            (rf'(exist{_token_terminator})({_space}{_stoken})',
+             bygroups(Keyword, using(this, state='text')), '#pop'),
+            (rf'({_number}{_space})({_opword})({_space}{_number})',
+             bygroups(using(this, state='arithmetic'), Operator.Word,
+                      using(this, state='arithmetic')), '#pop'),
+            (_stoken, using(this, state='text'), ('#pop', 'if2')),
+        ],
+        'if2': [
+            (rf'({_space}?)(==)({_space}?{_stoken})',
+             bygroups(using(this, state='text'), Operator,
+                      using(this, state='text')), '#pop'),
+            (rf'({_space})({_opword})({_space}{_stoken})',
+             bygroups(using(this, state='text'), Operator.Word,
+                      using(this, state='text')), '#pop')
+        ],
+        '(?': [
+            (_space, using(this, state='text')),
+            (r'\(', Punctuation, ('#pop', 'else?', 'root/compound')),
+            default('#pop')
+        ],
+        'else?': [
+            (_space, using(this, state='text')),
+            (rf'else{_token_terminator}', Keyword, '#pop'),
+            default('#pop')
+        ]
+    }
+
+
+class MSDOSSessionLexer(ShellSessionBaseLexer):
+    """
+    Lexer for MS DOS shell sessions, i.e. command lines, including a
+    prompt, interspersed with output.
+    """
+
+    name = 'MSDOS Session'
+    aliases = ['doscon']
+    filenames = []
+    mimetypes = []
+    url = 'https://en.wikipedia.org/wiki/MS-DOS'
+    version_added = '2.1'
+    _example = "doscon/session"
+
+    _innerLexerCls = BatchLexer
+    _ps1rgx = re.compile(r'^([^>]*>)(.*\n?)')
+    _ps2 = 'More? '
+
+
+class TcshLexer(RegexLexer):
+    """
+    Lexer for tcsh scripts.
+    """
+
+    name = 'Tcsh'
+    aliases = ['tcsh', 'csh']
+    filenames = ['*.tcsh', '*.csh']
+    mimetypes = ['application/x-csh']
+    url = 'https://www.tcsh.org'
+    version_added = '0.10'
+
+    tokens = {
+        'root': [
+            include('basic'),
+            (r'\$\(', Keyword, 'paren'),
+            (r'\$\{#?', Keyword, 'curly'),
+            (r'`', String.Backtick, 'backticks'),
+            include('data'),
+        ],
+        'basic': [
+            (r'\b(if|endif|else|while|then|foreach|case|default|'
+             r'break|continue|goto|breaksw|end|switch|endsw)\s*\b',
+             Keyword),
+            (r'\b(alias|alloc|bg|bindkey|builtins|bye|caller|cd|chdir|'
+             r'complete|dirs|echo|echotc|eval|exec|exit|fg|filetest|getxvers|'
+             r'glob|getspath|hashstat|history|hup|inlib|jobs|kill|'
+             r'limit|log|login|logout|ls-F|migrate|newgrp|nice|nohup|notify|'
+             r'onintr|popd|printenv|pushd|rehash|repeat|rootnode|popd|pushd|'
+             r'set|shift|sched|setenv|setpath|settc|setty|setxvers|shift|'
+             r'source|stop|suspend|source|suspend|telltc|time|'
+             r'umask|unalias|uncomplete|unhash|universe|unlimit|unset|unsetenv|'
+             r'ver|wait|warp|watchlog|where|which)\s*\b',
+             Name.Builtin),
+            (r'#.*', Comment),
+            (r'\\[\w\W]', String.Escape),
+            (r'(\b\w+)(\s*)(=)', bygroups(Name.Variable, Text, Operator)),
+            (r'[\[\]{}()=]+', Operator),
+            (r'<<\s*(\'?)\\?(\w+)[\w\W]+?\2', String),
+            (r';', Punctuation),
+        ],
+        'data': [
+            (r'(?s)"(\\\\|\\[0-7]+|\\.|[^"\\])*"', String.Double),
+            (r"(?s)'(\\\\|\\[0-7]+|\\.|[^'\\])*'", String.Single),
+            (r'\s+', Text),
+            (r'[^=\s\[\]{}()$"\'`\\;#]+', Text),
+            (r'\d+(?= |\Z)', Number),
+            (r'\$#?(\w+|.)', Name.Variable),
+        ],
+        'curly': [
+            (r'\}', Keyword, '#pop'),
+            (r':-', Keyword),
+            (r'\w+', Name.Variable),
+            (r'[^}:"\'`$]+', Punctuation),
+            (r':', Punctuation),
+            include('root'),
+        ],
+        'paren': [
+            (r'\)', Keyword, '#pop'),
+            include('root'),
+        ],
+        'backticks': [
+            (r'`', String.Backtick, '#pop'),
+            include('root'),
+        ],
+    }
+
+
+class TcshSessionLexer(ShellSessionBaseLexer):
+    """
+    Lexer for Tcsh sessions, i.e. command lines, including a
+    prompt, interspersed with output.
+    """
+
+    name = 'Tcsh Session'
+    aliases = ['tcshcon']
+    filenames = []
+    mimetypes = []
+    url = 'https://www.tcsh.org'
+    version_added = '2.1'
+    _example = "tcshcon/session"
+
+    _innerLexerCls = TcshLexer
+    _ps1rgx = re.compile(r'^([^>]+>)(.*\n?)')
+    _ps2 = '? '
+
+
+class PowerShellLexer(RegexLexer):
+    """
+    For Windows PowerShell code.
+    """
+    name = 'PowerShell'
+    aliases = ['powershell', 'pwsh', 'posh', 'ps1', 'psm1']
+    filenames = ['*.ps1', '*.psm1']
+    mimetypes = ['text/x-powershell']
+    url = 'https://learn.microsoft.com/en-us/powershell'
+    version_added = '1.5'
+
+    flags = re.DOTALL | re.IGNORECASE | re.MULTILINE
+
+    keywords = (
+        'while validateset validaterange validatepattern validatelength '
+        'validatecount until trap switch return ref process param parameter in '
+        'if global: local: function foreach for finally filter end elseif else '
+        'dynamicparam do default continue cmdletbinding break begin alias \\? '
+        '% #script #private #local #global mandatory parametersetname position '
+        'valuefrompipeline valuefrompipelinebypropertyname '
+        'valuefromremainingarguments helpmessage try catch throw').split()
+
+    operators = (
+        'and as band bnot bor bxor casesensitive ccontains ceq cge cgt cle '
+        'clike clt cmatch cne cnotcontains cnotlike cnotmatch contains '
+        'creplace eq exact f file ge gt icontains ieq ige igt ile ilike ilt '
+        'imatch ine inotcontains inotlike inotmatch ireplace is isnot le like '
+        'lt match ne not notcontains notlike notmatch or regex replace '
+        'wildcard').split()
+
+    verbs = (
+        'write where watch wait use update unregister unpublish unprotect '
+        'unlock uninstall undo unblock trace test tee take sync switch '
+        'suspend submit stop step start split sort skip show set send select '
+        'search scroll save revoke resume restore restart resolve resize '
+        'reset request repair rename remove register redo receive read push '
+        'publish protect pop ping out optimize open new move mount merge '
+        'measure lock limit join invoke install initialize import hide group '
+        'grant get format foreach find export expand exit enter enable edit '
+        'dismount disconnect disable deny debug cxnew copy convertto '
+        'convertfrom convert connect confirm compress complete compare close '
+        'clear checkpoint block backup assert approve aggregate add').split()
+
+    aliases_ = (
+        'ac asnp cat cd cfs chdir clc clear clhy cli clp cls clv cnsn '
+        'compare copy cp cpi cpp curl cvpa dbp del diff dir dnsn ebp echo epal '
+        'epcsv epsn erase etsn exsn fc fhx fl foreach ft fw gal gbp gc gci gcm '
+        'gcs gdr ghy gi gjb gl gm gmo gp gps gpv group gsn gsnp gsv gu gv gwmi '
+        'h history icm iex ihy ii ipal ipcsv ipmo ipsn irm ise iwmi iwr kill lp '
+        'ls man md measure mi mount move mp mv nal ndr ni nmo npssc nsn nv ogv '
+        'oh popd ps pushd pwd r rbp rcjb rcsn rd rdr ren ri rjb rm rmdir rmo '
+        'rni rnp rp rsn rsnp rujb rv rvpa rwmi sajb sal saps sasv sbp sc select '
+        'set shcm si sl sleep sls sort sp spjb spps spsv start sujb sv swmi tee '
+        'trcm type wget where wjb write').split()
+
+    commenthelp = (
+        'component description example externalhelp forwardhelpcategory '
+        'forwardhelptargetname functionality inputs link '
+        'notes outputs parameter remotehelprunspace role synopsis').split()
+
+    tokens = {
+        'root': [
+            # we need to count pairs of parentheses for correct highlight
+            # of '$(...)' blocks in strings
+            (r'\(', Punctuation, 'child'),
+            (r'\s+', Text),
+            (r'^(\s*#[#\s]*)(\.(?:{}))([^\n]*$)'.format('|'.join(commenthelp)),
+             bygroups(Comment, String.Doc, Comment)),
+            (r'#[^\n]*?$', Comment),
+            (r'(<|<)#', Comment.Multiline, 'multline'),
+            (r'@"\n', String.Heredoc, 'heredoc-double'),
+            (r"@'\n.*?\n'@", String.Heredoc),
+            # escaped syntax
+            (r'`[\'"$@-]', Punctuation),
+            (r'"', String.Double, 'string'),
+            (r"'([^']|'')*'", String.Single),
+            (r'(\$|@@|@)((global|script|private|env):)?\w+',
+             Name.Variable),
+            (r'({})\b'.format('|'.join(keywords)), Keyword),
+            (r'-({})\b'.format('|'.join(operators)), Operator),
+            (r'({})-[a-z_]\w*\b'.format('|'.join(verbs)), Name.Builtin),
+            (r'({})\s'.format('|'.join(aliases_)), Name.Builtin),
+            (r'\[[a-z_\[][\w. `,\[\]]*\]', Name.Constant),  # .net [type]s
+            (r'-[a-z_]\w*', Name),
+            (r'\w+', Name),
+            (r'[.,;:@{}\[\]$()=+*/\\&%!~?^`|<>-]', Punctuation),
+        ],
+        'child': [
+            (r'\)', Punctuation, '#pop'),
+            include('root'),
+        ],
+        'multline': [
+            (r'[^#&.]+', Comment.Multiline),
+            (r'#(>|>)', Comment.Multiline, '#pop'),
+            (r'\.({})'.format('|'.join(commenthelp)), String.Doc),
+            (r'[#&.]', Comment.Multiline),
+        ],
+        'string': [
+            (r"`[0abfnrtv'\"$`]", String.Escape),
+            (r'[^$`"]+', String.Double),
+            (r'\$\(', Punctuation, 'child'),
+            (r'""', String.Double),
+            (r'[`$]', String.Double),
+            (r'"', String.Double, '#pop'),
+        ],
+        'heredoc-double': [
+            (r'\n"@', String.Heredoc, '#pop'),
+            (r'\$\(', Punctuation, 'child'),
+            (r'[^@\n]+"]', String.Heredoc),
+            (r".", String.Heredoc),
+        ]
+    }
+
+
+class PowerShellSessionLexer(ShellSessionBaseLexer):
+    """
+    Lexer for PowerShell sessions, i.e. command lines, including a
+    prompt, interspersed with output.
+    """
+
+    name = 'PowerShell Session'
+    aliases = ['pwsh-session', 'ps1con']
+    filenames = []
+    mimetypes = []
+    url = 'https://learn.microsoft.com/en-us/powershell'
+    version_added = '2.1'
+    _example = "pwsh-session/session"
+
+    _innerLexerCls = PowerShellLexer
+    _bare_continuation = True
+    _ps1rgx = re.compile(r'^((?:\[[^]]+\]: )?PS[^>]*> ?)(.*\n?)')
+    _ps2 = '> '
+
+
+class FishShellLexer(RegexLexer):
+    """
+    Lexer for Fish shell scripts.
+    """
+
+    name = 'Fish'
+    aliases = ['fish', 'fishshell']
+    filenames = ['*.fish', '*.load']
+    mimetypes = ['application/x-fish']
+    url = 'https://fishshell.com'
+    version_added = '2.1'
+
+    tokens = {
+        'root': [
+            include('basic'),
+            include('data'),
+            include('interp'),
+        ],
+        'interp': [
+            (r'\$\(\(', Keyword, 'math'),
+            (r'\(', Keyword, 'paren'),
+            (r'\$#?(\w+|.)', Name.Variable),
+        ],
+        'basic': [
+            (r'\b(begin|end|if|else|while|break|for|in|return|function|block|'
+             r'case|continue|switch|not|and|or|set|echo|exit|pwd|true|false|'
+             r'cd|count|test)(\s*)\b',
+             bygroups(Keyword, Text)),
+            (r'\b(alias|bg|bind|breakpoint|builtin|command|commandline|'
+             r'complete|contains|dirh|dirs|emit|eval|exec|fg|fish|fish_config|'
+             r'fish_indent|fish_pager|fish_prompt|fish_right_prompt|'
+             r'fish_update_completions|fishd|funced|funcsave|functions|help|'
+             r'history|isatty|jobs|math|mimedb|nextd|open|popd|prevd|psub|'
+             r'pushd|random|read|set_color|source|status|trap|type|ulimit|'
+             r'umask|vared|fc|getopts|hash|kill|printf|time|wait)\s*\b(?!\.)',
+             Name.Builtin),
+            (r'#.*\n', Comment),
+            (r'\\[\w\W]', String.Escape),
+            (r'(\b\w+)(\s*)(=)', bygroups(Name.Variable, Whitespace, Operator)),
+            (r'[\[\]()=]', Operator),
+            (r'<<-?\s*(\'?)\\?(\w+)[\w\W]+?\2', String),
+        ],
+        'data': [
+            (r'(?s)\$?"(\\\\|\\[0-7]+|\\.|[^"\\$])*"', String.Double),
+            (r'"', String.Double, 'string'),
+            (r"(?s)\$'(\\\\|\\[0-7]+|\\.|[^'\\])*'", String.Single),
+            (r"(?s)'.*?'", String.Single),
+            (r';', Punctuation),
+            (r'&|\||\^|<|>', Operator),
+            (r'\s+', Text),
+            (r'\d+(?= |\Z)', Number),
+            (r'[^=\s\[\]{}()$"\'`\\<&|;]+', Text),
+        ],
+        'string': [
+            (r'"', String.Double, '#pop'),
+            (r'(?s)(\\\\|\\[0-7]+|\\.|[^"\\$])+', String.Double),
+            include('interp'),
+        ],
+        'paren': [
+            (r'\)', Keyword, '#pop'),
+            include('root'),
+        ],
+        'math': [
+            (r'\)\)', Keyword, '#pop'),
+            (r'[-+*/%^|&]|\*\*|\|\|', Operator),
+            (r'\d+#\d+', Number),
+            (r'\d+#(?! )', Number),
+            (r'\d+', Number),
+            include('root'),
+        ],
+    }
+
+class ExeclineLexer(RegexLexer):
+    """
+    Lexer for Laurent Bercot's execline language.
+    """
+
+    name = 'execline'
+    aliases = ['execline']
+    filenames = ['*.exec']
+    url = 'https://skarnet.org/software/execline'
+    version_added = '2.7'
+
+    tokens = {
+        'root': [
+            include('basic'),
+            include('data'),
+            include('interp')
+        ],
+        'interp': [
+            (r'\$\{', String.Interpol, 'curly'),
+            (r'\$[\w@#]+', Name.Variable),  # user variable
+            (r'\$', Text),
+        ],
+        'basic': [
+            (r'\b(background|backtick|cd|define|dollarat|elgetopt|'
+             r'elgetpositionals|elglob|emptyenv|envfile|exec|execlineb|'
+             r'exit|export|fdblock|fdclose|fdmove|fdreserve|fdswap|'
+             r'forbacktickx|foreground|forstdin|forx|getcwd|getpid|heredoc|'
+             r'homeof|if|ifelse|ifte|ifthenelse|importas|loopwhilex|'
+             r'multidefine|multisubstitute|pipeline|piperw|posix-cd|'
+             r'redirfd|runblock|shift|trap|tryexec|umask|unexport|wait|'
+             r'withstdinas)\b', Name.Builtin),
+            (r'\A#!.+\n', Comment.Hashbang),
+            (r'#.*\n', Comment.Single),
+            (r'[{}]', Operator)
+        ],
+        'data': [
+            (r'(?s)"(\\.|[^"\\$])*"', String.Double),
+            (r'"', String.Double, 'string'),
+            (r'\s+', Text),
+            (r'[^\s{}$"\\]+', Text)
+        ],
+        'string': [
+            (r'"', String.Double, '#pop'),
+            (r'(?s)(\\\\|\\.|[^"\\$])+', String.Double),
+            include('interp'),
+        ],
+        'curly': [
+            (r'\}', String.Interpol, '#pop'),
+            (r'[\w#@]+', Name.Variable),
+            include('root')
+        ]
+
+    }
+
+    def analyse_text(text):
+        if shebang_matches(text, r'execlineb'):
+            return 1
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/sieve.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/sieve.py
new file mode 100644
index 00000000..fc48980c
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/sieve.py
@@ -0,0 +1,78 @@
+"""
+    pygments.lexers.sieve
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    Lexer for Sieve file format.
+
+    https://tools.ietf.org/html/rfc5228
+    https://tools.ietf.org/html/rfc5173
+    https://tools.ietf.org/html/rfc5229
+    https://tools.ietf.org/html/rfc5230
+    https://tools.ietf.org/html/rfc5232
+    https://tools.ietf.org/html/rfc5235
+    https://tools.ietf.org/html/rfc5429
+    https://tools.ietf.org/html/rfc8580
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, bygroups
+from pygments.token import Comment, Name, Literal, String, Text, Punctuation, \
+    Keyword
+
+__all__ = ["SieveLexer"]
+
+
+class SieveLexer(RegexLexer):
+    """
+    Lexer for sieve format.
+    """
+    name = 'Sieve'
+    filenames = ['*.siv', '*.sieve']
+    aliases = ['sieve']
+    url = 'https://en.wikipedia.org/wiki/Sieve_(mail_filtering_language)'
+    version_added = '2.6'
+
+    tokens = {
+        'root': [
+            (r'\s+', Text),
+            (r'[();,{}\[\]]', Punctuation),
+            # import:
+            (r'(?i)require',
+             Keyword.Namespace),
+            # tags:
+            (r'(?i)(:)(addresses|all|contains|content|create|copy|comparator|'
+             r'count|days|detail|domain|fcc|flags|from|handle|importance|is|'
+             r'localpart|length|lowerfirst|lower|matches|message|mime|options|'
+             r'over|percent|quotewildcard|raw|regex|specialuse|subject|text|'
+             r'under|upperfirst|upper|value)',
+             bygroups(Name.Tag, Name.Tag)),
+            # tokens:
+            (r'(?i)(address|addflag|allof|anyof|body|discard|elsif|else|envelope|'
+             r'ereject|exists|false|fileinto|if|hasflag|header|keep|'
+             r'notify_method_capability|notify|not|redirect|reject|removeflag|'
+             r'setflag|size|spamtest|stop|string|true|vacation|virustest)',
+             Name.Builtin),
+            (r'(?i)set',
+             Keyword.Declaration),
+            # number:
+            (r'([0-9.]+)([kmgKMG])?',
+             bygroups(Literal.Number, Literal.Number)),
+            # comment:
+            (r'#.*$',
+             Comment.Single),
+            (r'/\*.*\*/',
+             Comment.Multiline),
+            # string:
+            (r'"[^"]*?"',
+             String),
+            # text block:
+            (r'text:',
+             Name.Tag, 'text'),
+        ],
+        'text': [
+            (r'[^.].*?\n', String),
+            (r'^\.', Punctuation, "#pop"),
+        ]
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/slash.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/slash.py
new file mode 100644
index 00000000..1c439d0d
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/slash.py
@@ -0,0 +1,183 @@
+"""
+    pygments.lexers.slash
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    Lexer for the Slash programming language.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import ExtendedRegexLexer, bygroups, DelegatingLexer
+from pygments.token import Name, Number, String, Comment, Punctuation, \
+    Other, Keyword, Operator, Whitespace
+
+__all__ = ['SlashLexer']
+
+
+class SlashLanguageLexer(ExtendedRegexLexer):
+    _nkw = r'(?=[^a-zA-Z_0-9])'
+
+    def move_state(new_state):
+        return ("#pop", new_state)
+
+    def right_angle_bracket(lexer, match, ctx):
+        if len(ctx.stack) > 1 and ctx.stack[-2] == "string":
+            ctx.stack.pop()
+        yield match.start(), String.Interpol, '}'
+        ctx.pos = match.end()
+        pass
+
+    tokens = {
+        "root": [
+            (r"<%=",        Comment.Preproc,    move_state("slash")),
+            (r"<%!!",       Comment.Preproc,    move_state("slash")),
+            (r"<%#.*?%>",   Comment.Multiline),
+            (r"<%",         Comment.Preproc,    move_state("slash")),
+            (r".|\n",       Other),
+        ],
+        "string": [
+            (r"\\",         String.Escape,      move_state("string_e")),
+            (r"\"",         String,             move_state("slash")),
+            (r"#\{",        String.Interpol,    "slash"),
+            (r'.|\n',       String),
+        ],
+        "string_e": [
+            (r'n',                  String.Escape,      move_state("string")),
+            (r't',                  String.Escape,      move_state("string")),
+            (r'r',                  String.Escape,      move_state("string")),
+            (r'e',                  String.Escape,      move_state("string")),
+            (r'x[a-fA-F0-9]{2}',    String.Escape,      move_state("string")),
+            (r'.',                  String.Escape,      move_state("string")),
+        ],
+        "regexp": [
+            (r'}[a-z]*',            String.Regex,       move_state("slash")),
+            (r'\\(.|\n)',           String.Regex),
+            (r'{',                  String.Regex,       "regexp_r"),
+            (r'.|\n',               String.Regex),
+        ],
+        "regexp_r": [
+            (r'}[a-z]*',            String.Regex,       "#pop"),
+            (r'\\(.|\n)',           String.Regex),
+            (r'{',                  String.Regex,       "regexp_r"),
+        ],
+        "slash": [
+            (r"%>",                     Comment.Preproc,    move_state("root")),
+            (r"\"",                     String,             move_state("string")),
+            (r"'[a-zA-Z0-9_]+",         String),
+            (r'%r{',                    String.Regex,       move_state("regexp")),
+            (r'/\*.*?\*/',              Comment.Multiline),
+            (r"(#|//).*?\n",            Comment.Single),
+            (r'-?[0-9]+e[+-]?[0-9]+',   Number.Float),
+            (r'-?[0-9]+\.[0-9]+(e[+-]?[0-9]+)?', Number.Float),
+            (r'-?[0-9]+',               Number.Integer),
+            (r'nil'+_nkw,               Name.Builtin),
+            (r'true'+_nkw,              Name.Builtin),
+            (r'false'+_nkw,             Name.Builtin),
+            (r'self'+_nkw,              Name.Builtin),
+            (r'(class)(\s+)([A-Z][a-zA-Z0-9_\']*)',
+                bygroups(Keyword, Whitespace, Name.Class)),
+            (r'class'+_nkw,             Keyword),
+            (r'extends'+_nkw,           Keyword),
+            (r'(def)(\s+)(self)(\s*)(\.)(\s*)([a-z_][a-zA-Z0-9_\']*=?|<<|>>|==|<=>|<=|<|>=|>|\+|-(self)?|~(self)?|\*|/|%|^|&&|&|\||\[\]=?)',
+                bygroups(Keyword, Whitespace, Name.Builtin, Whitespace, Punctuation, Whitespace, Name.Function)),
+            (r'(def)(\s+)([a-z_][a-zA-Z0-9_\']*=?|<<|>>|==|<=>|<=|<|>=|>|\+|-(self)?|~(self)?|\*|/|%|^|&&|&|\||\[\]=?)',
+                bygroups(Keyword, Whitespace, Name.Function)),
+            (r'def'+_nkw,               Keyword),
+            (r'if'+_nkw,                Keyword),
+            (r'elsif'+_nkw,             Keyword),
+            (r'else'+_nkw,              Keyword),
+            (r'unless'+_nkw,            Keyword),
+            (r'for'+_nkw,               Keyword),
+            (r'in'+_nkw,                Keyword),
+            (r'while'+_nkw,             Keyword),
+            (r'until'+_nkw,             Keyword),
+            (r'and'+_nkw,               Keyword),
+            (r'or'+_nkw,                Keyword),
+            (r'not'+_nkw,               Keyword),
+            (r'lambda'+_nkw,            Keyword),
+            (r'try'+_nkw,               Keyword),
+            (r'catch'+_nkw,             Keyword),
+            (r'return'+_nkw,            Keyword),
+            (r'next'+_nkw,              Keyword),
+            (r'last'+_nkw,              Keyword),
+            (r'throw'+_nkw,             Keyword),
+            (r'use'+_nkw,               Keyword),
+            (r'switch'+_nkw,            Keyword),
+            (r'\\',                     Keyword),
+            (r'λ',                      Keyword),
+            (r'__FILE__'+_nkw,          Name.Builtin.Pseudo),
+            (r'__LINE__'+_nkw,          Name.Builtin.Pseudo),
+            (r'[A-Z][a-zA-Z0-9_\']*'+_nkw, Name.Constant),
+            (r'[a-z_][a-zA-Z0-9_\']*'+_nkw, Name),
+            (r'@[a-z_][a-zA-Z0-9_\']*'+_nkw, Name.Variable.Instance),
+            (r'@@[a-z_][a-zA-Z0-9_\']*'+_nkw, Name.Variable.Class),
+            (r'\(',                     Punctuation),
+            (r'\)',                     Punctuation),
+            (r'\[',                     Punctuation),
+            (r'\]',                     Punctuation),
+            (r'\{',                     Punctuation),
+            (r'\}',                     right_angle_bracket),
+            (r';',                      Punctuation),
+            (r',',                      Punctuation),
+            (r'<<=',                    Operator),
+            (r'>>=',                    Operator),
+            (r'<<',                     Operator),
+            (r'>>',                     Operator),
+            (r'==',                     Operator),
+            (r'!=',                     Operator),
+            (r'=>',                     Operator),
+            (r'=',                      Operator),
+            (r'<=>',                    Operator),
+            (r'<=',                     Operator),
+            (r'>=',                     Operator),
+            (r'<',                      Operator),
+            (r'>',                      Operator),
+            (r'\+\+',                   Operator),
+            (r'\+=',                    Operator),
+            (r'-=',                     Operator),
+            (r'\*\*=',                  Operator),
+            (r'\*=',                    Operator),
+            (r'\*\*',                   Operator),
+            (r'\*',                     Operator),
+            (r'/=',                     Operator),
+            (r'\+',                     Operator),
+            (r'-',                      Operator),
+            (r'/',                      Operator),
+            (r'%=',                     Operator),
+            (r'%',                      Operator),
+            (r'^=',                     Operator),
+            (r'&&=',                    Operator),
+            (r'&=',                     Operator),
+            (r'&&',                     Operator),
+            (r'&',                      Operator),
+            (r'\|\|=',                  Operator),
+            (r'\|=',                    Operator),
+            (r'\|\|',                   Operator),
+            (r'\|',                     Operator),
+            (r'!',                      Operator),
+            (r'\.\.\.',                 Operator),
+            (r'\.\.',                   Operator),
+            (r'\.',                     Operator),
+            (r'::',                     Operator),
+            (r':',                      Operator),
+            (r'(\s|\n)+',               Whitespace),
+            (r'[a-z_][a-zA-Z0-9_\']*',  Name.Variable),
+        ],
+    }
+
+
+class SlashLexer(DelegatingLexer):
+    """
+    Lexer for the Slash programming language.
+    """
+
+    name = 'Slash'
+    aliases = ['slash']
+    filenames = ['*.sla']
+    url = 'https://github.com/arturadib/Slash-A'
+    version_added = '2.4'
+
+    def __init__(self, **options):
+        from pygments.lexers.web import HtmlLexer
+        super().__init__(HtmlLexer, SlashLanguageLexer, **options)
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/smalltalk.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/smalltalk.py
new file mode 100644
index 00000000..674b7b4b
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/smalltalk.py
@@ -0,0 +1,194 @@
+"""
+    pygments.lexers.smalltalk
+    ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for Smalltalk and related languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, include, bygroups, default
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation
+
+__all__ = ['SmalltalkLexer', 'NewspeakLexer']
+
+
+class SmalltalkLexer(RegexLexer):
+    """
+    For Smalltalk syntax.
+    Contributed by Stefan Matthias Aust.
+    Rewritten by Nils Winter.
+    """
+    name = 'Smalltalk'
+    url = 'http://www.smalltalk.org/'
+    filenames = ['*.st']
+    aliases = ['smalltalk', 'squeak', 'st']
+    mimetypes = ['text/x-smalltalk']
+    version_added = '0.10'
+
+    tokens = {
+        'root': [
+            (r'(<)(\w+:)(.*?)(>)', bygroups(Text, Keyword, Text, Text)),
+            include('squeak fileout'),
+            include('whitespaces'),
+            include('method definition'),
+            (r'(\|)([\w\s]*)(\|)', bygroups(Operator, Name.Variable, Operator)),
+            include('objects'),
+            (r'\^|\:=|\_', Operator),
+            # temporaries
+            (r'[\]({}.;!]', Text),
+        ],
+        'method definition': [
+            # Not perfect can't allow whitespaces at the beginning and the
+            # without breaking everything
+            (r'([a-zA-Z]+\w*:)(\s*)(\w+)',
+             bygroups(Name.Function, Text, Name.Variable)),
+            (r'^(\b[a-zA-Z]+\w*\b)(\s*)$', bygroups(Name.Function, Text)),
+            (r'^([-+*/\\~<>=|&!?,@%]+)(\s*)(\w+)(\s*)$',
+             bygroups(Name.Function, Text, Name.Variable, Text)),
+        ],
+        'blockvariables': [
+            include('whitespaces'),
+            (r'(:)(\s*)(\w+)',
+             bygroups(Operator, Text, Name.Variable)),
+            (r'\|', Operator, '#pop'),
+            default('#pop'),  # else pop
+        ],
+        'literals': [
+            (r"'(''|[^'])*'", String, 'afterobject'),
+            (r'\$.', String.Char, 'afterobject'),
+            (r'#\(', String.Symbol, 'parenth'),
+            (r'\)', Text, 'afterobject'),
+            (r'(\d+r)?-?\d+(\.\d+)?(e-?\d+)?', Number, 'afterobject'),
+        ],
+        '_parenth_helper': [
+            include('whitespaces'),
+            (r'(\d+r)?-?\d+(\.\d+)?(e-?\d+)?', Number),
+            (r'[-+*/\\~<>=|&#!?,@%\w:]+', String.Symbol),
+            # literals
+            (r"'(''|[^'])*'", String),
+            (r'\$.', String.Char),
+            (r'#*\(', String.Symbol, 'inner_parenth'),
+        ],
+        'parenth': [
+            # This state is a bit tricky since
+            # we can't just pop this state
+            (r'\)', String.Symbol, ('root', 'afterobject')),
+            include('_parenth_helper'),
+        ],
+        'inner_parenth': [
+            (r'\)', String.Symbol, '#pop'),
+            include('_parenth_helper'),
+        ],
+        'whitespaces': [
+            # skip whitespace and comments
+            (r'\s+', Text),
+            (r'"(""|[^"])*"', Comment),
+        ],
+        'objects': [
+            (r'\[', Text, 'blockvariables'),
+            (r'\]', Text, 'afterobject'),
+            (r'\b(self|super|true|false|nil|thisContext)\b',
+             Name.Builtin.Pseudo, 'afterobject'),
+            (r'\b[A-Z]\w*(?!:)\b', Name.Class, 'afterobject'),
+            (r'\b[a-z]\w*(?!:)\b', Name.Variable, 'afterobject'),
+            (r'#("(""|[^"])*"|[-+*/\\~<>=|&!?,@%]+|[\w:]+)',
+             String.Symbol, 'afterobject'),
+            include('literals'),
+        ],
+        'afterobject': [
+            (r'! !$', Keyword, '#pop'),  # squeak chunk delimiter
+            include('whitespaces'),
+            (r'\b(ifTrue:|ifFalse:|whileTrue:|whileFalse:|timesRepeat:)',
+             Name.Builtin, '#pop'),
+            (r'\b(new\b(?!:))', Name.Builtin),
+            (r'\:=|\_', Operator, '#pop'),
+            (r'\b[a-zA-Z]+\w*:', Name.Function, '#pop'),
+            (r'\b[a-zA-Z]+\w*', Name.Function),
+            (r'\w+:?|[-+*/\\~<>=|&!?,@%]+', Name.Function, '#pop'),
+            (r'\.', Punctuation, '#pop'),
+            (r';', Punctuation),
+            (r'[\])}]', Text),
+            (r'[\[({]', Text, '#pop'),
+        ],
+        'squeak fileout': [
+            # Squeak fileout format (optional)
+            (r'^"(""|[^"])*"!', Keyword),
+            (r"^'(''|[^'])*'!", Keyword),
+            (r'^(!)(\w+)( commentStamp: )(.*?)( prior: .*?!\n)(.*?)(!)',
+                bygroups(Keyword, Name.Class, Keyword, String, Keyword, Text, Keyword)),
+            (r"^(!)(\w+(?: class)?)( methodsFor: )('(?:''|[^'])*')(.*?!)",
+                bygroups(Keyword, Name.Class, Keyword, String, Keyword)),
+            (r'^(\w+)( subclass: )(#\w+)'
+             r'(\s+instanceVariableNames: )(.*?)'
+             r'(\s+classVariableNames: )(.*?)'
+             r'(\s+poolDictionaries: )(.*?)'
+             r'(\s+category: )(.*?)(!)',
+                bygroups(Name.Class, Keyword, String.Symbol, Keyword, String, Keyword,
+                         String, Keyword, String, Keyword, String, Keyword)),
+            (r'^(\w+(?: class)?)(\s+instanceVariableNames: )(.*?)(!)',
+                bygroups(Name.Class, Keyword, String, Keyword)),
+            (r'(!\n)(\].*)(! !)$', bygroups(Keyword, Text, Keyword)),
+            (r'! !$', Keyword),
+        ],
+    }
+
+
+class NewspeakLexer(RegexLexer):
+    """
+    For Newspeak syntax.
+    """
+    name = 'Newspeak'
+    url = 'http://newspeaklanguage.org/'
+    filenames = ['*.ns2']
+    aliases = ['newspeak', ]
+    mimetypes = ['text/x-newspeak']
+    version_added = '1.1'
+
+    tokens = {
+        'root': [
+            (r'\b(Newsqueak2)\b', Keyword.Declaration),
+            (r"'[^']*'", String),
+            (r'\b(class)(\s+)(\w+)(\s*)',
+             bygroups(Keyword.Declaration, Text, Name.Class, Text)),
+            (r'\b(mixin|self|super|private|public|protected|nil|true|false)\b',
+             Keyword),
+            (r'(\w+\:)(\s*)([a-zA-Z_]\w+)',
+             bygroups(Name.Function, Text, Name.Variable)),
+            (r'(\w+)(\s*)(=)',
+             bygroups(Name.Attribute, Text, Operator)),
+            (r'<\w+>', Comment.Special),
+            include('expressionstat'),
+            include('whitespace')
+        ],
+
+        'expressionstat': [
+            (r'(\d+\.\d*|\.\d+|\d+[fF])[fF]?', Number.Float),
+            (r'\d+', Number.Integer),
+            (r':\w+', Name.Variable),
+            (r'(\w+)(::)', bygroups(Name.Variable, Operator)),
+            (r'\w+:', Name.Function),
+            (r'\w+', Name.Variable),
+            (r'\(|\)', Punctuation),
+            (r'\[|\]', Punctuation),
+            (r'\{|\}', Punctuation),
+
+            (r'(\^|\+|\/|~|\*|<|>|=|@|%|\||&|\?|!|,|-|:)', Operator),
+            (r'\.|;', Punctuation),
+            include('whitespace'),
+            include('literals'),
+        ],
+        'literals': [
+            (r'\$.', String),
+            (r"'[^']*'", String),
+            (r"#'[^']*'", String.Symbol),
+            (r"#\w+:?", String.Symbol),
+            (r"#(\+|\/|~|\*|<|>|=|@|%|\||&|\?|!|,|-)+", String.Symbol)
+        ],
+        'whitespace': [
+            (r'\s+', Text),
+            (r'"[^"]*"', Comment)
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/smithy.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/smithy.py
new file mode 100644
index 00000000..bd479aec
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/smithy.py
@@ -0,0 +1,77 @@
+"""
+    pygments.lexers.smithy
+    ~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for the Smithy IDL.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, bygroups, words
+from pygments.token import Text, Comment, Keyword, Name, String, \
+    Number, Whitespace, Punctuation
+
+__all__ = ['SmithyLexer']
+
+
+class SmithyLexer(RegexLexer):
+    """
+    For Smithy IDL
+    """
+    name = 'Smithy'
+    url = 'https://awslabs.github.io/smithy/'
+    filenames = ['*.smithy']
+    aliases = ['smithy']
+    version_added = '2.10'
+
+    unquoted = r'[A-Za-z0-9_\.#$-]+'
+    identifier = r"[A-Za-z0-9_\.#$-]+"
+
+    simple_shapes = (
+        'use', 'byte', 'short', 'integer', 'long', 'float', 'document',
+        'double', 'bigInteger', 'bigDecimal', 'boolean', 'blob', 'string',
+        'timestamp',
+    )
+
+    aggregate_shapes = (
+       'apply', 'list', 'map', 'set', 'structure', 'union', 'resource',
+       'operation', 'service', 'trait'
+    )
+
+    tokens = {
+        'root': [
+            (r'///.*$', Comment.Multiline),
+            (r'//.*$', Comment),
+            (r'@[0-9a-zA-Z\.#-]*', Name.Decorator),
+            (r'(=)', Name.Decorator),
+            (r'^(\$version)(:)(.+)',
+                bygroups(Keyword.Declaration, Name.Decorator, Name.Class)),
+            (r'^(namespace)(\s+' + identifier + r')\b',
+                bygroups(Keyword.Declaration, Name.Class)),
+            (words(simple_shapes,
+                   prefix=r'^', suffix=r'(\s+' + identifier + r')\b'),
+                bygroups(Keyword.Declaration, Name.Class)),
+            (words(aggregate_shapes,
+                   prefix=r'^', suffix=r'(\s+' + identifier + r')'),
+                bygroups(Keyword.Declaration, Name.Class)),
+            (r'^(metadata)(\s+)((?:\S+)|(?:\"[^"]+\"))(\s*)(=)',
+                bygroups(Keyword.Declaration, Whitespace, Name.Class,
+                         Whitespace, Name.Decorator)),
+            (r"(true|false|null)", Keyword.Constant),
+            (r"(-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)", Number),
+            (identifier + ":", Name.Label),
+            (identifier, Name.Variable.Class),
+            (r'\[', Text, "#push"),
+            (r'\]', Text, "#pop"),
+            (r'\(', Text, "#push"),
+            (r'\)', Text, "#pop"),
+            (r'\{', Text, "#push"),
+            (r'\}', Text, "#pop"),
+            (r'"{3}(\\\\|\n|\\")*"{3}', String.Doc),
+            (r'"(\\\\|\n|\\"|[^"])*"', String.Double),
+            (r"'(\\\\|\n|\\'|[^'])*'", String.Single),
+            (r'[:,]+', Punctuation),
+            (r'\s+', Whitespace),
+        ]
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/smv.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/smv.py
new file mode 100644
index 00000000..bf97b52a
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/smv.py
@@ -0,0 +1,78 @@
+"""
+    pygments.lexers.smv
+    ~~~~~~~~~~~~~~~~~~~
+
+    Lexers for the SMV languages.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, words
+from pygments.token import Comment, Keyword, Name, Number, Operator, \
+    Punctuation, Text
+
+__all__ = ['NuSMVLexer']
+
+
+class NuSMVLexer(RegexLexer):
+    """
+    Lexer for the NuSMV language.
+    """
+
+    name = 'NuSMV'
+    aliases = ['nusmv']
+    filenames = ['*.smv']
+    mimetypes = []
+    url = 'https://nusmv.fbk.eu'
+    version_added = '2.2'
+
+    tokens = {
+        'root': [
+            # Comments
+            (r'(?s)\/\-\-.*?\-\-/', Comment),
+            (r'--.*\n', Comment),
+
+            # Reserved
+            (words(('MODULE', 'DEFINE', 'MDEFINE', 'CONSTANTS', 'VAR', 'IVAR',
+                    'FROZENVAR', 'INIT', 'TRANS', 'INVAR', 'SPEC', 'CTLSPEC',
+                    'LTLSPEC', 'PSLSPEC', 'COMPUTE', 'NAME', 'INVARSPEC',
+                    'FAIRNESS', 'JUSTICE', 'COMPASSION', 'ISA', 'ASSIGN',
+                    'CONSTRAINT', 'SIMPWFF', 'CTLWFF', 'LTLWFF', 'PSLWFF',
+                    'COMPWFF', 'IN', 'MIN', 'MAX', 'MIRROR', 'PRED',
+                    'PREDICATES'), suffix=r'(?![\w$#-])'),
+             Keyword.Declaration),
+            (r'process(?![\w$#-])', Keyword),
+            (words(('array', 'of', 'boolean', 'integer', 'real', 'word'),
+                   suffix=r'(?![\w$#-])'), Keyword.Type),
+            (words(('case', 'esac'), suffix=r'(?![\w$#-])'), Keyword),
+            (words(('word1', 'bool', 'signed', 'unsigned', 'extend', 'resize',
+                    'sizeof', 'uwconst', 'swconst', 'init', 'self', 'count',
+                    'abs', 'max', 'min'), suffix=r'(?![\w$#-])'),
+             Name.Builtin),
+            (words(('EX', 'AX', 'EF', 'AF', 'EG', 'AG', 'E', 'F', 'O', 'G',
+                    'H', 'X', 'Y', 'Z', 'A', 'U', 'S', 'V', 'T', 'BU', 'EBF',
+                    'ABF', 'EBG', 'ABG', 'next', 'mod', 'union', 'in', 'xor',
+                    'xnor'), suffix=r'(?![\w$#-])'),
+                Operator.Word),
+            (words(('TRUE', 'FALSE'), suffix=r'(?![\w$#-])'), Keyword.Constant),
+
+            # Names
+            (r'[a-zA-Z_][\w$#-]*', Name.Variable),
+
+            # Operators
+            (r':=', Operator),
+            (r'[-&|+*/<>!=]', Operator),
+
+            # Literals
+            (r'\-?\d+\b', Number.Integer),
+            (r'0[su][bB]\d*_[01_]+', Number.Bin),
+            (r'0[su][oO]\d*_[0-7_]+', Number.Oct),
+            (r'0[su][dD]\d*_[\d_]+', Number.Decimal),
+            (r'0[su][hH]\d*_[\da-fA-F_]+', Number.Hex),
+
+            # Whitespace, punctuation and the rest
+            (r'\s+', Text.Whitespace),
+            (r'[()\[\]{};?:.,]', Punctuation),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/snobol.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/snobol.py
new file mode 100644
index 00000000..bab51e9b
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/snobol.py
@@ -0,0 +1,82 @@
+"""
+    pygments.lexers.snobol
+    ~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for the SNOBOL language.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, bygroups
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation
+
+__all__ = ['SnobolLexer']
+
+
+class SnobolLexer(RegexLexer):
+    """
+    Lexer for the SNOBOL4 programming language.
+
+    Recognizes the common ASCII equivalents of the original SNOBOL4 operators.
+    Does not require spaces around binary operators.
+    """
+
+    name = "Snobol"
+    aliases = ["snobol"]
+    filenames = ['*.snobol']
+    mimetypes = ['text/x-snobol']
+    url = 'https://www.regressive.org/snobol4'
+    version_added = '1.5'
+
+    tokens = {
+        # root state, start of line
+        # comments, continuation lines, and directives start in column 1
+        # as do labels
+        'root': [
+            (r'\*.*\n', Comment),
+            (r'[+.] ', Punctuation, 'statement'),
+            (r'-.*\n', Comment),
+            (r'END\s*\n', Name.Label, 'heredoc'),
+            (r'[A-Za-z$][\w$]*', Name.Label, 'statement'),
+            (r'\s+', Text, 'statement'),
+        ],
+        # statement state, line after continuation or label
+        'statement': [
+            (r'\s*\n', Text, '#pop'),
+            (r'\s+', Text),
+            (r'(?<=[^\w.])(LT|LE|EQ|NE|GE|GT|INTEGER|IDENT|DIFFER|LGT|SIZE|'
+             r'REPLACE|TRIM|DUPL|REMDR|DATE|TIME|EVAL|APPLY|OPSYN|LOAD|UNLOAD|'
+             r'LEN|SPAN|BREAK|ANY|NOTANY|TAB|RTAB|REM|POS|RPOS|FAIL|FENCE|'
+             r'ABORT|ARB|ARBNO|BAL|SUCCEED|INPUT|OUTPUT|TERMINAL)(?=[^\w.])',
+             Name.Builtin),
+            (r'[A-Za-z][\w.]*', Name),
+            # ASCII equivalents of original operators
+            # | for the EBCDIC equivalent, ! likewise
+            # \ for EBCDIC negation
+            (r'\*\*|[?$.!%*/#+\-@|&\\=]', Operator),
+            (r'"[^"]*"', String),
+            (r"'[^']*'", String),
+            # Accept SPITBOL syntax for real numbers
+            # as well as Macro SNOBOL4
+            (r'[0-9]+(?=[^.EeDd])', Number.Integer),
+            (r'[0-9]+(\.[0-9]*)?([EDed][-+]?[0-9]+)?', Number.Float),
+            # Goto
+            (r':', Punctuation, 'goto'),
+            (r'[()<>,;]', Punctuation),
+        ],
+        # Goto block
+        'goto': [
+            (r'\s*\n', Text, "#pop:2"),
+            (r'\s+', Text),
+            (r'F|S', Keyword),
+            (r'(\()([A-Za-z][\w.]*)(\))',
+             bygroups(Punctuation, Name.Label, Punctuation))
+        ],
+        # everything after the END statement is basically one
+        # big heredoc.
+        'heredoc': [
+            (r'.*\n', String.Heredoc)
+        ]
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/solidity.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/solidity.py
new file mode 100644
index 00000000..3182a148
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/solidity.py
@@ -0,0 +1,87 @@
+"""
+    pygments.lexers.solidity
+    ~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for Solidity.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, bygroups, include, words
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation, Whitespace
+
+__all__ = ['SolidityLexer']
+
+
+class SolidityLexer(RegexLexer):
+    """
+    For Solidity source code.
+    """
+
+    name = 'Solidity'
+    aliases = ['solidity']
+    filenames = ['*.sol']
+    mimetypes = []
+    url = 'https://soliditylang.org'
+    version_added = '2.5'
+
+    datatype = (
+        r'\b(address|bool|(?:(?:bytes|hash|int|string|uint)(?:8|16|24|32|40|48|56|64'
+        r'|72|80|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208'
+        r'|216|224|232|240|248|256)?))\b'
+    )
+
+    tokens = {
+        'root': [
+            include('whitespace'),
+            include('comments'),
+            (r'\bpragma\s+solidity\b', Keyword, 'pragma'),
+            (r'\b(contract)(\s+)([a-zA-Z_]\w*)',
+             bygroups(Keyword, Whitespace, Name.Entity)),
+            (datatype + r'(\s+)((?:external|public|internal|private)\s+)?' +
+             r'([a-zA-Z_]\w*)',
+             bygroups(Keyword.Type, Whitespace, Keyword, Name.Variable)),
+            (r'\b(enum|event|function|struct)(\s+)([a-zA-Z_]\w*)',
+             bygroups(Keyword.Type, Whitespace, Name.Variable)),
+            (r'\b(msg|block|tx)\.([A-Za-z_][a-zA-Z0-9_]*)\b', Keyword),
+            (words((
+                'block', 'break', 'constant', 'constructor', 'continue',
+                'contract', 'do', 'else', 'external', 'false', 'for',
+                'function', 'if', 'import', 'inherited', 'internal', 'is',
+                'library', 'mapping', 'memory', 'modifier', 'msg', 'new',
+                'payable', 'private', 'public', 'require', 'return',
+                'returns', 'struct', 'suicide', 'throw', 'this', 'true',
+                'tx', 'var', 'while'), prefix=r'\b', suffix=r'\b'),
+             Keyword.Type),
+            (words(('keccak256',), prefix=r'\b', suffix=r'\b'), Name.Builtin),
+            (datatype, Keyword.Type),
+            include('constants'),
+            (r'[a-zA-Z_]\w*', Text),
+            (r'[~!%^&*+=|?:<>/-]', Operator),
+            (r'[.;{}(),\[\]]', Punctuation)
+        ],
+        'comments': [
+            (r'//(\n|[\w\W]*?[^\\]\n)', Comment.Single),
+            (r'/(\\\n)?[*][\w\W]*?[*](\\\n)?/', Comment.Multiline),
+            (r'/(\\\n)?[*][\w\W]*', Comment.Multiline)
+        ],
+        'constants': [
+            (r'("(\\"|.)*?")', String.Double),
+            (r"('(\\'|.)*?')", String.Single),
+            (r'\b0[xX][0-9a-fA-F]+\b', Number.Hex),
+            (r'\b\d+\b', Number.Decimal),
+        ],
+        'pragma': [
+            include('whitespace'),
+            include('comments'),
+            (r'(\^|>=|<)(\s*)(\d+\.\d+\.\d+)',
+             bygroups(Operator, Whitespace, Keyword)),
+            (r';', Punctuation, '#pop')
+        ],
+        'whitespace': [
+            (r'\s+', Whitespace),
+            (r'\n', Whitespace)
+        ]
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/soong.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/soong.py
new file mode 100644
index 00000000..bbf204dd
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/soong.py
@@ -0,0 +1,78 @@
+"""
+    pygments.lexers.soong
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for Soong (Android.bp Blueprint) files.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, bygroups, include
+from pygments.token import Comment, Name, Number, Operator, Punctuation, \
+        String, Whitespace
+
+__all__ = ['SoongLexer']
+
+class SoongLexer(RegexLexer):
+    name = 'Soong'
+    version_added = '2.18'
+    url = 'https://source.android.com/docs/setup/reference/androidbp'
+    aliases = ['androidbp', 'bp', 'soong']
+    filenames = ['Android.bp']
+
+    tokens = {
+        'root': [
+            # A variable assignment
+            (r'(\w*)(\s*)(\+?=)(\s*)',
+             bygroups(Name.Variable, Whitespace, Operator, Whitespace),
+             'assign-rhs'),
+
+            # A top-level module
+            (r'(\w*)(\s*)(\{)',
+             bygroups(Name.Function, Whitespace, Punctuation),
+             'in-rule'),
+
+            # Everything else
+            include('comments'),
+            (r'\s+', Whitespace),  # newlines okay
+        ],
+        'assign-rhs': [
+            include('expr'),
+            (r'\n', Whitespace, '#pop'),
+        ],
+        'in-list': [
+            include('expr'),
+            include('comments'),
+            (r'\s+', Whitespace),  # newlines okay in a list
+            (r',', Punctuation),
+            (r'\]', Punctuation, '#pop'),
+        ],
+        'in-map': [
+            # A map key
+            (r'(\w+)(:)(\s*)', bygroups(Name, Punctuation, Whitespace)),
+
+            include('expr'),
+            include('comments'),
+            (r'\s+', Whitespace),  # newlines okay in a map
+            (r',', Punctuation),
+            (r'\}', Punctuation, '#pop'),
+        ],
+        'in-rule': [
+            # Just re-use map syntax
+            include('in-map'),
+        ],
+        'comments': [
+            (r'//.*', Comment.Single),
+            (r'/(\\\n)?[*](.|\n)*?[*](\\\n)?/', Comment.Multiline),
+        ],
+        'expr': [
+            (r'(true|false)\b', Name.Builtin),
+            (r'0x[0-9a-fA-F]+', Number.Hex),
+            (r'\d+', Number.Integer),
+            (r'".*?"', String),
+            (r'\{', Punctuation, 'in-map'),
+            (r'\[', Punctuation, 'in-list'),
+            (r'\w+', Name),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/sophia.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/sophia.py
new file mode 100644
index 00000000..37fcec5c
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/sophia.py
@@ -0,0 +1,102 @@
+"""
+    pygments.lexers.sophia
+    ~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexer for Sophia.
+
+    Derived from pygments/lexers/reason.py.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, include, default, words
+from pygments.token import Comment, Keyword, Name, Number, Operator, \
+    Punctuation, String, Text
+
+__all__ = ['SophiaLexer']
+
+class SophiaLexer(RegexLexer):
+    """
+    A Sophia lexer.
+    """
+
+    name = 'Sophia'
+    aliases = ['sophia']
+    filenames = ['*.aes']
+    mimetypes = []
+    url = 'https://docs.aeternity.com/aesophia'
+    version_added = '2.11'
+
+    keywords = (
+        'contract', 'include', 'let', 'switch', 'type', 'record', 'datatype',
+        'if', 'elif', 'else', 'function', 'stateful', 'payable', 'public',
+        'entrypoint', 'private', 'indexed', 'namespace', 'interface', 'main',
+        'using', 'as', 'for', 'hiding',
+    )
+
+    builtins = ('state', 'put', 'abort', 'require')
+
+    word_operators = ('mod', 'band', 'bor', 'bxor', 'bnot')
+
+    primitive_types = ('int', 'address', 'bool', 'bits', 'bytes', 'string',
+                       'list', 'option', 'char', 'unit', 'map', 'event',
+                       'hash', 'signature', 'oracle', 'oracle_query')
+
+    tokens = {
+        'escape-sequence': [
+            (r'\\[\\"\'ntbr]', String.Escape),
+            (r'\\[0-9]{3}', String.Escape),
+            (r'\\x[0-9a-fA-F]{2}', String.Escape),
+        ],
+        'root': [
+            (r'\s+', Text.Whitespace),
+            (r'(true|false)\b', Keyword.Constant),
+            (r'\b([A-Z][\w\']*)(?=\s*\.)', Name.Class, 'dotted'),
+            (r'\b([A-Z][\w\']*)', Name.Function),
+            (r'//.*?\n', Comment.Single),
+            (r'\/\*(?!/)', Comment.Multiline, 'comment'),
+
+            (r'0[xX][\da-fA-F][\da-fA-F_]*', Number.Hex),
+            (r'#[\da-fA-F][\da-fA-F_]*', Name.Label),
+            (r'\d[\d_]*', Number.Integer),
+
+            (words(keywords, suffix=r'\b'), Keyword),
+            (words(builtins, suffix=r'\b'), Name.Builtin),
+            (words(word_operators, prefix=r'\b', suffix=r'\b'), Operator.Word),
+            (words(primitive_types, prefix=r'\b', suffix=r'\b'), Keyword.Type),
+
+            (r'[=!<>+\\*/:&|?~@^-]', Operator.Word),
+            (r'[.;:{}(),\[\]]', Punctuation),
+
+            (r"(ak_|ok_|oq_|ct_)[\w']*", Name.Label),
+            (r"[^\W\d][\w']*", Name),
+
+            (r"'(?:(\\[\\\"'ntbr ])|(\\[0-9]{3})|(\\x[0-9a-fA-F]{2}))'",
+             String.Char),
+            (r"'.'", String.Char),
+            (r"'[a-z][\w]*", Name.Variable),
+
+            (r'"', String.Double, 'string')
+        ],
+        'comment': [
+            (r'[^/*]+', Comment.Multiline),
+            (r'\/\*', Comment.Multiline, '#push'),
+            (r'\*\/', Comment.Multiline, '#pop'),
+            (r'\*', Comment.Multiline),
+        ],
+        'string': [
+            (r'[^\\"]+', String.Double),
+            include('escape-sequence'),
+            (r'\\\n', String.Double),
+            (r'"', String.Double, '#pop'),
+        ],
+        'dotted': [
+            (r'\s+', Text),
+            (r'\.', Punctuation),
+            (r'[A-Z][\w\']*(?=\s*\.)', Name.Function),
+            (r'[A-Z][\w\']*', Name.Function, '#pop'),
+            (r'[a-z_][\w\']*', Name, '#pop'),
+            default('#pop'),
+        ],
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/special.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/special.py
new file mode 100644
index 00000000..524946fc
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/special.py
@@ -0,0 +1,122 @@
+"""
+    pygments.lexers.special
+    ~~~~~~~~~~~~~~~~~~~~~~~
+
+    Special lexers.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import ast
+
+from pygments.lexer import Lexer, line_re
+from pygments.token import Token, Error, Text, Generic
+from pygments.util import get_choice_opt
+
+
+__all__ = ['TextLexer', 'OutputLexer', 'RawTokenLexer']
+
+
+class TextLexer(Lexer):
+    """
+    "Null" lexer, doesn't highlight anything.
+    """
+    name = 'Text only'
+    aliases = ['text']
+    filenames = ['*.txt']
+    mimetypes = ['text/plain']
+    url = ""
+    version_added = ''
+
+    priority = 0.01
+
+    def get_tokens_unprocessed(self, text):
+        yield 0, Text, text
+
+    def analyse_text(text):
+        return TextLexer.priority
+
+
+class OutputLexer(Lexer):
+    """
+    Simple lexer that highlights everything as ``Token.Generic.Output``.
+    """
+    name = 'Text output'
+    aliases = ['output']
+    url = ""
+    version_added = '2.10'
+    _example = "output/output"
+
+    def get_tokens_unprocessed(self, text):
+        yield 0, Generic.Output, text
+
+
+_ttype_cache = {}
+
+
+class RawTokenLexer(Lexer):
+    """
+    Recreate a token stream formatted with the `RawTokenFormatter`.
+
+    Additional options accepted:
+
+    `compress`
+        If set to ``"gz"`` or ``"bz2"``, decompress the token stream with
+        the given compression algorithm before lexing (default: ``""``).
+    """
+    name = 'Raw token data'
+    aliases = []
+    filenames = []
+    mimetypes = ['application/x-pygments-tokens']
+    url = 'https://pygments.org/docs/formatters/#RawTokenFormatter'
+    version_added = ''
+
+    def __init__(self, **options):
+        self.compress = get_choice_opt(options, 'compress',
+                                       ['', 'none', 'gz', 'bz2'], '')
+        Lexer.__init__(self, **options)
+
+    def get_tokens(self, text):
+        if self.compress:
+            if isinstance(text, str):
+                text = text.encode('latin1')
+            try:
+                if self.compress == 'gz':
+                    import gzip
+                    text = gzip.decompress(text)
+                elif self.compress == 'bz2':
+                    import bz2
+                    text = bz2.decompress(text)
+            except OSError:
+                yield Error, text.decode('latin1')
+        if isinstance(text, bytes):
+            text = text.decode('latin1')
+
+        # do not call Lexer.get_tokens() because stripping is not optional.
+        text = text.strip('\n') + '\n'
+        for i, t, v in self.get_tokens_unprocessed(text):
+            yield t, v
+
+    def get_tokens_unprocessed(self, text):
+        length = 0
+        for match in line_re.finditer(text):
+            try:
+                ttypestr, val = match.group().rstrip().split('\t', 1)
+                ttype = _ttype_cache.get(ttypestr)
+                if not ttype:
+                    ttype = Token
+                    ttypes = ttypestr.split('.')[1:]
+                    for ttype_ in ttypes:
+                        if not ttype_ or not ttype_[0].isupper():
+                            raise ValueError('malformed token name')
+                        ttype = getattr(ttype, ttype_)
+                    _ttype_cache[ttypestr] = ttype
+                val = ast.literal_eval(val)
+                if not isinstance(val, str):
+                    raise ValueError('expected str')
+            except (SyntaxError, ValueError):
+                val = match.group()
+                ttype = Error
+            yield length, ttype, val
+            length += len(val)
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/spice.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/spice.py
new file mode 100644
index 00000000..9d2b1a1a
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/spice.py
@@ -0,0 +1,70 @@
+"""
+    pygments.lexers.spice
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for the Spice programming language.
+
+    :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, bygroups, words
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation, Whitespace
+
+__all__ = ['SpiceLexer']
+
+
+class SpiceLexer(RegexLexer):
+    """
+    For Spice source.
+    """
+    name = 'Spice'
+    url = 'https://www.spicelang.com'
+    filenames = ['*.spice']
+    aliases = ['spice', 'spicelang']
+    mimetypes = ['text/x-spice']
+    version_added = '2.11'
+
+    tokens = {
+        'root': [
+            (r'\n', Whitespace),
+            (r'\s+', Whitespace),
+            (r'\\\n', Text),
+            # comments
+            (r'//(.*?)\n', Comment.Single),
+            (r'/(\\\n)?[*]{2}(.|\n)*?[*](\\\n)?/', String.Doc),
+            (r'/(\\\n)?[*](.|\n)*?[*](\\\n)?/', Comment.Multiline),
+            # keywords
+            (r'(import|as)\b', Keyword.Namespace),
+            (r'(f|p|type|struct|interface|enum|alias|operator)\b', Keyword.Declaration),
+            (words(('if', 'else', 'switch', 'case', 'default', 'for', 'foreach', 'do',
+                    'while', 'break', 'continue', 'fallthrough', 'return', 'assert',
+                    'unsafe', 'ext'), suffix=r'\b'), Keyword),
+            (words(('const', 'signed', 'unsigned', 'inline', 'public', 'heap', 'compose'),
+                   suffix=r'\b'), Keyword.Pseudo),
+            (words(('new', 'yield', 'stash', 'pick', 'sync', 'class'), suffix=r'\b'),
+                   Keyword.Reserved),
+            (r'(true|false|nil)\b', Keyword.Constant),
+            (words(('double', 'int', 'short', 'long', 'byte', 'char', 'string',
+                    'bool', 'dyn'), suffix=r'\b'), Keyword.Type),
+            (words(('printf', 'sizeof', 'alignof', 'len', 'panic'), suffix=r'\b(\()'),
+             bygroups(Name.Builtin, Punctuation)),
+            # numeric literals
+            (r'[-]?[0-9]*[.][0-9]+([eE][+-]?[0-9]+)?', Number.Double),
+            (r'0[bB][01]+[slu]?', Number.Bin),
+            (r'0[oO][0-7]+[slu]?', Number.Oct),
+            (r'0[xXhH][0-9a-fA-F]+[slu]?', Number.Hex),
+            (r'(0[dD])?[0-9]+[slu]?', Number.Integer),
+            # string literal
+            (r'"(\\\\|\\[^\\]|[^"\\])*"', String),
+            # char literal
+            (r'\'(\\\\|\\[^\\]|[^\'\\])\'', String.Char),
+            # tokens
+            (r'<<=|>>=|<<|>>|<=|>=|\+=|-=|\*=|/=|\%=|\|=|&=|\^=|&&|\|\||&|\||'
+             r'\+\+|--|\%|\^|\~|==|!=|->|::|[.]{3}|#!|#|[+\-*/&]', Operator),
+            (r'[|<>=!()\[\]{}.,;:\?]', Punctuation),
+            # identifiers
+            (r'[^\W\d]\w*', Name.Other),
+        ]
+    }
diff --git a/Backend/venv/lib/python3.12/site-packages/pygments/lexers/sql.py b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/sql.py
new file mode 100644
index 00000000..d3e6f17f
--- /dev/null
+++ b/Backend/venv/lib/python3.12/site-packages/pygments/lexers/sql.py
@@ -0,0 +1,1109 @@
+"""
+    pygments.lexers.sql
+    ~~~~~~~~~~~~~~~~~~~
+
+    Lexers for various SQL dialects and related interactive sessions.
+
+    Postgres specific lexers:
+
+    `PostgresLexer`
+        A SQL lexer for the PostgreSQL dialect. Differences w.r.t. the SQL
+        lexer are:
+
+        - keywords and data types list parsed from the PG docs (run the
+          `_postgres_builtins` module to update them);
+        - Content of $-strings parsed using a specific lexer, e.g. the content
+          of a PL/Python function is parsed using the Python lexer;
+        - parse PG specific constructs: E-strings, $-strings, U&-strings,
+          different operators and punctuation.
+
+    `PlPgsqlLexer`
+        A lexer for the PL/pgSQL language. Adds a few specific construct on
+        top of the PG SQL lexer (such as <
+
+
+

Custom Report Builder

+

Select metrics and generate custom analytics reports

+
+ {onClose && ( + + )} +
+ + {/* Report Name */} +
+ + setReportName(e.target.value)} + placeholder="e.g., Monthly Revenue Report" + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent" + /> +
+ + {/* Date Range */} +
+ +
+ setDateRange({ ...dateRange, from: e.target.value })} + className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500" + /> + to + setDateRange({ ...dateRange, to: e.target.value })} + className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500" + /> +
+
+ + {/* Category Filter */} +
+ +
+ {categories.map((category) => ( + + ))} +
+
+ + {/* Metrics Selection */} +
+
+ +
+ + +
+
+
+
+ {filteredMetrics.map((metric) => { + const isSelected = selectedMetrics.includes(metric.id); + return ( + + ); + })} +
+
+
+ + {/* Actions */} +
+ + + +
+ + {loading && ( +
+ Generating report... +
+ )} +
+ ); +}; + +export default CustomReportBuilder; + diff --git a/Frontend/src/components/analytics/SimpleChart.tsx b/Frontend/src/components/analytics/SimpleChart.tsx new file mode 100644 index 00000000..f6ce07ff --- /dev/null +++ b/Frontend/src/components/analytics/SimpleChart.tsx @@ -0,0 +1,224 @@ +import React from 'react'; +import { TrendingUp } from 'lucide-react'; + +// Simple chart components using CSS/SVG (no external library dependency) +// These provide basic visualizations without requiring recharts + +interface SimpleBarChartProps { + data: Array<{ label: string; value: number; color?: string }>; + height?: number; + title?: string; +} + +export const SimpleBarChart: React.FC = ({ data, height = 200, title }) => { + const maxValue = Math.max(...data.map(d => d.value), 1); + + return ( +
+ {title &&

{title}

} +
+ {data.map((item, index) => ( +
+
{item.label}
+
+
+ + {typeof item.value === 'number' ? item.value.toLocaleString() : item.value} + +
+
+ ))} +
+
+ ); +}; + +interface SimpleLineChartProps { + data: Array<{ label: string; value: number }>; + height?: number; + title?: string; + color?: string; +} + +export const SimpleLineChart: React.FC = ({ data, height = 200, title, color = '#3b82f6' }) => { + if (!data || data.length === 0) return null; + + const maxValue = Math.max(...data.map(d => d.value), 1); + const minValue = Math.min(...data.map(d => d.value), 0); + const range = maxValue - minValue || 1; + + const points = data.map((item, index) => { + const x = (index / (data.length - 1 || 1)) * 100; + const y = 100 - ((item.value - minValue) / range) * 100; + return `${x},${y}`; + }).join(' '); + + return ( +
+ {title &&

{title}

} +
+ + + {data.map((item, index) => { + const x = (index / (data.length - 1 || 1)) * 100; + const y = 100 - ((item.value - minValue) / range) * 100; + return ( + + ); + })} + +
+ {data.map((item, index) => ( + + {item.label} + + ))} +
+
+
+ ); +}; + +interface SimplePieChartProps { + data: Array<{ label: string; value: number; color?: string }>; + size?: number; + title?: string; +} + +export const SimplePieChart: React.FC = ({ data, size = 200, title }) => { + const total = data.reduce((sum, item) => sum + item.value, 0); + if (total === 0) return null; + + const colors = [ + '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', + '#ec4899', '#06b6d4', '#84cc16', '#f97316', '#6366f1' + ]; + + let currentAngle = -90; + const segments = data.map((item, index) => { + const percentage = (item.value / total) * 100; + const angle = (percentage / 100) * 360; + const startAngle = currentAngle; + currentAngle += angle; + + const x1 = 50 + 50 * Math.cos((startAngle * Math.PI) / 180); + const y1 = 50 + 50 * Math.sin((startAngle * Math.PI) / 180); + const x2 = 50 + 50 * Math.cos((currentAngle * Math.PI) / 180); + const y2 = 50 + 50 * Math.sin((currentAngle * Math.PI) / 180); + const largeArc = angle > 180 ? 1 : 0; + + return { + path: `M 50 50 L ${x1} ${y1} A 50 50 0 ${largeArc} 1 ${x2} ${y2} Z`, + color: item.color || colors[index % colors.length], + label: item.label, + value: item.value, + percentage: percentage.toFixed(1) + }; + }); + + return ( +
+ {title &&

{title}

} +
+
+ + {segments.map((segment, index) => ( + + ))} + +
+
+ {segments.map((segment, index) => ( +
+
+ {segment.label} + + {segment.percentage}% + +
+ ))} +
+
+
+ ); +}; + +interface KPICardProps { + title: string; + value: string | number; + change?: number; + icon?: React.ReactNode; + color?: string; + subtitle?: string; +} + +export const KPICard: React.FC = ({ + title, + value, + change, + icon, + color = 'blue', + subtitle +}) => { + const colorClasses = { + blue: 'from-blue-500 to-indigo-600', + green: 'from-emerald-500 to-green-600', + orange: 'from-orange-500 to-amber-600', + purple: 'from-purple-500 to-indigo-600', + red: 'from-rose-500 to-red-600', + }; + + return ( +
+
+
+

{title}

+

+ {typeof value === 'number' ? value.toLocaleString() : value} +

+ {subtitle &&

{subtitle}

} +
+ {icon && ( +
+ {icon} +
+ )} +
+ {change !== undefined && ( +
= 0 ? 'text-emerald-600' : 'text-rose-600' + }`}> + + {Math.abs(change).toFixed(1)}% + vs previous period +
+ )} +
+ ); +}; + diff --git a/Frontend/src/components/auth/AccountantRoute.tsx b/Frontend/src/components/auth/AccountantRoute.tsx index 52afa7d2..5317983f 100644 --- a/Frontend/src/components/auth/AccountantRoute.tsx +++ b/Frontend/src/components/auth/AccountantRoute.tsx @@ -47,6 +47,12 @@ const AccountantRoute: React.FC = ({ // Check if user is accountant const isAccountant = userInfo?.role === 'accountant'; if (!isAccountant) { + // Redirect to appropriate dashboard based on role + if (userInfo?.role === 'admin') { + return ; + } else if (userInfo?.role === 'staff') { + return ; + } return ; } diff --git a/Frontend/src/components/auth/AdminRoute.tsx b/Frontend/src/components/auth/AdminRoute.tsx index 66a05696..3df428e1 100644 --- a/Frontend/src/components/auth/AdminRoute.tsx +++ b/Frontend/src/components/auth/AdminRoute.tsx @@ -47,6 +47,12 @@ const AdminRoute: React.FC = ({ const isAdmin = userInfo?.role === 'admin'; if (!isAdmin) { + // Redirect to appropriate dashboard based on role + if (userInfo?.role === 'staff') { + return ; + } else if (userInfo?.role === 'accountant') { + return ; + } return ; } diff --git a/Frontend/src/components/ResetPasswordRouteHandler.tsx b/Frontend/src/components/auth/ResetPasswordRouteHandler.tsx similarity index 88% rename from Frontend/src/components/ResetPasswordRouteHandler.tsx rename to Frontend/src/components/auth/ResetPasswordRouteHandler.tsx index 78fcc259..505c087a 100644 --- a/Frontend/src/components/ResetPasswordRouteHandler.tsx +++ b/Frontend/src/components/auth/ResetPasswordRouteHandler.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { useAuthModal } from '../contexts/AuthModalContext'; +import { useAuthModal } from '../../contexts/AuthModalContext'; const ResetPasswordRouteHandler: React.FC = () => { const { token } = useParams<{ token: string }>(); diff --git a/Frontend/src/components/auth/StaffRoute.tsx b/Frontend/src/components/auth/StaffRoute.tsx index 7f8eef5b..04381ec6 100644 --- a/Frontend/src/components/auth/StaffRoute.tsx +++ b/Frontend/src/components/auth/StaffRoute.tsx @@ -47,6 +47,12 @@ const StaffRoute: React.FC = ({ const isStaff = userInfo?.role === 'staff'; if (!isStaff) { + // Redirect to appropriate dashboard based on role + if (userInfo?.role === 'admin') { + return ; + } else if (userInfo?.role === 'accountant') { + return ; + } return ; } diff --git a/Frontend/src/components/auth/index.ts b/Frontend/src/components/auth/index.ts index 262c8385..df5bec05 100644 --- a/Frontend/src/components/auth/index.ts +++ b/Frontend/src/components/auth/index.ts @@ -3,3 +3,4 @@ export { default as AdminRoute } from './AdminRoute'; export { default as StaffRoute } from './StaffRoute'; export { default as AccountantRoute } from './AccountantRoute'; export { default as CustomerRoute } from './CustomerRoute'; +export { default as ResetPasswordRouteHandler } from './ResetPasswordRouteHandler'; diff --git a/Frontend/src/components/chat/StaffChatNotification.tsx b/Frontend/src/components/chat/StaffChatNotification.tsx index bcd353b8..d7748195 100644 --- a/Frontend/src/components/chat/StaffChatNotification.tsx +++ b/Frontend/src/components/chat/StaffChatNotification.tsx @@ -92,6 +92,16 @@ const StaffChatNotification: React.FC = () => { }, autoClose: 10000 }); + } else if (data.type === 'housekeeping_task_assigned') { + const taskData = data.data; + const taskTypeLabel = taskData.task_type ? taskData.task_type.charAt(0).toUpperCase() + taskData.task_type.slice(1) : 'Housekeeping'; + + toast.success(`New ${taskTypeLabel} task assigned: Room ${taskData.room_number}`, { + onClick: () => { + navigate('/staff/advanced-rooms?tab=housekeeping'); + }, + autoClose: 10000 + }); } } catch (error) { console.error('Error parsing notification:', error); diff --git a/Frontend/src/components/common/PaymentMethodSelector.tsx b/Frontend/src/components/common/PaymentMethodSelector.tsx index d5ae7460..d0b27967 100644 --- a/Frontend/src/components/common/PaymentMethodSelector.tsx +++ b/Frontend/src/components/common/PaymentMethodSelector.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { CreditCard } from 'lucide-react'; interface PaymentMethodSelectorProps { - value: 'cash' | 'stripe'; - onChange: (value: 'cash' | 'stripe') => void; + value: 'cash' | 'stripe' | 'borica' | 'paypal'; + onChange: (value: 'cash' | 'stripe' | 'borica' | 'paypal') => void; error?: string; disabled?: boolean; } @@ -110,6 +110,55 @@ const PaymentMethodSelector: React.FC<
+ + {/* Borica Payment */} +
{error && ( @@ -126,7 +175,11 @@ const PaymentMethodSelector: React.FC< 💡 Note: {' '} {value === 'cash' ? 'You will pay when checking in. Cash and card accepted at the hotel.' - : 'Your payment will be processed securely through Stripe.'} + : value === 'stripe' + ? 'Your payment will be processed securely through Stripe.' + : value === 'borica' + ? 'Your payment will be processed securely through Borica payment gateway.' + : 'Your payment will be processed securely.'}

diff --git a/Frontend/src/components/common/Preloader.tsx b/Frontend/src/components/common/Preloader.tsx new file mode 100644 index 00000000..262d639a --- /dev/null +++ b/Frontend/src/components/common/Preloader.tsx @@ -0,0 +1,87 @@ +import React, { useState } from 'react'; +import { useCompanySettings } from '../../contexts/CompanySettingsContext'; +import { Loader2 } from 'lucide-react'; + +interface PreloaderProps { + isLoading?: boolean; +} + +const Preloader: React.FC = ({ isLoading = true }) => { + const { settings } = useCompanySettings(); + const [logoError, setLogoError] = useState(false); + const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000'; + + // Get logo URL - handle both absolute and relative URLs + const logoUrl = settings.company_logo_url && !logoError + ? settings.company_logo_url.startsWith('http') + ? settings.company_logo_url + : `${baseUrl}${settings.company_logo_url}` + : null; + + if (!isLoading) return null; + + return ( +
+
+
+ {/* Animated background glow */} +
+ + {/* Main container */} +
+ {/* Logo or fallback */} + {logoUrl && !logoError ? ( +
+ {settings.company_name setLogoError(true)} + /> +
+ ) : ( +
+ +
+ )} + + {/* Loading bar */} +
+
+
+
+
+ + {/* Loading text */} +

+ {settings.company_name ? `Loading ${settings.company_name}...` : 'Loading...'} +

+
+ + {/* CSS for shimmer animation */} + +
+ ); +}; + +export default Preloader; + diff --git a/Frontend/src/components/layout/Header.tsx b/Frontend/src/components/layout/Header.tsx index 9bfbb189..3d03e129 100644 --- a/Frontend/src/components/layout/Header.tsx +++ b/Frontend/src/components/layout/Header.tsx @@ -13,11 +13,13 @@ import { Mail, Calendar, Star, + Users, } from 'lucide-react'; import { useClickOutside } from '../../hooks/useClickOutside'; import { useCompanySettings } from '../../contexts/CompanySettingsContext'; import { useAuthModal } from '../../contexts/AuthModalContext'; import { normalizeImageUrl } from '../../utils/imageUtils'; +import InAppNotificationBell from '../notifications/InAppNotificationBell'; interface HeaderProps { isAuthenticated?: boolean; @@ -76,7 +78,6 @@ const Header: React.FC = ({ return (
- {}
@@ -96,10 +97,8 @@ const Header: React.FC = ({
- {}
- {} - + + {userInfo?.name?.charAt(0) + .toUpperCase()} + +
+ )} + + {userInfo?.name} + + - {} - {isUserMenuOpen && ( + {isUserMenuOpen && (
Loyalty Program + setIsUserMenuOpen(false)} + className="flex items-center space-x-3 + px-4 py-2.5 text-white/90 + hover:bg-[#d4af37]/10 hover:text-[#d4af37] + transition-all duration-300 border-l-2 border-transparent + hover:border-[#d4af37]" + > + + Group Bookings + )} {userInfo?.role === 'admin' && ( @@ -356,79 +368,74 @@ const Header: React.FC = ({ Logout
- )} + )} +
)} + +
- {} - - - - {} - {isMobileMenuOpen && ( -
-
- setIsMobileMenuOpen(false)} - className="px-4 py-3 text-white/90 - hover:bg-[#d4af37]/10 hover:text-[#d4af37] - rounded-sm transition-all duration-300 - border-l-2 border-transparent - hover:border-[#d4af37] font-light tracking-wide" - > - Home - - setIsMobileMenuOpen(false)} - className="px-4 py-3 text-white/90 - hover:bg-[#d4af37]/10 hover:text-[#d4af37] - rounded-sm transition-all duration-300 - border-l-2 border-transparent - hover:border-[#d4af37] font-light tracking-wide" - > - Rooms - - setIsMobileMenuOpen(false)} - className="px-4 py-3 text-white/90 - hover:bg-[#d4af37]/10 hover:text-[#d4af37] - rounded-sm transition-all duration-300 - border-l-2 border-transparent - hover:border-[#d4af37] font-light tracking-wide" - > - About - - setIsMobileMenuOpen(false)} - className="px-4 py-3 text-white/90 - hover:bg-[#d4af37]/10 hover:text-[#d4af37] - rounded-sm transition-all duration-300 - border-l-2 border-transparent - hover:border-[#d4af37] font-light tracking-wide" - > - Contact - - -
+
+ setIsMobileMenuOpen(false)} + className="px-4 py-3 text-white/90 + hover:bg-[#d4af37]/10 hover:text-[#d4af37] + rounded-sm transition-all duration-300 + border-l-2 border-transparent + hover:border-[#d4af37] font-light tracking-wide" + > + Home + + setIsMobileMenuOpen(false)} + className="px-4 py-3 text-white/90 + hover:bg-[#d4af37]/10 hover:text-[#d4af37] + rounded-sm transition-all duration-300 + border-l-2 border-transparent + hover:border-[#d4af37] font-light tracking-wide" + > + Rooms + + setIsMobileMenuOpen(false)} + className="px-4 py-3 text-white/90 + hover:bg-[#d4af37]/10 hover:text-[#d4af37] + rounded-sm transition-all duration-300 + border-l-2 border-transparent + hover:border-[#d4af37] font-light tracking-wide" + > + About + + setIsMobileMenuOpen(false)} + className="px-4 py-3 text-white/90 + hover:bg-[#d4af37]/10 hover:text-[#d4af37] + rounded-sm transition-all duration-300 + border-l-2 border-transparent + hover:border-[#d4af37] font-light tracking-wide" + > + Contact + + +
{!isAuthenticated ? ( @@ -570,10 +577,11 @@ const Header: React.FC = ({ )} +
-
- )} + )} +
); diff --git a/Frontend/src/components/layout/SidebarAccountant.tsx b/Frontend/src/components/layout/SidebarAccountant.tsx index 55c9c64f..f8fa9540 100644 --- a/Frontend/src/components/layout/SidebarAccountant.tsx +++ b/Frontend/src/components/layout/SidebarAccountant.tsx @@ -13,6 +13,7 @@ import { Receipt } from 'lucide-react'; import useAuthStore from '../../store/useAuthStore'; +import { useResponsive } from '../../hooks'; interface SidebarAccountantProps { isCollapsed?: boolean; @@ -24,11 +25,11 @@ const SidebarAccountant: React.FC = ({ onToggle }) => { const [internalCollapsed, setInternalCollapsed] = useState(false); - const [isMobile, setIsMobile] = useState(false); const [isMobileOpen, setIsMobileOpen] = useState(false); const location = useLocation(); const navigate = useNavigate(); const { logout } = useAuthStore(); + const { isMobile, isTablet, isDesktop } = useResponsive(); const handleLogout = async () => { try { @@ -42,19 +43,12 @@ const SidebarAccountant: React.FC = ({ } }; - // Handle mobile responsiveness + // Close mobile menu when screen becomes desktop useEffect(() => { - const checkMobile = () => { - setIsMobile(window.innerWidth < 1024); - if (window.innerWidth >= 1024) { - setIsMobileOpen(false); - } - }; - - checkMobile(); - window.addEventListener('resize', checkMobile); - return () => window.removeEventListener('resize', checkMobile); - }, []); + if (isDesktop) { + setIsMobileOpen(false); + } + }, [isDesktop]); const isCollapsed = controlledCollapsed !== undefined diff --git a/Frontend/src/components/layout/SidebarAdmin.tsx b/Frontend/src/components/layout/SidebarAdmin.tsx index ebdc9bf7..cafc7646 100644 --- a/Frontend/src/components/layout/SidebarAdmin.tsx +++ b/Frontend/src/components/layout/SidebarAdmin.tsx @@ -14,25 +14,48 @@ import { Menu, X, Award, - User + User, + Workflow, + CheckSquare, + Bell, + UserCheck, + Hotel, + Tag, + Package, + Shield, + Mail, + TrendingUp, + Building2, + Crown } from 'lucide-react'; import useAuthStore from '../../store/useAuthStore'; +import { useResponsive } from '../../hooks'; interface SidebarAdminProps { isCollapsed?: boolean; onToggle?: () => void; } +interface MenuGroup { + title: string; + icon?: React.ComponentType<{ className?: string }>; + items: Array<{ + path: string; + icon: React.ComponentType<{ className?: string }>; + label: string; + }>; +} + const SidebarAdmin: React.FC = ({ isCollapsed: controlledCollapsed, onToggle }) => { const [internalCollapsed, setInternalCollapsed] = useState(false); - const [isMobile, setIsMobile] = useState(false); const [isMobileOpen, setIsMobileOpen] = useState(false); const location = useLocation(); const navigate = useNavigate(); const { logout } = useAuthStore(); + const { isMobile, isTablet, isDesktop } = useResponsive(); const handleLogout = async () => { try { @@ -46,19 +69,12 @@ const SidebarAdmin: React.FC = ({ } }; - + // Close mobile menu when screen becomes desktop useEffect(() => { - const checkMobile = () => { - setIsMobile(window.innerWidth < 1024); - if (window.innerWidth >= 1024) { - setIsMobileOpen(false); - } - }; - - checkMobile(); - window.addEventListener('resize', checkMobile); - return () => window.removeEventListener('resize', checkMobile); - }, []); + if (isDesktop) { + setIsMobileOpen(false); + } + }, [isDesktop]); const isCollapsed = controlledCollapsed !== undefined @@ -83,63 +99,161 @@ const SidebarAdmin: React.FC = ({ } }; - const menuItems = [ - { - path: '/admin/dashboard', - icon: LayoutDashboard, - label: 'Dashboard' + const menuGroups: MenuGroup[] = [ + { + title: 'Overview', + icon: LayoutDashboard, + items: [ + { + path: '/admin/dashboard', + icon: LayoutDashboard, + label: 'Dashboard' + }, + ] }, - { - path: '/admin/users', - icon: Users, - label: 'Users' + { + title: 'Operations', + icon: Building2, + items: [ + { + path: '/admin/reception', + icon: LogIn, + label: 'Reception' + }, + { + path: '/admin/advanced-rooms', + icon: Hotel, + label: 'Room Management' + }, + ] }, - { - path: '/admin/guest-profiles', - icon: User, - label: 'Guest Profiles' + { + title: 'Business', + icon: TrendingUp, + items: [ + { + path: '/admin/business', + icon: FileText, + label: 'Business Dashboard' + }, + ] }, - { - path: '/admin/loyalty', - icon: Award, - label: 'Loyalty Program' + { + title: 'Analytics & Reports', + icon: BarChart3, + items: [ + { + path: '/admin/analytics', + icon: BarChart3, + label: 'Analytics' + }, + ] }, - { - path: '/admin/business', - icon: FileText, - label: 'Business' + { + title: 'Users & Guests', + icon: Users, + items: [ + { + path: '/admin/users', + icon: Users, + label: 'Users' + }, + { + path: '/admin/guest-profiles', + icon: User, + label: 'Guest Profiles' + }, + { + path: '/admin/group-bookings', + icon: UserCheck, + label: 'Group Bookings' + }, + { + path: '/admin/loyalty', + icon: Award, + label: 'Loyalty Program' + }, + ] }, - { - path: '/admin/reception', - icon: LogIn, - label: 'Reception' + { + title: 'Products & Pricing', + icon: Tag, + items: [ + { + path: '/admin/rate-plans', + icon: Tag, + label: 'Rate Plans' + }, + { + path: '/admin/packages', + icon: Package, + label: 'Packages' + }, + ] }, - { - path: '/admin/page-content', - icon: Globe, - label: 'Page Content' + { + title: 'Marketing', + icon: Mail, + items: [ + { + path: '/admin/email-campaigns', + icon: Mail, + label: 'Email Campaigns' + }, + ] }, - { - path: '/admin/analytics', - icon: BarChart3, - label: 'Analytics' + { + title: 'Content Management', + icon: Globe, + items: [ + { + path: '/admin/page-content', + icon: Globe, + label: 'Page Content' + }, + ] }, - { - path: '/admin/settings', - icon: Settings, - label: 'Settings' + { + title: 'System', + icon: Settings, + items: [ + { + path: '/admin/security', + icon: Shield, + label: 'Security' + }, + { + path: '/admin/tasks', + icon: CheckSquare, + label: 'Tasks' + }, + { + path: '/admin/workflows', + icon: Workflow, + label: 'Workflows' + }, + { + path: '/admin/notifications', + icon: Bell, + label: 'Notifications' + }, + { + path: '/admin/settings', + icon: Settings, + label: 'Settings' + }, + ] }, ]; const isActive = (path: string) => { - if (location.pathname === path) return true; - if (path === '/admin/settings' || path === '/admin/analytics' || path === '/admin/business' || path === '/admin/reception' || path === '/admin/page-content' || path === '/admin/loyalty') { + if (path === '/admin/settings' || path === '/admin/analytics' || path === '/admin/business' || path === '/admin/reception' || path === '/admin/advanced-rooms' || path === '/admin/page-content' || path === '/admin/loyalty') { return location.pathname === path; } - if (path === '/admin/reception') { + if (path === '/admin/reception' || path === '/admin/advanced-rooms') { return location.pathname === path || location.pathname.startsWith(`${path}/`); } @@ -148,168 +262,242 @@ const SidebarAdmin: React.FC = ({ return ( <> - {} - {isMobile && ( - - )} + {/* Mobile Menu Button - Always visible on mobile screens */} + - {} - {isMobile && isMobileOpen && ( + {/* Mobile Overlay */} + {isMobileOpen && (
)} - {} diff --git a/Frontend/src/components/layout/SidebarStaff.tsx b/Frontend/src/components/layout/SidebarStaff.tsx index 5e6de804..d8fa7a9d 100644 --- a/Frontend/src/components/layout/SidebarStaff.tsx +++ b/Frontend/src/components/layout/SidebarStaff.tsx @@ -13,10 +13,12 @@ import { CreditCard, MessageCircle, Award, - Users + Users, + Wrench } from 'lucide-react'; import useAuthStore from '../../store/useAuthStore'; import { useChatNotifications } from '../../contexts/ChatNotificationContext'; +import { useResponsive } from '../../hooks'; interface SidebarStaffProps { isCollapsed?: boolean; @@ -28,12 +30,12 @@ const SidebarStaff: React.FC = ({ onToggle }) => { const [internalCollapsed, setInternalCollapsed] = useState(false); - const [isMobile, setIsMobile] = useState(false); const [isMobileOpen, setIsMobileOpen] = useState(false); const location = useLocation(); const navigate = useNavigate(); const { logout } = useAuthStore(); const { unreadCount } = useChatNotifications(); + const { isMobile, isTablet, isDesktop } = useResponsive(); const handleLogout = async () => { try { @@ -47,19 +49,12 @@ const SidebarStaff: React.FC = ({ } }; - + // Close mobile menu when screen becomes desktop useEffect(() => { - const checkMobile = () => { - setIsMobile(window.innerWidth < 1024); - if (window.innerWidth >= 1024) { - setIsMobileOpen(false); - } - }; - - checkMobile(); - window.addEventListener('resize', checkMobile); - return () => window.removeEventListener('resize', checkMobile); - }, []); + if (isDesktop) { + setIsMobileOpen(false); + } + }, [isDesktop]); const isCollapsed = controlledCollapsed !== undefined @@ -116,6 +111,11 @@ const SidebarStaff: React.FC = ({ icon: Users, label: 'Guest Profiles' }, + { + path: '/staff/advanced-rooms', + icon: Wrench, + label: 'Room Management' + }, { path: '/staff/chats', icon: MessageCircle, diff --git a/Frontend/src/components/notifications/InAppNotificationBell.tsx b/Frontend/src/components/notifications/InAppNotificationBell.tsx new file mode 100644 index 00000000..1c5f0e5d --- /dev/null +++ b/Frontend/src/components/notifications/InAppNotificationBell.tsx @@ -0,0 +1,139 @@ +import React, { useState, useEffect } from 'react'; +import { Bell, X } from 'lucide-react'; +import { toast } from 'react-toastify'; +import notificationService, { Notification } from '../../services/api/notificationService'; +import { formatDate } from '../../utils/format'; + +const InAppNotificationBell: React.FC = () => { + const [notifications, setNotifications] = useState([]); + const [unreadCount, setUnreadCount] = useState(0); + const [showDropdown, setShowDropdown] = useState(false); + const [loading, setLoading] = useState(false); + + useEffect(() => { + loadNotifications(); + // Poll for new notifications every 30 seconds + const interval = setInterval(loadNotifications, 30000); + return () => clearInterval(interval); + }, []); + + const loadNotifications = async () => { + try { + const response = await notificationService.getMyNotifications({ + status: 'delivered', + limit: 10, + }); + const notifs = response.data.data || []; + setNotifications(notifs); + setUnreadCount(notifs.filter(n => !n.read_at).length); + } catch (error) { + // Silently fail + } + }; + + const handleMarkAsRead = async (notificationId: number) => { + try { + await notificationService.markAsRead(notificationId); + setNotifications(notifications.map(n => + n.id === notificationId ? { ...n, status: 'read' as any, read_at: new Date().toISOString() } : n + )); + setUnreadCount(Math.max(0, unreadCount - 1)); + } catch (error: any) { + toast.error(error.message || 'Failed to mark as read'); + } + }; + + const handleMarkAllAsRead = async () => { + try { + setLoading(true); + const unread = notifications.filter(n => !n.read_at); + await Promise.all(unread.map(n => notificationService.markAsRead(n.id))); + setNotifications(notifications.map(n => ({ ...n, status: 'read' as any, read_at: new Date().toISOString() }))); + setUnreadCount(0); + } catch (error: any) { + toast.error(error.message || 'Failed to mark all as read'); + } finally { + setLoading(false); + } + }; + + return ( +
+ + + {showDropdown && ( + <> +
setShowDropdown(false)} + /> +
+
+

Notifications

+ {unreadCount > 0 && ( + + )} +
+
+ {notifications.length === 0 ? ( +
+ No notifications +
+ ) : ( + notifications.map((notification) => ( +
{ + if (!notification.read_at) { + handleMarkAsRead(notification.id); + } + }} + > +
+
+

+ {notification.subject || notification.notification_type.replace('_', ' ')} +

+

+ {notification.content} +

+

+ {formatDate(new Date(notification.created_at), 'short')} +

+
+ {!notification.read_at && ( +
+ )} +
+
+ )) + )} +
+
+ + )} +
+ ); +}; + +export default InAppNotificationBell; + diff --git a/Frontend/src/components/notifications/NotificationPreferences.tsx b/Frontend/src/components/notifications/NotificationPreferences.tsx new file mode 100644 index 00000000..15836892 --- /dev/null +++ b/Frontend/src/components/notifications/NotificationPreferences.tsx @@ -0,0 +1,302 @@ +import React, { useState, useEffect } from 'react'; +import { Bell, Mail, MessageSquare, Smartphone, Save } from 'lucide-react'; +import { toast } from 'react-toastify'; +import { Loading } from '../common'; +import notificationService, { NotificationPreferences } from '../../services/api/notificationService'; + +const NotificationPreferences: React.FC = () => { + const [preferences, setPreferences] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + useEffect(() => { + loadPreferences(); + }, []); + + const loadPreferences = async () => { + try { + setLoading(true); + const response = await notificationService.getPreferences(); + setPreferences(response.data.data); + } catch (error: any) { + toast.error(error.message || 'Failed to load preferences'); + } finally { + setLoading(false); + } + }; + + const handleSave = async () => { + if (!preferences) return; + + try { + setSaving(true); + await notificationService.updatePreferences(preferences); + toast.success('Preferences saved successfully'); + } catch (error: any) { + toast.error(error.message || 'Failed to save preferences'); + } finally { + setSaving(false); + } + }; + + const updatePreference = (key: keyof NotificationPreferences, value: boolean) => { + if (preferences) { + setPreferences({ ...preferences, [key]: value }); + } + }; + + if (loading) { + return ; + } + + if (!preferences) { + return
Failed to load preferences
; + } + + return ( +
+
+

Notification Preferences

+ + {/* Global Channel Preferences */} +
+

Channel Preferences

+
+ + + + +
+
+ + {/* Type-Specific Preferences */} +
+
+

Booking Confirmations

+
+ + +
+
+ +
+

Payment Receipts

+
+ + +
+
+ +
+

Reminders

+
+
+

Pre-Arrival

+
+ + +
+
+
+

Check-In

+
+ + +
+
+
+

Check-Out

+
+ + +
+
+
+
+ +
+

Marketing & Updates

+
+
+

Marketing Campaigns

+
+ + +
+
+
+

Loyalty Updates

+
+ + +
+
+
+
+
+ +
+ +
+
+
+ ); +}; + +export default NotificationPreferences; + diff --git a/Frontend/src/components/notifications/NotificationTemplatesModal.tsx b/Frontend/src/components/notifications/NotificationTemplatesModal.tsx new file mode 100644 index 00000000..7345fa88 --- /dev/null +++ b/Frontend/src/components/notifications/NotificationTemplatesModal.tsx @@ -0,0 +1,210 @@ +import React, { useState, useEffect } from 'react'; +import { X, Plus, Trash2, Edit } from 'lucide-react'; +import { toast } from 'react-toastify'; +import notificationService, { NotificationTemplate } from '../../services/api/notificationService'; + +interface NotificationTemplatesModalProps { + onClose: () => void; +} + +const NotificationTemplatesModal: React.FC = ({ onClose }) => { + const [templates, setTemplates] = useState([]); + const [loading, setLoading] = useState(true); + const [showCreate, setShowCreate] = useState(false); + const [formData, setFormData] = useState({ + name: '', + notification_type: 'booking_confirmation', + channel: 'email', + subject: '', + content: '', + }); + + useEffect(() => { + loadTemplates(); + }, []); + + const loadTemplates = async () => { + try { + setLoading(true); + const response = await notificationService.getTemplates(); + setTemplates(response.data.data || []); + } catch (error: any) { + toast.error(error.message || 'Failed to load templates'); + } finally { + setLoading(false); + } + }; + + const handleCreate = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!formData.name.trim() || !formData.content.trim()) { + toast.error('Name and content are required'); + return; + } + + try { + await notificationService.createTemplate(formData); + toast.success('Template created successfully'); + setShowCreate(false); + setFormData({ + name: '', + notification_type: 'booking_confirmation', + channel: 'email', + subject: '', + content: '', + }); + loadTemplates(); + } catch (error: any) { + toast.error(error.message || 'Failed to create template'); + } + }; + + return ( +
+
+
+

Notification Templates

+
+ + +
+
+ +
+ {showCreate ? ( + +
+ + setFormData({ ...formData, name: e.target.value })} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" + required + /> +
+
+
+ + +
+
+ + +
+
+ {(formData.channel === 'email' || formData.channel === 'push') && ( +
+ + setFormData({ ...formData, subject: e.target.value })} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" + placeholder="Use {{variable_name}} for variables" + /> +
+ )} +
+ +

' : '\U0001d4ab', + '\\' : '\U0001d4ac', + '\\' : '\U0000211b', + '\\' : '\U0001d4ae', + '\\' : '\U0001d4af', + '\\' : '\U0001d4b0', + '\\' : '\U0001d4b1', + '\\' : '\U0001d4b2', + '\\' : '\U0001d4b3', + '\\' : '\U0001d4b4', + '\\' : '\U0001d4b5', + '\\' : '\U0001d5ba', + '\\' : '\U0001d5bb', + '\\' : '\U0001d5bc', + '\\' : '\U0001d5bd', + '\\' : '\U0001d5be', + '\\' : '\U0001d5bf', + '\\' : '\U0001d5c0', + '\\' : '\U0001d5c1', + '\\' : '\U0001d5c2', + '\\' : '\U0001d5c3', + '\\' : '\U0001d5c4', + '\\' : '\U0001d5c5', + '\\' : '\U0001d5c6', + '\\' : '\U0001d5c7', + '\\' : '\U0001d5c8', + '\\