diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 42874400..52ec75c3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,7 +14,7 @@ - [ ] I have read the [CONTRIBUTING] guide. - [ ] I have added tests that prove my fix is effective or that my feature works. - [ ] If appropriate, I have added necessary documentation. -- [ ] For user-facing changes, refactorings, performance improvements or documentation updates, I have added a changelog entry using the [Changie] tool. +- [ ] For user-facing changes, refactorings, performance improvements or documentation updates, I have added a changelog entry using [Changie]. For both the title of the PR and the changelog entry, prefer simple past tense or constructions with "now". For example: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b1f3dfd5..e4008fb7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -45,7 +45,7 @@ env: jobs: tests: - name: "Test ${{ matrix.python-version }}/ ${{ matrix.os }}" + name: "Test ${{ matrix.python-version }} ${{ matrix.nightly && '(nightly) ' || '' }}/ ${{ matrix.os }}" runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.experimental || false }} env: @@ -60,13 +60,15 @@ jobs: - "3.10" - "3.11" - "3.12" + - "3.13" - "pypy3.10" os: ["ubuntu-latest"] include: - - python-version: "3.13" + - python-version: "3.14" os: "ubuntu-latest" session: "tests" experimental: true + nightly: true - python-version: "3.12" os: "windows-latest" @@ -83,6 +85,7 @@ jobs: fetch-tags: true - name: Setup Python ${{ matrix.python-version }} + if: "${{ !matrix.nightly }}" uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d with: python-version: ${{ matrix.python-version }} @@ -93,6 +96,12 @@ jobs: pyproject.toml .github/workflows/constraint.txt + - name: Setup Python ${{ matrix.python-version }} (nightly) + if: "${{ matrix.nightly }}" + uses: deadsnakes/action@241105c567c5b7600b24062b638324ff3ae8f4cd + with: + python-version: "${{ matrix.python-version }}-dev" + - name: Install tools uses: ./.github/actions/install-tools @@ -136,7 +145,8 @@ jobs: cache: pip cache-dependency-path: | pyproject.toml - .github/workflows/constraint.txt + requirements/*.txt + .github/workflows/constraints.txt - name: Install tools uses: ./.github/actions/install-tools diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7b6ea1fd..51823c75 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.28.3 + rev: 0.28.4 hooks: - id: check-dependabot - id: check-github-workflows @@ -66,14 +66,20 @@ repos: args: [--all] - repo: https://github.com/tox-dev/pyproject-fmt - rev: "1.8.0" + rev: "2.1.3" hooks: - id: pyproject-fmt - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.1.42 + rev: 0.1.45 hooks: - id: pip-compile files: ^pyproject\.toml$ - args: ["pyproject.toml", "--extra", "docs", "-o", "docs/requirements.txt"] + args: ["pyproject.toml", "--pre", "--python-version", "3.12", "--extra", "docs", "-o", "docs/requirements.txt"] language_version: python3.12 + - id: pip-compile + files: ^pyproject\.toml$ + args: ["pyproject.toml", "--resolution", "highest", "-o", "requirements/requirements-highest.txt"] + - id: pip-compile + files: ^pyproject\.toml$ + args: ["pyproject.toml", "--resolution", "lowest-direct", "-o", "requirements/requirements-lowest-direct.txt"] diff --git a/README.md b/README.md index 7850b2bd..a200c451 100644 --- a/README.md +++ b/README.md @@ -126,9 +126,11 @@ If you'd like to contribute to this project, please see the [contributing guide] ## Credits +- The [LimeSurvey][limesurvey-site] team for providing a great survey platform. - [Markus Opolka][martialblog] for maintaining a very robust set of [LimeSurvey Docker images](https://github.com/martialblog/docker-limesurvey/). - [Claudio Jolowicz][claudio] and [his amazing blog post][hypermodern]. [claudio]: https://twitter.com/cjolowicz/ [hypermodern]: https://cjolowicz.github.io/posts/hypermodern-python-01-setup/ +[limesurvey-site]: https://www.limesurvey.org/ [martialblog]: https://github.com/martialblog/ diff --git a/code_samples/duckdb_sql.py b/code_samples/duckdb_sql.py index 3f942ea1..8f2158ea 100644 --- a/code_samples/duckdb_sql.py +++ b/code_samples/duckdb_sql.py @@ -2,7 +2,7 @@ from __future__ import annotations -# ruff: noqa: I001, PTH123 +# ruff: noqa: I001, PTH123, FURB103 # start example from pathlib import Path diff --git a/docs/index.md b/docs/index.md index 0f603543..7b7ea74b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,13 +22,12 @@ Release **v{sub-ref}`version`**. ([What's new?](./changelog.md)) Integration tests are run against a LimeSurvey instance, and both PostgreSQL and MySQL backends, using Docker Compose. The following versions of LimeSurvey were tested for this release: -- {ls_tag}`6.4.5+240205` -- {ls_tag}`6.4.4+240130` -- {ls_tag}`6.4.3+240122` -- {ls_tag}`6.4.2+240115` -- {ls_tag}`5.6.53+240131` -- {ls_tag}`5.6.52+240123` -- {ls_tag}`5.6.51+240116` +- {ls_tag}`6.5.3+240415` +- {ls_tag}`6.5.1+240320` +- {ls_tag}`6.5.0+240319` +- {ls_tag}`5.6.59+240416` +- {ls_tag}`5.6.57+240312` +- {ls_tag}`5.6.56+240227` But also, the latest 5.x and 6.x are tested continuously and are expected to work. diff --git a/docs/requirements.txt b/docs/requirements.txt index 2056dab4..38b048dd 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,29 +1,40 @@ # This file was autogenerated by uv via the following command: -# uv pip compile pyproject.toml --extra docs -o docs/requirements.txt +# uv pip compile pyproject.toml --pre --python-version 3.12 --extra docs -o docs/requirements.txt alabaster==0.7.16 # via sphinx -anyascii==0.3.2 - # via sphinx-autoapi -astroid==3.1.0 +anyio==4.3.0 + # via + # starlette + # watchfiles +astroid==3.2.1 # via sphinx-autoapi autodocsumm==0.2.12 -babel==2.14.0 + # via citric (pyproject.toml) +babel==2.15.0 # via sphinx -beautifulsoup4==4.12.3 +beautifulsoup4==4.13.0b2 # via furo certifi==2024.2.2 # via requests charset-normalizer==3.3.2 # via requests +click==8.1.7 + # via uvicorn colorama==0.4.6 # via sphinx-autobuild -docutils==0.20.1 +docutils==0.21.2 # via + # citric (pyproject.toml) # myst-parser # sphinx -furo==2024.1.29 +furo==2024.5.6 + # via citric (pyproject.toml) +h11==0.14.0 + # via uvicorn idna==3.7 - # via requests + # via + # anyio + # requests imagesize==1.4.1 # via sphinx jinja2==3.1.4 @@ -31,22 +42,21 @@ jinja2==3.1.4 # myst-parser # sphinx # sphinx-autoapi -livereload==2.6.3 - # via sphinx-autobuild markdown-it-py==3.0.0 # via # mdit-py-plugins # myst-parser markupsafe==2.1.5 # via jinja2 -mdit-py-plugins==0.4.0 +mdit-py-plugins==0.4.1 # via myst-parser mdurl==0.1.2 # via markdown-it-py -myst-parser==2.0.0 +myst-parser==3.0.1 + # via citric (pyproject.toml) packaging==24.0 # via sphinx -pygments==2.17.2 +pygments==2.18.0 # via # furo # sphinx @@ -55,15 +65,18 @@ pyyaml==6.0.1 # myst-parser # sphinx-autoapi requests==2.31.0 - # via sphinx -six==1.16.0 - # via livereload + # via + # citric (pyproject.toml) + # sphinx +sniffio==1.3.1 + # via anyio snowballstemmer==2.2.0 # via sphinx soupsieve==2.5 # via beautifulsoup4 -sphinx==7.2.6 +sphinx==7.3.7 # via + # citric (pyproject.toml) # autodocsumm # furo # myst-parser @@ -74,13 +87,18 @@ sphinx==7.2.6 # sphinx-hoverxref # sphinx-notfound-page # sphinxcontrib-jquery -sphinx-autoapi==3.0.0 -sphinx-autobuild==2024.2.4 +sphinx-autoapi==3.1.0b0 + # via citric (pyproject.toml) +sphinx-autobuild==2024.4.16 + # via citric (pyproject.toml) sphinx-basic-ng==1.0.0b2 # via furo sphinx-copybutton==0.5.2 + # via citric (pyproject.toml) sphinx-hoverxref==1.3.0 + # via citric (pyproject.toml) sphinx-notfound-page==1.0.0 + # via citric (pyproject.toml) sphinxcontrib-applehelp==1.0.8 # via sphinx sphinxcontrib-devhelp==1.0.6 @@ -95,7 +113,15 @@ sphinxcontrib-qthelp==1.0.7 # via sphinx sphinxcontrib-serializinghtml==1.1.10 # via sphinx -tornado==6.4 - # via livereload +starlette==0.37.2 + # via sphinx-autobuild +typing-extensions==4.12.0rc1 + # via beautifulsoup4 urllib3==2.2.1 # via requests +uvicorn==0.29.0 + # via sphinx-autobuild +watchfiles==0.21.0 + # via sphinx-autobuild +websockets==12.0 + # via sphinx-autobuild diff --git a/noxfile.py b/noxfile.py index 7fbc1be0..c8b889e6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -11,7 +11,16 @@ GH_ACTIONS_ENV_VAR = "GITHUB_ACTIONS" FORCE_COLOR = "FORCE_COLOR" -nox.options.default_venv_backend = "uv" +nox.options.sessions = [ + "tests", + "xdoctest", + "deps", + "mypy", + "docs-build", + "api", +] +nox.needs_version = ">=2024.4.15" +nox.options.default_venv_backend = "uv|virtualenv" package = "citric" @@ -20,7 +29,7 @@ all_python_versions = python_versions + pypy_versions main_cpython_version = "3.12" -main_pypy_version = "pypy3.9" +main_pypy_version = "pypy3.10" locations = "src", "tests", "noxfile.py", "docs/conf.py" @@ -34,10 +43,16 @@ def _run_tests(session: nox.Session, *args: str) -> None: session.notify("coverage", posargs=[]) -@nox.session(python=all_python_versions, tags=["test"]) -def tests(session: nox.Session) -> None: +@nox.session(python=python_versions, tags=["test"]) +@nox.parametrize("constraints", ["highest", "lowest-direct"]) +def tests(session: nox.Session, constraints: str) -> None: """Execute pytest tests and compute coverage.""" - session.install("-v", "citric[tests] @ .") + session.install( + "-v", + "citric[tests] @ .", + "-c", + f"requirements/requirements-{constraints}.txt", + ) args = session.posargs or ["-m", "not integration_test"] _run_tests(session, *args) diff --git a/pyproject.toml b/pyproject.toml index 5e8c02de..0b704466 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,9 +13,13 @@ keywords = [ "json-rpc", "limesurvey", ] -license = {file = "LICENSE"} -maintainers = [{ name = "Edgar Ramírez-Mondragón", email = "edgarrm358@gmail.com" }] -authors = [{ name = "Edgar Ramírez-Mondragón", email = "edgarrm358@gmail.com" }] +license = { file = "LICENSE" } +maintainers = [ + { name = "Edgar Ramírez-Mondragón", email = "edgarrm358@gmail.com" }, +] +authors = [ + { name = "Edgar Ramírez-Mondragón", email = "edgarrm358@gmail.com" }, +] requires-python = ">=3.8" classifiers = [ "Development Status :: 5 - Production/Stable", @@ -47,23 +51,23 @@ dependencies = [ optional-dependencies.dev = [ "citric[docs,tests,typing]", "colorama>=0.4.6", + "deptry>=0.12", "requests-cache>=1.1", ] optional-dependencies.docs = [ - "autodocsumm>=0.2.5", # 0.2.4 has a problematic dependency 'Sphinx>=2.2.*' - "docutils", - "furo", - "myst-parser", - "sphinx", - "sphinx-autoapi", - "sphinx-autobuild", - "sphinx-copybutton", - "sphinx-hoverxref", - "sphinx-notfound-page", + "autodocsumm>=0.2.5", # 0.2.4 has a problematic dependency 'Sphinx>=2.2.*' + "docutils>=0.20", + "furo>=2024.1.29", + "myst-parser>=2", + "sphinx>=7", + "sphinx-autoapi>=3", + "sphinx-autobuild>=2021.3.14", + "sphinx-copybutton>=0.5.2", + "sphinx-hoverxref>=1.3", + "sphinx-notfound-page>=1", ] optional-dependencies.tests = [ "coverage[toml]>=7.4.2", - "deptry>=0.12", "faker>=19", "pytest>=8", "pytest-github-actions-annotate-failures>=0.1.7", @@ -80,7 +84,7 @@ optional-dependencies.typing = [ "mypy>=1.9", "sphinx", "types-requests>=2.31.0.2", - 'typing-extensions>=4.6; python_version < "3.12"', + "typing-extensions>=4.6; python_version<'3.12'", ] urls.Documentation = "https://citric.readthedocs.io" urls.Homepage = 'https://github.com/edgarrmondragon/citric' @@ -91,146 +95,142 @@ urls.Repository = "https://github.com/edgarrmondragon/citric" source = "vcs" [tool.ruff] -include = ["*.py", "*.pyi", "**/pyproject.toml", "*.ipynb"] line-length = 88 -src = ["src", "tests", "docs"] - -[tool.ruff.lint] -explicit-preview-rules = false -ignore = [ - "ANN101", # missing-type-self - "DJ", # flake8-django - "FIX002", # line-contains-todo - "COM812", # missing-trailing-comma - "ISC001", # single-line-implicit-string-concatenation - "D107", # undocumented-public-init -] -preview = true -select = [ - "F", # Pyflakes - "E", # pycodestyle (error) - "W", # pycodestyle (warning) - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "S", # bandit - "BLE", # flake8-blind-except - "FBT", # flake8-boolean-trap - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "EM", # flake8-errmsg - "FA", # flake8-future-annotations - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "G", # flake8-logging-format - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PT", # flake8-pytest-style - "Q", # flake8-quotes - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "TD", # flake8-todos - "FIX", # flake8-fixme - "ERA", # flake8-eradicate - "PD", # pandas-vet - "PGH", # pygrep-hooks - "PLC", # pylint - "PLE", # pylint - "PLR", # pylint - "PLW", # pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # perflint - "FURB", # refurb - "LOG", # flake8-logging - "RUF", # Ruff-specific rules -] -unfixable = [ - "ERA", # Don't remove commented out code +src = [ + "docs", + "src", + "tests", ] -[tool.ruff.format] -docstring-code-format = true +include = [ + "**/pyproject.toml", + "*.ipynb", + "*.py", + "*.pyi", +] +format.preview = true +format.docstring-code-format = true # Enable preview style formatting. -preview = true - -[tool.ruff.lint.per-file-ignores] -"docs/notebooks/*" = [ +lint.select = [ + "A", # flake8-builtins + "ANN", # flake8-annotations + "ARG", # flake8-unused-arguments + "B", # flake8-bugbear + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "C90", # mccabe + "COM", # flake8-commas + "D", # pydocstyle + "DTZ", # flake8-datetimez + "E", # pycodestyle (error) + "EM", # flake8-errmsg + "ERA", # flake8-eradicate + "F", # Pyflakes + "FA", # flake8-future-annotations + "FBT", # flake8-boolean-trap + "FIX", # flake8-fixme + "FLY", # flynt + "FURB", # refurb + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "INP", # flake8-no-pep420 + "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging + "N", # pep8-naming + "PD", # pandas-vet + "PERF", # perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PLC", # pylint + "PLE", # pylint + "PLR", # pylint + "PLW", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "Q", # flake8-quotes + "RET", # flake8-return + "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "S", # bandit + "SIM", # flake8-simplify + "SLF", # flake8-self + "SLOT", # flake8-slots + "T10", # flake8-debugger + "T20", # flake8-print + "TCH", # flake8-type-checking + "TD", # flake8-todos + "TID", # flake8-tidy-imports + "TRY", # tryceratops + "UP", # pyupgrade + "W", # pycodestyle (warning) + "YTT", # flake8-2020 +] +lint.ignore = [ + "ANN101", # missing-type-self + "COM812", # missing-trailing-comma + "D107", # undocumented-public-init + "DJ", # flake8-django + "FIX002", # line-contains-todo + "ISC001", # single-line-implicit-string-concatenation +] +lint.explicit-preview-rules = false +lint.per-file-ignores."docs/notebooks/*" = [ "D100", # undocumented-public-module - "INP001", # implicit-namespace-package - "I002", # missing-required-import "E402", # module-import-not-at-top-of-file + "I002", # missing-required-import + "INP001", # implicit-namespace-package ] -"src/*" = [ +lint.per-file-ignores."src/*" = [ "PD", # pandas-vet ] -"tests/*" = [ +lint.per-file-ignores."tests/*" = [ + "ANN201", # missing-return-type-undocumented-public-function "ARG00", # unused-method-argument "C901", # complex-structure + "PLR2004", # magic-value-comparison + "PLR6301", # no-self-use "S101", # assert "S105", # hardcoded-password-string "S106", # hardcoded-password-func-arg - "ANN201", # missing-return-type-undocumented-public-function - "PLR2004", # magic-value-comparison "SLF001", # private-member-access - "PLR6301", # no-self-use ] - -[tool.ruff.lint.flake8-quotes] -docstring-quotes = "double" -inline-quotes = "double" -multiline-quotes = "double" - -[tool.ruff.lint.flake8-annotations] -allow-star-arg-any = true -mypy-init-return = true -suppress-dummy-args = true - -[tool.ruff.lint.flake8-errmsg] -max-string-length = 30 - -[tool.ruff.lint.flake8-import-conventions] -banned-from = ["typing"] - -[tool.ruff.lint.flake8-import-conventions.extend-aliases] -typing = "t" - -[tool.ruff.lint.flake8-pytest-style] -fixture-parentheses = false -mark-parentheses = false - -[tool.ruff.lint.isort] -known-first-party = ["citric"] -required-imports = ["from __future__ import annotations"] - -[tool.ruff.lint.mccabe] -max-complexity = 5 - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.pylint] -max-args = 10 +lint.unfixable = [ + "ERA", # Don't remove commented out code +] +lint.flake8-annotations.allow-star-arg-any = true +lint.flake8-annotations.mypy-init-return = true +lint.flake8-annotations.suppress-dummy-args = true +lint.flake8-errmsg.max-string-length = 30 +lint.flake8-import-conventions.banned-from = [ + "typing", +] +lint.flake8-import-conventions.extend-aliases.typing = "t" +lint.flake8-pytest-style.fixture-parentheses = false +lint.flake8-pytest-style.mark-parentheses = false +lint.flake8-quotes.docstring-quotes = "double" +lint.flake8-quotes.inline-quotes = "double" +lint.flake8-quotes.multiline-quotes = "double" +lint.isort.known-first-party = [ + "citric", +] +lint.isort.required-imports = [ + "from __future__ import annotations", +] +lint.mccabe.max-complexity = 5 +lint.pydocstyle.convention = "google" +lint.pylint.max-args = 10 +lint.preview = true [tool.codespell] skip = ".mypy_cache,.nox,.ruff_cache,build,docs/index.md,docs/requirements.txt" +[tool.deptry] +pep621_dev_dependency_groups = [ + "dev", + "docs", +] + [tool.deptry.package_module_name_map] types-requests = "requests" typing-extensions = "typing_extensions" @@ -244,36 +244,21 @@ DEP001 = [ "sqlalchemy", ] DEP002 = [ - "autodocsumm", - "citric", - "colorama", "coverage", - "deptry", "faker", - "furo", - "jupyterlab", "mypy", - "myst-parser", "pytest", "pytest-github-actions-annotate-failures", "pytest-httpserver", "pytest-reverse", "pytest-subtests", "python-dotenv", - "requests-cache", "semver", - "sphinx", - "sphinx-autoapi", - "sphinx-autobuild", - "sphinx-autodoc-typehints", - "sphinx-copybutton", - "sphinx-hoverxref", - "sphinx-notfound-page", "tinydb", "xdoctest", ] DEP004 = [ - "typing_extensions", + "docutils", ] [tool.pyproject-fmt] @@ -302,13 +287,19 @@ package = [ [tool.coverage.run] branch = true parallel = true -source = ["citric"] +source = [ + "citric", +] relative_files = true [tool.coverage.report] -exclude_also = ['''if (t\.)?TYPE_CHECKING:'''] +exclude_also = [ + '''if (t\.)?TYPE_CHECKING:''', +] fail_under = 85 -omit = ["src/citric/types.py"] +omit = [ + "src/citric/types.py", +] precision = 2 show_missing = true @@ -324,5 +315,5 @@ warn_unused_ignores = true ignore_missing_imports = true module = [ "nox.*", - "pytest_subtests.*", # TODO: Remove after https://github.com/pytest-dev/pytest-subtests/pull/115 is published + "pytest_subtests.*", # TODO: Remove after https://github.com/pytest-dev/pytest-subtests/pull/115 is published ] diff --git a/requirements/requirements-highest.txt b/requirements/requirements-highest.txt new file mode 100644 index 00000000..3f208699 --- /dev/null +++ b/requirements/requirements-highest.txt @@ -0,0 +1,12 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml --resolution highest -o requirements/requirements-highest.txt +certifi==2024.2.2 + # via requests +charset-normalizer==3.3.2 + # via requests +idna==3.7 + # via requests +requests==2.31.0 + # via citric (pyproject.toml) +urllib3==2.2.1 + # via requests diff --git a/requirements/requirements-lowest-direct.txt b/requirements/requirements-lowest-direct.txt new file mode 100644 index 00000000..9bf8d71f --- /dev/null +++ b/requirements/requirements-lowest-direct.txt @@ -0,0 +1,12 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml --resolution lowest-direct -o requirements/requirements-lowest-direct.txt +certifi==2024.2.2 + # via requests +chardet==4.0.0 + # via requests +idna==2.10 + # via requests +requests==2.25.1 + # via citric (pyproject.toml) +urllib3==1.26.18 + # via requests